Compare commits
353 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
715982e40a | ||
|
|
d00cd4453a | ||
|
|
429c08c24a | ||
|
|
6a71490e20 | ||
|
|
9bceda0646 | ||
|
|
a1027a6773 | ||
|
|
302d4b75f9 | ||
|
|
5f6ee0e883 | ||
|
|
27f9720de1 | ||
|
|
22aa3fdbbc | ||
|
|
069ecdd33f | ||
|
|
dd545ae933 | ||
|
|
6650b705c4 | ||
|
|
59b0350289 | ||
|
|
1ad159f820 | ||
|
|
0bf42190e9 | ||
|
|
d2fa836232 | ||
|
|
c387774093 | ||
|
|
e99736ba3c | ||
|
|
16cb54fcc9 | ||
|
|
5aa15c51ec | ||
|
|
a8aedd9cf3 | ||
|
|
b851b632bc | ||
|
|
541e07fb65 | ||
|
|
6ad16a897d | ||
|
|
72f1053a93 | ||
|
|
fb15a2762c | ||
|
|
9165248b91 | ||
|
|
add18b29db | ||
|
|
1971653548 | ||
|
|
392cd64d7b | ||
|
|
b5affbb7c8 | ||
|
|
71d1206277 | ||
|
|
26e6a8c409 | ||
|
|
eb54fae11a | ||
|
|
ee773e5966 | ||
|
|
7218ccdba8 | ||
|
|
332400e48a | ||
|
|
ad1a5d3702 | ||
|
|
3006b4184d | ||
|
|
84eb84a080 | ||
|
|
60beea548b | ||
|
|
5f9c149e59 | ||
|
|
53367c6f04 | ||
|
|
d7f817ee44 | ||
|
|
d33a87da54 | ||
|
|
3aebfb12b7 | ||
|
|
1d6c55ffa6 | ||
|
|
5e7080aac3 | ||
|
|
fad739bc01 | ||
|
|
c6b7f23884 | ||
|
|
a6f7e446de | ||
|
|
89d95d3ae1 | ||
|
|
764208698f | ||
|
|
57129cf934 | ||
|
|
aae1a842d5 | ||
|
|
623f35aec7 | ||
|
|
870bf842cf | ||
|
|
07f2d7dd5c | ||
|
|
f223f2edc5 | ||
|
|
e848a9a577 | ||
|
|
7569d98e07 | ||
|
|
596dee2f24 | ||
|
|
9970403964 | ||
|
|
07a88ae00d | ||
|
|
5475b4d287 | ||
|
|
6631dcfd3e | ||
|
|
0dd3f337f3 | ||
|
|
8eb27b5875 | ||
|
|
2d1863031c | ||
|
|
9feb76ca81 | ||
|
|
993e8f4ab3 | ||
|
|
e08ae95d4f | ||
|
|
15359e8846 | ||
|
|
d1457b312b | ||
|
|
c9dd2af196 | ||
|
|
564ef4e688 | ||
|
|
a33e6e8bb5 | ||
|
|
cf34f33f04 | ||
|
|
827cfe4e8f | ||
|
|
2ce1c2383c | ||
|
|
6fc0a665ae | ||
|
|
4f16d01263 | ||
|
|
67cc37354a | ||
|
|
e388243ef4 | ||
|
|
3dc92763c7 | ||
|
|
dfe97dd466 | ||
|
|
2803cee29b | ||
|
|
3a03020e54 | ||
|
|
64443cc703 | ||
|
|
4d1aa6ed18 | ||
|
|
84837e88d2 | ||
|
|
ff49c936ea | ||
|
|
e6e0901329 | ||
|
|
23b6284b51 | ||
|
|
33dfbcbe32 | ||
|
|
700c23d537 | ||
|
|
369fac9e38 | ||
|
|
2229eb1167 | ||
|
|
a3dec841b6 | ||
|
|
b17620bdb6 | ||
|
|
f39cd5ae2f | ||
|
|
83a19e005b | ||
|
|
a9dd01b0c8 | ||
|
|
eb59afa1d1 | ||
|
|
2adcfce9d0 | ||
|
|
314ab9b304 | ||
|
|
8576fb82c7 | ||
|
|
0f95a6bb2f | ||
|
|
ad5104567d | ||
|
|
ece68ba1d5 | ||
|
|
acccd3a586 | ||
|
|
8ebef1c1ca | ||
|
|
28abc0d5ed | ||
|
|
1efe25d3ec | ||
|
|
c40e4f8e4b | ||
|
|
baca84092d | ||
|
|
346d4da059 | ||
|
|
ade64d6c0a | ||
|
|
8204bdfc5f | ||
|
|
1a9bb3e986 | ||
|
|
49356479e5 | ||
|
|
c44e9a7292 | ||
|
|
21771a593f | ||
|
|
84458dfc4c | ||
|
|
5835632dab | ||
|
|
67aa7229ef | ||
|
|
b72dc3ed3a | ||
|
|
0f93d4a5bd | ||
|
|
106320b035 | ||
|
|
63951705cd | ||
|
|
a8d56921d5 | ||
|
|
10bc133cf1 | ||
|
|
adeb5b35c9 | ||
|
|
589ff46ea5 | ||
|
|
656fcb9fe7 | ||
|
|
1cb9353006 | ||
|
|
57bf16ba07 | ||
|
|
659846ed88 | ||
|
|
25894044e0 | ||
|
|
e7a0826beb | ||
|
|
1f7ddee23b | ||
|
|
7e186730db | ||
|
|
6713a50208 | ||
|
|
7c9d8fcfec | ||
|
|
33bfc8cfe8 | ||
|
|
ca735bc14a | ||
|
|
4ba748a18b | ||
|
|
f1845106f8 | ||
|
|
67e7156c4b | ||
|
|
4a476adebf | ||
|
|
918798f8cc | ||
|
|
5a3f868866 | ||
|
|
feea2c6396 | ||
|
|
707b4c46d9 | ||
|
|
89ca39fc2b | ||
|
|
204281b12d | ||
|
|
a8538a7e95 | ||
|
|
dee1b471e9 | ||
|
|
aa04e9b01f | ||
|
|
350f0dc604 | ||
|
|
6021f2efd6 | ||
|
|
51838ec25a | ||
|
|
54768a121e | ||
|
|
8ff72cdca3 | ||
|
|
2cb53ad06b | ||
|
|
b8349de31d | ||
|
|
d7e11af7f8 | ||
|
|
dd8d39e698 | ||
|
|
afb1316daa | ||
|
|
04d7017536 | ||
|
|
6a1c75b060 | ||
|
|
5c94611f3b | ||
|
|
4e5676e80f | ||
|
|
c96d688a9c | ||
|
|
804242e9a5 | ||
|
|
0ec9760b17 | ||
|
|
d481ae3da4 | ||
|
|
4742c14fc1 | ||
|
|
509b0d501b | ||
|
|
d4c9b04d4e | ||
|
|
16fb4d331b | ||
|
|
e9e5bf31a7 | ||
|
|
221418120e | ||
|
|
46f852e26e | ||
|
|
4234cf0a31 | ||
|
|
7f3daea648 | ||
|
|
2eb16c82f4 | ||
|
|
e00b2ce591 | ||
|
|
d71e1311ca | ||
|
|
2cf16963e3 | ||
|
|
10bf7b7fb4 | ||
|
|
182c85a228 | ||
|
|
94b1988b90 | ||
|
|
6f7e62e9a0 | ||
|
|
aa7076af04 | ||
|
|
c928e8f0d4 | ||
|
|
5c6b106f68 | ||
|
|
d45bcea1ff | ||
|
|
6ff2dc79f8 | ||
|
|
b752329987 | ||
|
|
f21465335a | ||
|
|
0801adfc4b | ||
|
|
5bee8052d5 | ||
|
|
68dca5dfef | ||
|
|
3f51dd1d2f | ||
|
|
7f80889d77 | ||
|
|
efc61c0222 | ||
|
|
6fc0a05d34 | ||
|
|
a9be872d7a | ||
|
|
6ca85f099e | ||
|
|
86ff677b8a | ||
|
|
35e295df86 | ||
|
|
cd4d301790 | ||
|
|
93bb329c3d | ||
|
|
7c1e0f2c30 | ||
|
|
b57f471f44 | ||
|
|
252a9a2ed6 | ||
|
|
7258d4d787 | ||
|
|
75522fa295 | ||
|
|
4ba8f41d95 | ||
|
|
f326f8e4de | ||
|
|
f863dc058e | ||
|
|
20891db251 | ||
|
|
f1d05f1342 | ||
|
|
8dd636b0eb | ||
|
|
6b5bda8ee1 | ||
|
|
ddc5597157 | ||
|
|
ae112c7257 | ||
|
|
c22f10f96a | ||
|
|
18d10c9bec | ||
|
|
890e430cb7 | ||
|
|
dadc3d4cd7 | ||
|
|
d98b4d7320 | ||
|
|
340f532238 | ||
|
|
7669f68e7c | ||
|
|
3557e5514f | ||
|
|
a9f09b7614 | ||
|
|
845b9e4568 | ||
|
|
24a6092dcf | ||
|
|
195ae7d8b1 | ||
|
|
a5c6ea7ffc | ||
|
|
eb7a4ac29f | ||
|
|
508ef73fde | ||
|
|
838d6d8076 | ||
|
|
762c3159b8 | ||
|
|
7a88a06bcf | ||
|
|
0b1e3d7de5 | ||
|
|
9a83c73f21 | ||
|
|
aa50c7b268 | ||
|
|
179a5a80f4 | ||
|
|
0ddae527ef | ||
|
|
ee7a46de26 | ||
|
|
95522fda74 | ||
|
|
e58881c2bd | ||
|
|
36a902a44e | ||
|
|
16b74549a2 | ||
|
|
da7ededfb1 | ||
|
|
790bb08718 | ||
|
|
e6765f421f | ||
|
|
7e8f1fe904 | ||
|
|
eacce4578a | ||
|
|
07b2543972 | ||
|
|
d1c3fc8493 | ||
|
|
f453b16010 | ||
|
|
05151d8978 | ||
|
|
8218e1acc3 | ||
|
|
30212fc89a | ||
|
|
b31c13fcae | ||
|
|
6b95fc6f1d | ||
|
|
369cf17eb2 | ||
|
|
4dd8f512cc | ||
|
|
26cfec7d80 | ||
|
|
67a87ccf00 | ||
|
|
667cebcf94 | ||
|
|
bc1747ca1c | ||
|
|
945d8647bf | ||
|
|
dfe2e94627 | ||
|
|
09a5591eec | ||
|
|
f2bf06a0ba | ||
|
|
eedad4ab1c | ||
|
|
336a62ab29 | ||
|
|
b5603a5233 | ||
|
|
73890f553c | ||
|
|
f6243b8968 | ||
|
|
3770dc74d4 | ||
|
|
45f4e947c5 | ||
|
|
9928d7c6e1 | ||
|
|
bf776eeb2b | ||
|
|
ae7c0e9195 | ||
|
|
e90b640602 | ||
|
|
ba7529d3f5 | ||
|
|
34667f252e | ||
|
|
d18bddcb7b | ||
|
|
96dff49d33 | ||
|
|
b389728338 | ||
|
|
cdc7da86f3 | ||
|
|
4745cc0378 | ||
|
|
434f132479 | ||
|
|
fb0f31ffc7 | ||
|
|
bb1d73c0ae | ||
|
|
0e823d1191 | ||
|
|
48f4199ff3 | ||
|
|
eaf379587b | ||
|
|
672446b7d1 | ||
|
|
dfe52c1b07 | ||
|
|
d63df03ad8 | ||
|
|
aba4f9f2ce | ||
|
|
ac5c1e7803 | ||
|
|
d521dbf50e | ||
|
|
f210ed3e6a | ||
|
|
df3cac4ea6 | ||
|
|
f778c5175b | ||
|
|
6c66ff28dd | ||
|
|
d5b6ec702b | ||
|
|
c62a5fcef2 | ||
|
|
59c47e9200 | ||
|
|
4ba44d8932 | ||
|
|
27dae05e1b | ||
|
|
a251ae9b90 | ||
|
|
7e960b2bde | ||
|
|
5df4825158 | ||
|
|
8984d06d93 | ||
|
|
eed7aac047 | ||
|
|
54b068de4a | ||
|
|
f0f33b00b6 | ||
|
|
1043405088 | ||
|
|
0131b10805 | ||
|
|
a19b441f62 | ||
|
|
28edc31d43 | ||
|
|
0f9872a818 | ||
|
|
76ce4296f3 | ||
|
|
3dd2671380 | ||
|
|
298ca31332 | ||
|
|
8f911aa6b9 | ||
|
|
82a5c7d9b1 | ||
|
|
7f013dcdba | ||
|
|
68e2e16076 | ||
|
|
ea23c763c9 | ||
|
|
5dcecb3206 | ||
|
|
5bd48e2d0e | ||
|
|
afd0a02589 | ||
|
|
2379192d53 | ||
|
|
a6489290c8 | ||
|
|
5f74c43415 | ||
|
|
aa8b84a302 | ||
|
|
b987d041b0 | ||
|
|
b62e37307e | ||
|
|
61a59aa6ac | ||
|
|
f79ec27f1d | ||
|
|
b993fe380f | ||
|
|
d974b5f55f | ||
|
|
1be6f8f87a |
@@ -8,7 +8,7 @@ ENV VIRTUAL_ENV ${WORKSPACE_DIR}/api/tacticalrmm/env
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
EXPOSE 8000 8383
|
||||
EXPOSE 8000 8383 8005
|
||||
|
||||
RUN groupadd -g 1000 tactical && \
|
||||
useradd -u 1000 -g 1000 tactical
|
||||
|
||||
@@ -115,7 +115,10 @@ services:
|
||||
redis-dev:
|
||||
container_name: trmm-redis-dev
|
||||
restart: always
|
||||
command: redis-server --appendonly yes
|
||||
image: redis:6.0-alpine
|
||||
volumes:
|
||||
- redis-data-dev:/data
|
||||
networks:
|
||||
dev:
|
||||
aliases:
|
||||
@@ -227,11 +230,27 @@ services:
|
||||
volumes:
|
||||
- tactical-data-dev:/opt/tactical
|
||||
|
||||
mkdocs-dev:
|
||||
container_name: trmm-mkdocs-dev
|
||||
image: api-dev
|
||||
restart: always
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./api.dockerfile
|
||||
command: ["tactical-mkdocs-dev"]
|
||||
ports:
|
||||
- "8005:8005"
|
||||
volumes:
|
||||
- ..:/workspace:cached
|
||||
networks:
|
||||
- dev
|
||||
|
||||
volumes:
|
||||
tactical-data-dev:
|
||||
postgres-data-dev:
|
||||
mongo-dev-data:
|
||||
mesh-data-dev:
|
||||
redis-data-dev:
|
||||
|
||||
networks:
|
||||
dev:
|
||||
|
||||
@@ -170,3 +170,8 @@ if [ "$1" = 'tactical-websockets-dev' ]; then
|
||||
check_tactical_ready
|
||||
"${VIRTUAL_ENV}"/bin/daphne tacticalrmm.asgi:application --port 8383 -b 0.0.0.0
|
||||
fi
|
||||
|
||||
if [ "$1" = 'tactical-mkdocs-dev' ]; then
|
||||
cd "${WORKSPACE_DIR}/docs"
|
||||
"${VIRTUAL_ENV}"/bin/mkdocs serve
|
||||
fi
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
asyncio-nats-client
|
||||
celery
|
||||
channels
|
||||
channels_redis
|
||||
Django
|
||||
django-cors-headers
|
||||
django-rest-knox
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -47,3 +47,4 @@ docs/.vuepress/dist
|
||||
nats-rmm.conf
|
||||
.mypy_cache
|
||||
docs/site/
|
||||
reset_db.sh
|
||||
|
||||
@@ -11,8 +11,6 @@ It uses an [agent](https://github.com/wh1te909/rmmagent) written in golang and i
|
||||
# [LIVE DEMO](https://rmm.tacticalrmm.io/)
|
||||
Demo database resets every hour. Alot of features are disabled for obvious reasons due to the nature of this app.
|
||||
|
||||
*Tactical RMM is currently in alpha and subject to breaking changes. Use in production at your own risk.*
|
||||
|
||||
### [Discord Chat](https://discord.gg/upGTkWp)
|
||||
|
||||
### [Documentation](https://wh1te909.github.io/tacticalrmm/)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from django.contrib import admin
|
||||
from rest_framework.authtoken.admin import TokenAdmin
|
||||
|
||||
from .models import User
|
||||
from .models import User, Role
|
||||
|
||||
admin.site.register(User)
|
||||
TokenAdmin.raw_id_fields = ("user",)
|
||||
admin.site.register(Role)
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2 on 2021-04-11 01:43
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0013_user_client_tree_sort'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='client_tree_splitter',
|
||||
field=models.PositiveIntegerField(default=11),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2 on 2021-04-11 03:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0014_user_client_tree_splitter'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='loading_bar_color',
|
||||
field=models.CharField(default='red', max_length=255),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 3.2.1 on 2021-05-07 15:26
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0022_urlaction'),
|
||||
('accounts', '0015_user_loading_bar_color'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='url_action',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='user', to='core.urlaction'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='agent_dblclick_action',
|
||||
field=models.CharField(choices=[('editagent', 'Edit Agent'), ('takecontrol', 'Take Control'), ('remotebg', 'Remote Background'), ('urlaction', 'URL Action')], default='editagent', max_length=50),
|
||||
),
|
||||
]
|
||||
173
api/tacticalrmm/accounts/migrations/0017_auto_20210508_1716.py
Normal file
173
api/tacticalrmm/accounts/migrations/0017_auto_20210508_1716.py
Normal file
@@ -0,0 +1,173 @@
|
||||
# Generated by Django 3.2.1 on 2021-05-08 17:16
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0016_auto_20210507_1526'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_code_sign',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_do_server_maint',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_edit_agent',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_edit_core_settings',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_install_agents',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_manage_accounts',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_manage_alerts',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_manage_automation_policies',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_manage_autotasks',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_manage_checks',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_manage_clients',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_manage_deployments',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_manage_notes',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_manage_pendingactions',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_manage_procs',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_manage_scripts',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_manage_sites',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_manage_software',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_manage_winsvcs',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_manage_winupdates',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_reboot_agents',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_run_autotasks',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_run_bulk',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_run_checks',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_run_scripts',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_send_cmd',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_uninstall_agents',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_update_agents',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_use_mesh',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_view_auditlogs',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_view_debuglogs',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='can_view_eventlogs',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
181
api/tacticalrmm/accounts/migrations/0018_auto_20210511_0233.py
Normal file
181
api/tacticalrmm/accounts/migrations/0018_auto_20210511_0233.py
Normal file
@@ -0,0 +1,181 @@
|
||||
# Generated by Django 3.2.1 on 2021-05-11 02:33
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0017_auto_20210508_1716'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Role',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, unique=True)),
|
||||
('is_superuser', models.BooleanField(default=False)),
|
||||
('can_use_mesh', models.BooleanField(default=False)),
|
||||
('can_uninstall_agents', models.BooleanField(default=False)),
|
||||
('can_update_agents', models.BooleanField(default=False)),
|
||||
('can_edit_agent', models.BooleanField(default=False)),
|
||||
('can_manage_procs', models.BooleanField(default=False)),
|
||||
('can_view_eventlogs', models.BooleanField(default=False)),
|
||||
('can_send_cmd', models.BooleanField(default=False)),
|
||||
('can_reboot_agents', models.BooleanField(default=False)),
|
||||
('can_install_agents', models.BooleanField(default=False)),
|
||||
('can_run_scripts', models.BooleanField(default=False)),
|
||||
('can_run_bulk', models.BooleanField(default=False)),
|
||||
('can_manage_notes', models.BooleanField(default=False)),
|
||||
('can_edit_core_settings', models.BooleanField(default=False)),
|
||||
('can_do_server_maint', models.BooleanField(default=False)),
|
||||
('can_code_sign', models.BooleanField(default=False)),
|
||||
('can_manage_checks', models.BooleanField(default=False)),
|
||||
('can_run_checks', models.BooleanField(default=False)),
|
||||
('can_manage_clients', models.BooleanField(default=False)),
|
||||
('can_manage_sites', models.BooleanField(default=False)),
|
||||
('can_manage_deployments', models.BooleanField(default=False)),
|
||||
('can_manage_automation_policies', models.BooleanField(default=False)),
|
||||
('can_manage_autotasks', models.BooleanField(default=False)),
|
||||
('can_run_autotasks', models.BooleanField(default=False)),
|
||||
('can_view_auditlogs', models.BooleanField(default=False)),
|
||||
('can_manage_pendingactions', models.BooleanField(default=False)),
|
||||
('can_view_debuglogs', models.BooleanField(default=False)),
|
||||
('can_manage_scripts', models.BooleanField(default=False)),
|
||||
('can_manage_alerts', models.BooleanField(default=False)),
|
||||
('can_manage_winsvcs', models.BooleanField(default=False)),
|
||||
('can_manage_software', models.BooleanField(default=False)),
|
||||
('can_manage_winupdates', models.BooleanField(default=False)),
|
||||
('can_manage_accounts', models.BooleanField(default=False)),
|
||||
],
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_code_sign',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_do_server_maint',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_edit_agent',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_edit_core_settings',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_install_agents',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_manage_accounts',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_manage_alerts',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_manage_automation_policies',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_manage_autotasks',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_manage_checks',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_manage_clients',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_manage_deployments',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_manage_notes',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_manage_pendingactions',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_manage_procs',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_manage_scripts',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_manage_sites',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_manage_software',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_manage_winsvcs',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_manage_winupdates',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_reboot_agents',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_run_autotasks',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_run_bulk',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_run_checks',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_run_scripts',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_send_cmd',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_uninstall_agents',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_update_agents',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_use_mesh',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_view_auditlogs',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_view_debuglogs',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='can_view_eventlogs',
|
||||
),
|
||||
]
|
||||
25
api/tacticalrmm/accounts/migrations/0019_user_role.py
Normal file
25
api/tacticalrmm/accounts/migrations/0019_user_role.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 3.2.1 on 2021-05-11 02:33
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("accounts", "0018_auto_20210511_0233"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="role",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="roles",
|
||||
to="accounts.role",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.1 on 2021-05-11 17:37
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0019_user_role'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='role',
|
||||
name='can_manage_roles',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -7,6 +7,7 @@ AGENT_DBLCLICK_CHOICES = [
|
||||
("editagent", "Edit Agent"),
|
||||
("takecontrol", "Take Control"),
|
||||
("remotebg", "Remote Background"),
|
||||
("urlaction", "URL Action"),
|
||||
]
|
||||
|
||||
AGENT_TBL_TAB_CHOICES = [
|
||||
@@ -29,6 +30,13 @@ class User(AbstractUser, BaseAuditModel):
|
||||
agent_dblclick_action = models.CharField(
|
||||
max_length=50, choices=AGENT_DBLCLICK_CHOICES, default="editagent"
|
||||
)
|
||||
url_action = models.ForeignKey(
|
||||
"core.URLAction",
|
||||
related_name="user",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
default_agent_tbl_tab = models.CharField(
|
||||
max_length=50, choices=AGENT_TBL_TAB_CHOICES, default="server"
|
||||
)
|
||||
@@ -36,6 +44,8 @@ class User(AbstractUser, BaseAuditModel):
|
||||
client_tree_sort = models.CharField(
|
||||
max_length=50, choices=CLIENT_TREE_SORT_CHOICES, default="alphafail"
|
||||
)
|
||||
client_tree_splitter = models.PositiveIntegerField(default=11)
|
||||
loading_bar_color = models.CharField(max_length=255, default="red")
|
||||
|
||||
agent = models.OneToOneField(
|
||||
"agents.Agent",
|
||||
@@ -45,9 +55,123 @@ class User(AbstractUser, BaseAuditModel):
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
|
||||
role = models.ForeignKey(
|
||||
"accounts.Role",
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="roles",
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def serialize(user):
|
||||
# serializes the task and returns json
|
||||
from .serializers import UserSerializer
|
||||
|
||||
return UserSerializer(user).data
|
||||
|
||||
|
||||
class Role(models.Model):
|
||||
name = models.CharField(max_length=255, unique=True)
|
||||
is_superuser = models.BooleanField(default=False)
|
||||
|
||||
# agents
|
||||
can_use_mesh = models.BooleanField(default=False)
|
||||
can_uninstall_agents = models.BooleanField(default=False)
|
||||
can_update_agents = models.BooleanField(default=False)
|
||||
can_edit_agent = models.BooleanField(default=False)
|
||||
can_manage_procs = models.BooleanField(default=False)
|
||||
can_view_eventlogs = models.BooleanField(default=False)
|
||||
can_send_cmd = models.BooleanField(default=False)
|
||||
can_reboot_agents = models.BooleanField(default=False)
|
||||
can_install_agents = models.BooleanField(default=False)
|
||||
can_run_scripts = models.BooleanField(default=False)
|
||||
can_run_bulk = models.BooleanField(default=False)
|
||||
|
||||
# core
|
||||
can_manage_notes = models.BooleanField(default=False)
|
||||
can_edit_core_settings = models.BooleanField(default=False)
|
||||
can_do_server_maint = models.BooleanField(default=False)
|
||||
can_code_sign = models.BooleanField(default=False)
|
||||
|
||||
# checks
|
||||
can_manage_checks = models.BooleanField(default=False)
|
||||
can_run_checks = models.BooleanField(default=False)
|
||||
|
||||
# clients
|
||||
can_manage_clients = models.BooleanField(default=False)
|
||||
can_manage_sites = models.BooleanField(default=False)
|
||||
can_manage_deployments = models.BooleanField(default=False)
|
||||
|
||||
# automation
|
||||
can_manage_automation_policies = models.BooleanField(default=False)
|
||||
|
||||
# automated tasks
|
||||
can_manage_autotasks = models.BooleanField(default=False)
|
||||
can_run_autotasks = models.BooleanField(default=False)
|
||||
|
||||
# logs
|
||||
can_view_auditlogs = models.BooleanField(default=False)
|
||||
can_manage_pendingactions = models.BooleanField(default=False)
|
||||
can_view_debuglogs = models.BooleanField(default=False)
|
||||
|
||||
# scripts
|
||||
can_manage_scripts = models.BooleanField(default=False)
|
||||
|
||||
# alerts
|
||||
can_manage_alerts = models.BooleanField(default=False)
|
||||
|
||||
# win services
|
||||
can_manage_winsvcs = models.BooleanField(default=False)
|
||||
|
||||
# software
|
||||
can_manage_software = models.BooleanField(default=False)
|
||||
|
||||
# windows updates
|
||||
can_manage_winupdates = models.BooleanField(default=False)
|
||||
|
||||
# accounts
|
||||
can_manage_accounts = models.BooleanField(default=False)
|
||||
can_manage_roles = models.BooleanField(default=False)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@staticmethod
|
||||
def perms():
|
||||
return [
|
||||
"is_superuser",
|
||||
"can_use_mesh",
|
||||
"can_uninstall_agents",
|
||||
"can_update_agents",
|
||||
"can_edit_agent",
|
||||
"can_manage_procs",
|
||||
"can_view_eventlogs",
|
||||
"can_send_cmd",
|
||||
"can_reboot_agents",
|
||||
"can_install_agents",
|
||||
"can_run_scripts",
|
||||
"can_run_bulk",
|
||||
"can_manage_notes",
|
||||
"can_edit_core_settings",
|
||||
"can_do_server_maint",
|
||||
"can_code_sign",
|
||||
"can_manage_checks",
|
||||
"can_run_checks",
|
||||
"can_manage_clients",
|
||||
"can_manage_sites",
|
||||
"can_manage_deployments",
|
||||
"can_manage_automation_policies",
|
||||
"can_manage_autotasks",
|
||||
"can_run_autotasks",
|
||||
"can_view_auditlogs",
|
||||
"can_manage_pendingactions",
|
||||
"can_view_debuglogs",
|
||||
"can_manage_scripts",
|
||||
"can_manage_alerts",
|
||||
"can_manage_winsvcs",
|
||||
"can_manage_software",
|
||||
"can_manage_winupdates",
|
||||
"can_manage_accounts",
|
||||
"can_manage_roles",
|
||||
]
|
||||
|
||||
19
api/tacticalrmm/accounts/permissions.py
Normal file
19
api/tacticalrmm/accounts/permissions.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
from tacticalrmm.permissions import _has_perm
|
||||
|
||||
|
||||
class AccountsPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
if r.method == "GET":
|
||||
return True
|
||||
|
||||
return _has_perm(r, "can_manage_accounts")
|
||||
|
||||
|
||||
class RolesPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
if r.method == "GET":
|
||||
return True
|
||||
|
||||
return _has_perm(r, "can_manage_roles")
|
||||
@@ -1,7 +1,7 @@
|
||||
import pyotp
|
||||
from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
||||
|
||||
from .models import User
|
||||
from .models import User, Role
|
||||
|
||||
|
||||
class UserUISerializer(ModelSerializer):
|
||||
@@ -11,15 +11,18 @@ class UserUISerializer(ModelSerializer):
|
||||
"dark_mode",
|
||||
"show_community_scripts",
|
||||
"agent_dblclick_action",
|
||||
"url_action",
|
||||
"default_agent_tbl_tab",
|
||||
"client_tree_sort",
|
||||
"client_tree_splitter",
|
||||
"loading_bar_color",
|
||||
]
|
||||
|
||||
|
||||
class UserSerializer(ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = (
|
||||
fields = [
|
||||
"id",
|
||||
"username",
|
||||
"first_name",
|
||||
@@ -27,7 +30,8 @@ class UserSerializer(ModelSerializer):
|
||||
"email",
|
||||
"is_active",
|
||||
"last_login",
|
||||
)
|
||||
"role",
|
||||
]
|
||||
|
||||
|
||||
class TOTPSetupSerializer(ModelSerializer):
|
||||
@@ -46,3 +50,9 @@ class TOTPSetupSerializer(ModelSerializer):
|
||||
return pyotp.totp.TOTP(obj.totp_key).provisioning_uri(
|
||||
obj.username, issuer_name="Tactical RMM"
|
||||
)
|
||||
|
||||
|
||||
class RoleSerializer(ModelSerializer):
|
||||
class Meta:
|
||||
model = Role
|
||||
fields = "__all__"
|
||||
|
||||
@@ -278,6 +278,8 @@ class TestUserAction(TacticalTestCase):
|
||||
"agent_dblclick_action": "editagent",
|
||||
"default_agent_tbl_tab": "mixed",
|
||||
"client_tree_sort": "alpha",
|
||||
"client_tree_splitter": 14,
|
||||
"loading_bar_color": "green",
|
||||
}
|
||||
r = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
@@ -9,4 +9,7 @@ urlpatterns = [
|
||||
path("users/reset_totp/", views.UserActions.as_view()),
|
||||
path("users/setup_totp/", views.TOTPSetup.as_view()),
|
||||
path("users/ui/", views.UserUI.as_view()),
|
||||
path("permslist/", views.PermsList.as_view()),
|
||||
path("roles/", views.GetAddRoles.as_view()),
|
||||
path("<int:pk>/role/", views.GetUpdateDeleteRole.as_view()),
|
||||
]
|
||||
|
||||
@@ -6,15 +6,21 @@ from django.shortcuts import get_object_or_404
|
||||
from knox.views import LoginView as KnoxLoginView
|
||||
from rest_framework import status
|
||||
from rest_framework.authtoken.serializers import AuthTokenSerializer
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from logs.models import AuditLog
|
||||
from tacticalrmm.utils import notify_error
|
||||
|
||||
from .models import User
|
||||
from .serializers import TOTPSetupSerializer, UserSerializer, UserUISerializer
|
||||
from .models import User, Role
|
||||
from .permissions import AccountsPerms, RolesPerms
|
||||
from .serializers import (
|
||||
TOTPSetupSerializer,
|
||||
UserSerializer,
|
||||
UserUISerializer,
|
||||
RoleSerializer,
|
||||
)
|
||||
|
||||
|
||||
def _is_root_user(request, user) -> bool:
|
||||
@@ -78,6 +84,8 @@ class LoginView(KnoxLoginView):
|
||||
|
||||
|
||||
class GetAddUsers(APIView):
|
||||
permission_classes = [IsAuthenticated, AccountsPerms]
|
||||
|
||||
def get(self, request):
|
||||
users = User.objects.filter(agent=None)
|
||||
|
||||
@@ -98,13 +106,17 @@ class GetAddUsers(APIView):
|
||||
|
||||
user.first_name = request.data["first_name"]
|
||||
user.last_name = request.data["last_name"]
|
||||
# Can be changed once permissions and groups are introduced
|
||||
user.is_superuser = True
|
||||
if "role" in request.data.keys() and isinstance(request.data["role"], int):
|
||||
role = get_object_or_404(Role, pk=request.data["role"])
|
||||
user.role = role
|
||||
|
||||
user.save()
|
||||
return Response(user.username)
|
||||
|
||||
|
||||
class GetUpdateDeleteUser(APIView):
|
||||
permission_classes = [IsAuthenticated, AccountsPerms]
|
||||
|
||||
def get(self, request, pk):
|
||||
user = get_object_or_404(User, pk=pk)
|
||||
|
||||
@@ -133,7 +145,7 @@ class GetUpdateDeleteUser(APIView):
|
||||
|
||||
|
||||
class UserActions(APIView):
|
||||
|
||||
permission_classes = [IsAuthenticated, AccountsPerms]
|
||||
# reset password
|
||||
def post(self, request):
|
||||
user = get_object_or_404(User, pk=request.data["id"])
|
||||
@@ -182,3 +194,42 @@ class UserUI(APIView):
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return Response("ok")
|
||||
|
||||
|
||||
class PermsList(APIView):
|
||||
def get(self, request):
|
||||
return Response(Role.perms())
|
||||
|
||||
|
||||
class GetAddRoles(APIView):
|
||||
permission_classes = [IsAuthenticated, RolesPerms]
|
||||
|
||||
def get(self, request):
|
||||
roles = Role.objects.all()
|
||||
return Response(RoleSerializer(roles, many=True).data)
|
||||
|
||||
def post(self, request):
|
||||
serializer = RoleSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return Response("ok")
|
||||
|
||||
|
||||
class GetUpdateDeleteRole(APIView):
|
||||
permission_classes = [IsAuthenticated, RolesPerms]
|
||||
|
||||
def get(self, request, pk):
|
||||
role = get_object_or_404(Role, pk=pk)
|
||||
return Response(RoleSerializer(role).data)
|
||||
|
||||
def put(self, request, pk):
|
||||
role = get_object_or_404(Role, pk=pk)
|
||||
serializer = RoleSerializer(instance=role, data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return Response("ok")
|
||||
|
||||
def delete(self, request, pk):
|
||||
role = get_object_or_404(Role, pk=pk)
|
||||
role.delete()
|
||||
return Response("ok")
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.7 on 2021-04-17 01:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('agents', '0035_auto_20210329_1709'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='agent',
|
||||
name='block_policy_inheritance',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -63,6 +63,7 @@ class Agent(BaseAuditModel):
|
||||
max_length=255, choices=TZ_CHOICES, null=True, blank=True
|
||||
)
|
||||
maintenance_mode = models.BooleanField(default=False)
|
||||
block_policy_inheritance = models.BooleanField(default=False)
|
||||
alert_template = models.ForeignKey(
|
||||
"alerts.AlertTemplate",
|
||||
related_name="agents",
|
||||
@@ -96,9 +97,9 @@ class Agent(BaseAuditModel):
|
||||
# or if site has changed on agent and if so generate-policies
|
||||
if (
|
||||
not old_agent
|
||||
or old_agent
|
||||
and old_agent.policy != self.policy
|
||||
or old_agent.site != self.site
|
||||
or (old_agent and old_agent.policy != self.policy)
|
||||
or (old_agent.site != self.site)
|
||||
or (old_agent.block_policy_inheritance != self.block_policy_inheritance)
|
||||
):
|
||||
self.generate_checks_from_policies()
|
||||
self.generate_tasks_from_policies()
|
||||
@@ -166,7 +167,7 @@ class Agent(BaseAuditModel):
|
||||
|
||||
@property
|
||||
def checks(self):
|
||||
total, passing, failing = 0, 0, 0
|
||||
total, passing, failing, warning, info = 0, 0, 0, 0, 0
|
||||
|
||||
if self.agentchecks.exists(): # type: ignore
|
||||
for i in self.agentchecks.all(): # type: ignore
|
||||
@@ -174,13 +175,20 @@ class Agent(BaseAuditModel):
|
||||
if i.status == "passing":
|
||||
passing += 1
|
||||
elif i.status == "failing":
|
||||
failing += 1
|
||||
if i.alert_severity == "error":
|
||||
failing += 1
|
||||
elif i.alert_severity == "warning":
|
||||
warning += 1
|
||||
elif i.alert_severity == "info":
|
||||
info += 1
|
||||
|
||||
ret = {
|
||||
"total": total,
|
||||
"passing": passing,
|
||||
"failing": failing,
|
||||
"has_failing_checks": failing > 0,
|
||||
"warning": warning,
|
||||
"info": info,
|
||||
"has_failing_checks": failing > 0 or warning > 0,
|
||||
}
|
||||
return ret
|
||||
|
||||
@@ -195,6 +203,27 @@ class Agent(BaseAuditModel):
|
||||
except:
|
||||
return ["unknown cpu model"]
|
||||
|
||||
@property
|
||||
def graphics(self):
|
||||
ret, mrda = [], []
|
||||
try:
|
||||
graphics = self.wmi_detail["graphics"]
|
||||
for i in graphics:
|
||||
caption = [x["Caption"] for x in i if "Caption" in x][0]
|
||||
if "microsoft remote display adapter" in caption.lower():
|
||||
mrda.append("yes")
|
||||
continue
|
||||
|
||||
ret.append([x["Caption"] for x in i if "Caption" in x][0])
|
||||
|
||||
# only return this if no other graphics cards
|
||||
if not ret and mrda:
|
||||
return "Microsoft Remote Display Adapter"
|
||||
|
||||
return ", ".join(ret)
|
||||
except:
|
||||
return "Graphics info requires agent v1.4.14"
|
||||
|
||||
@property
|
||||
def local_ips(self):
|
||||
ret = []
|
||||
@@ -234,6 +263,11 @@ class Agent(BaseAuditModel):
|
||||
make = [x["Manufacturer"] for x in mobo if "Manufacturer" in x][0]
|
||||
model = [x["Product"] for x in mobo if "Product" in x][0]
|
||||
|
||||
if make.lower() == "lenovo":
|
||||
sysfam = [x["SystemFamily"] for x in comp_sys if "SystemFamily" in x][0]
|
||||
if "to be filled" not in sysfam.lower():
|
||||
model = sysfam
|
||||
|
||||
return f"{make} {model}"
|
||||
except:
|
||||
pass
|
||||
@@ -322,7 +356,7 @@ class Agent(BaseAuditModel):
|
||||
online = [
|
||||
agent
|
||||
for agent in Agent.objects.only(
|
||||
"pk", "last_seen", "overdue_time", "offline_time"
|
||||
"pk", "agent_id", "last_seen", "overdue_time", "offline_time"
|
||||
)
|
||||
if agent.status == "online"
|
||||
]
|
||||
@@ -393,21 +427,34 @@ class Agent(BaseAuditModel):
|
||||
|
||||
# check site policy if agent policy doesn't have one
|
||||
elif site.server_policy and site.server_policy.winupdatepolicy.exists():
|
||||
patch_policy = site.server_policy.winupdatepolicy.get()
|
||||
# make sure agent isn;t blocking policy inheritance
|
||||
if not self.block_policy_inheritance:
|
||||
patch_policy = site.server_policy.winupdatepolicy.get()
|
||||
|
||||
# if site doesn't have a patch policy check the client
|
||||
elif (
|
||||
site.client.server_policy
|
||||
and site.client.server_policy.winupdatepolicy.exists()
|
||||
):
|
||||
patch_policy = site.client.server_policy.winupdatepolicy.get()
|
||||
# make sure agent and site are not blocking inheritance
|
||||
if (
|
||||
not self.block_policy_inheritance
|
||||
and not site.block_policy_inheritance
|
||||
):
|
||||
patch_policy = site.client.server_policy.winupdatepolicy.get()
|
||||
|
||||
# if patch policy still doesn't exist check default policy
|
||||
elif (
|
||||
core_settings.server_policy
|
||||
and core_settings.server_policy.winupdatepolicy.exists()
|
||||
):
|
||||
patch_policy = core_settings.server_policy.winupdatepolicy.get()
|
||||
# make sure agent site and client are not blocking inheritance
|
||||
if (
|
||||
not self.block_policy_inheritance
|
||||
and not site.block_policy_inheritance
|
||||
and not site.client.block_policy_inheritance
|
||||
):
|
||||
patch_policy = core_settings.server_policy.winupdatepolicy.get()
|
||||
|
||||
elif self.monitoring_type == "workstation":
|
||||
# check agent policy first which should override client or site policy
|
||||
@@ -418,21 +465,36 @@ class Agent(BaseAuditModel):
|
||||
site.workstation_policy
|
||||
and site.workstation_policy.winupdatepolicy.exists()
|
||||
):
|
||||
patch_policy = site.workstation_policy.winupdatepolicy.get()
|
||||
# make sure agent isn;t blocking policy inheritance
|
||||
if not self.block_policy_inheritance:
|
||||
patch_policy = site.workstation_policy.winupdatepolicy.get()
|
||||
|
||||
# if site doesn't have a patch policy check the client
|
||||
elif (
|
||||
site.client.workstation_policy
|
||||
and site.client.workstation_policy.winupdatepolicy.exists()
|
||||
):
|
||||
patch_policy = site.client.workstation_policy.winupdatepolicy.get()
|
||||
# make sure agent and site are not blocking inheritance
|
||||
if (
|
||||
not self.block_policy_inheritance
|
||||
and not site.block_policy_inheritance
|
||||
):
|
||||
patch_policy = site.client.workstation_policy.winupdatepolicy.get()
|
||||
|
||||
# if patch policy still doesn't exist check default policy
|
||||
elif (
|
||||
core_settings.workstation_policy
|
||||
and core_settings.workstation_policy.winupdatepolicy.exists()
|
||||
):
|
||||
patch_policy = core_settings.workstation_policy.winupdatepolicy.get()
|
||||
# make sure agent site and client are not blocking inheritance
|
||||
if (
|
||||
not self.block_policy_inheritance
|
||||
and not site.block_policy_inheritance
|
||||
and not site.client.block_policy_inheritance
|
||||
):
|
||||
patch_policy = (
|
||||
core_settings.workstation_policy.winupdatepolicy.get()
|
||||
)
|
||||
|
||||
# if policy still doesn't exist return the agent patch policy
|
||||
if not patch_policy:
|
||||
@@ -499,6 +561,7 @@ class Agent(BaseAuditModel):
|
||||
and site.server_policy
|
||||
and site.server_policy.alert_template
|
||||
and site.server_policy.alert_template.is_active
|
||||
and not self.block_policy_inheritance
|
||||
):
|
||||
templates.append(site.server_policy.alert_template)
|
||||
if (
|
||||
@@ -506,6 +569,7 @@ class Agent(BaseAuditModel):
|
||||
and site.workstation_policy
|
||||
and site.workstation_policy.alert_template
|
||||
and site.workstation_policy.alert_template.is_active
|
||||
and not self.block_policy_inheritance
|
||||
):
|
||||
templates.append(site.workstation_policy.alert_template)
|
||||
|
||||
@@ -519,6 +583,8 @@ class Agent(BaseAuditModel):
|
||||
and client.server_policy
|
||||
and client.server_policy.alert_template
|
||||
and client.server_policy.alert_template.is_active
|
||||
and not self.block_policy_inheritance
|
||||
and not site.block_policy_inheritance
|
||||
):
|
||||
templates.append(client.server_policy.alert_template)
|
||||
if (
|
||||
@@ -526,15 +592,28 @@ class Agent(BaseAuditModel):
|
||||
and client.workstation_policy
|
||||
and client.workstation_policy.alert_template
|
||||
and client.workstation_policy.alert_template.is_active
|
||||
and not self.block_policy_inheritance
|
||||
and not site.block_policy_inheritance
|
||||
):
|
||||
templates.append(client.workstation_policy.alert_template)
|
||||
|
||||
# check if alert template is on client and return
|
||||
if client.alert_template and client.alert_template.is_active:
|
||||
if (
|
||||
client.alert_template
|
||||
and client.alert_template.is_active
|
||||
and not self.block_policy_inheritance
|
||||
and not site.block_policy_inheritance
|
||||
):
|
||||
templates.append(client.alert_template)
|
||||
|
||||
# check if alert template is applied globally and return
|
||||
if core.alert_template and core.alert_template.is_active:
|
||||
if (
|
||||
core.alert_template
|
||||
and core.alert_template.is_active
|
||||
and not self.block_policy_inheritance
|
||||
and not site.block_policy_inheritance
|
||||
and not client.block_policy_inheritance
|
||||
):
|
||||
templates.append(core.alert_template)
|
||||
|
||||
# if agent is a workstation, check if policy with alert template is assigned to the site, client, or core
|
||||
@@ -543,6 +622,9 @@ class Agent(BaseAuditModel):
|
||||
and core.server_policy
|
||||
and core.server_policy.alert_template
|
||||
and core.server_policy.alert_template.is_active
|
||||
and not self.block_policy_inheritance
|
||||
and not site.block_policy_inheritance
|
||||
and not client.block_policy_inheritance
|
||||
):
|
||||
templates.append(core.server_policy.alert_template)
|
||||
if (
|
||||
@@ -550,6 +632,9 @@ class Agent(BaseAuditModel):
|
||||
and core.workstation_policy
|
||||
and core.workstation_policy.alert_template
|
||||
and core.workstation_policy.alert_template.is_active
|
||||
and not self.block_policy_inheritance
|
||||
and not site.block_policy_inheritance
|
||||
and not client.block_policy_inheritance
|
||||
):
|
||||
templates.append(core.workstation_policy.alert_template)
|
||||
|
||||
@@ -703,36 +788,6 @@ class Agent(BaseAuditModel):
|
||||
except:
|
||||
pass
|
||||
|
||||
# define how the agent should handle pending actions
|
||||
def handle_pending_actions(self):
|
||||
pending_actions = self.pendingactions.filter(status="pending") # type: ignore
|
||||
|
||||
for action in pending_actions:
|
||||
if action.action_type == "taskaction":
|
||||
from autotasks.tasks import (
|
||||
create_win_task_schedule,
|
||||
delete_win_task_schedule,
|
||||
enable_or_disable_win_task,
|
||||
)
|
||||
|
||||
task_id = action.details["task_id"]
|
||||
|
||||
if action.details["action"] == "taskcreate":
|
||||
create_win_task_schedule.delay(task_id, pending_action=action.id)
|
||||
elif action.details["action"] == "tasktoggle":
|
||||
enable_or_disable_win_task.delay(
|
||||
task_id, action.details["value"], pending_action=action.id
|
||||
)
|
||||
elif action.details["action"] == "taskdelete":
|
||||
delete_win_task_schedule.delay(task_id, pending_action=action.id)
|
||||
|
||||
# for clearing duplicate pending actions on agent
|
||||
def remove_matching_pending_task_actions(self, task_id):
|
||||
# remove any other pending actions on agent with same task_id
|
||||
for action in self.pendingactions.filter(action_type="taskaction").exclude(status="completed"): # type: ignore
|
||||
if action.details["task_id"] == task_id:
|
||||
action.delete()
|
||||
|
||||
def should_create_alert(self, alert_template=None):
|
||||
return (
|
||||
self.overdue_dashboard_alert
|
||||
@@ -819,12 +874,6 @@ class RecoveryAction(models.Model):
|
||||
def __str__(self):
|
||||
return f"{self.agent.hostname} - {self.mode}"
|
||||
|
||||
def send(self):
|
||||
ret = {"recovery": self.mode}
|
||||
if self.mode == "command":
|
||||
ret["cmd"] = self.command
|
||||
return ret
|
||||
|
||||
|
||||
class Note(models.Model):
|
||||
agent = models.ForeignKey(
|
||||
|
||||
63
api/tacticalrmm/agents/permissions.py
Normal file
63
api/tacticalrmm/agents/permissions.py
Normal file
@@ -0,0 +1,63 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
from tacticalrmm.permissions import _has_perm
|
||||
|
||||
|
||||
class MeshPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_use_mesh")
|
||||
|
||||
|
||||
class UninstallPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_uninstall_agents")
|
||||
|
||||
|
||||
class UpdateAgentPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_update_agents")
|
||||
|
||||
|
||||
class EditAgentPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_edit_agent")
|
||||
|
||||
|
||||
class ManageProcPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_manage_procs")
|
||||
|
||||
|
||||
class EvtLogPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_view_eventlogs")
|
||||
|
||||
|
||||
class SendCMDPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_send_cmd")
|
||||
|
||||
|
||||
class RebootAgentPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_reboot_agents")
|
||||
|
||||
|
||||
class InstallAgentPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_install_agents")
|
||||
|
||||
|
||||
class RunScriptPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_run_scripts")
|
||||
|
||||
|
||||
class ManageNotesPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_manage_notes")
|
||||
|
||||
|
||||
class RunBulkPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_run_bulk")
|
||||
@@ -16,6 +16,7 @@ class AgentSerializer(serializers.ModelSerializer):
|
||||
local_ips = serializers.ReadOnlyField()
|
||||
make_model = serializers.ReadOnlyField()
|
||||
physical_disks = serializers.ReadOnlyField()
|
||||
graphics = serializers.ReadOnlyField()
|
||||
checks = serializers.ReadOnlyField()
|
||||
timezone = serializers.ReadOnlyField()
|
||||
all_timezones = serializers.SerializerMethodField()
|
||||
@@ -115,6 +116,7 @@ class AgentTableSerializer(serializers.ModelSerializer):
|
||||
"logged_username",
|
||||
"italic",
|
||||
"policy",
|
||||
"block_policy_inheritance",
|
||||
]
|
||||
depth = 2
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import asyncio
|
||||
import datetime as dt
|
||||
import json
|
||||
import random
|
||||
import subprocess
|
||||
import tempfile
|
||||
import urllib.parse
|
||||
from time import sleep
|
||||
from typing import Union
|
||||
|
||||
@@ -13,21 +11,21 @@ from loguru import logger
|
||||
from packaging import version as pyver
|
||||
|
||||
from agents.models import Agent
|
||||
from core.models import CoreSettings
|
||||
from core.models import CodeSignToken, CoreSettings
|
||||
from logs.models import PendingAction
|
||||
from scripts.models import Script
|
||||
from tacticalrmm.celery import app
|
||||
from tacticalrmm.utils import run_nats_api_cmd
|
||||
|
||||
logger.configure(**settings.LOG_CONFIG)
|
||||
|
||||
|
||||
def agent_update(pk: int) -> str:
|
||||
def agent_update(pk: int, codesigntoken: str = None, force: bool = False) -> str:
|
||||
from agents.utils import get_exegen_url
|
||||
|
||||
agent = Agent.objects.get(pk=pk)
|
||||
|
||||
if pyver.parse(agent.version) <= pyver.parse("1.1.11"):
|
||||
logger.warning(
|
||||
f"{agent.hostname} v{agent.version} is running an unsupported version. Refusing to auto update."
|
||||
)
|
||||
if pyver.parse(agent.version) <= pyver.parse("1.3.0"):
|
||||
return "not supported"
|
||||
|
||||
# skip if we can't determine the arch
|
||||
@@ -37,35 +35,33 @@ def agent_update(pk: int) -> str:
|
||||
)
|
||||
return "noarch"
|
||||
|
||||
# removed sqlite in 1.4.0 to get rid of cgo dependency
|
||||
# 1.3.0 has migration func to move from sqlite to win registry, so force an upgrade to 1.3.0 if old agent
|
||||
if pyver.parse(agent.version) >= pyver.parse("1.3.0"):
|
||||
version = settings.LATEST_AGENT_VER
|
||||
url = agent.winagent_dl
|
||||
inno = agent.win_inno_exe
|
||||
version = settings.LATEST_AGENT_VER
|
||||
inno = agent.win_inno_exe
|
||||
|
||||
if codesigntoken is not None and pyver.parse(version) >= pyver.parse("1.5.0"):
|
||||
base_url = get_exegen_url() + "/api/v1/winagents/?"
|
||||
params = {"version": version, "arch": agent.arch, "token": codesigntoken}
|
||||
url = base_url + urllib.parse.urlencode(params)
|
||||
else:
|
||||
version = "1.3.0"
|
||||
inno = (
|
||||
"winagent-v1.3.0.exe" if agent.arch == "64" else "winagent-v1.3.0-x86.exe"
|
||||
)
|
||||
url = f"https://github.com/wh1te909/rmmagent/releases/download/v1.3.0/{inno}"
|
||||
url = agent.winagent_dl
|
||||
|
||||
if agent.pendingactions.filter(
|
||||
action_type="agentupdate", status="pending"
|
||||
).exists():
|
||||
agent.pendingactions.filter(
|
||||
if not force:
|
||||
if agent.pendingactions.filter(
|
||||
action_type="agentupdate", status="pending"
|
||||
).delete()
|
||||
).exists():
|
||||
agent.pendingactions.filter(
|
||||
action_type="agentupdate", status="pending"
|
||||
).delete()
|
||||
|
||||
PendingAction.objects.create(
|
||||
agent=agent,
|
||||
action_type="agentupdate",
|
||||
details={
|
||||
"url": url,
|
||||
"version": version,
|
||||
"inno": inno,
|
||||
},
|
||||
)
|
||||
PendingAction.objects.create(
|
||||
agent=agent,
|
||||
action_type="agentupdate",
|
||||
details={
|
||||
"url": url,
|
||||
"version": version,
|
||||
"inno": inno,
|
||||
},
|
||||
)
|
||||
|
||||
nats_data = {
|
||||
"func": "agentupdate",
|
||||
@@ -79,12 +75,32 @@ def agent_update(pk: int) -> str:
|
||||
return "created"
|
||||
|
||||
|
||||
@app.task
|
||||
def force_code_sign(pks: list[int]) -> None:
|
||||
try:
|
||||
token = CodeSignToken.objects.first().token
|
||||
except:
|
||||
return
|
||||
|
||||
chunks = (pks[i : i + 50] for i in range(0, len(pks), 50))
|
||||
for chunk in chunks:
|
||||
for pk in chunk:
|
||||
agent_update(pk=pk, codesigntoken=token, force=True)
|
||||
sleep(0.05)
|
||||
sleep(4)
|
||||
|
||||
|
||||
@app.task
|
||||
def send_agent_update_task(pks: list[int]) -> None:
|
||||
try:
|
||||
codesigntoken = CodeSignToken.objects.first().token
|
||||
except:
|
||||
codesigntoken = None
|
||||
|
||||
chunks = (pks[i : i + 30] for i in range(0, len(pks), 30))
|
||||
for chunk in chunks:
|
||||
for pk in chunk:
|
||||
agent_update(pk)
|
||||
agent_update(pk, codesigntoken)
|
||||
sleep(0.05)
|
||||
sleep(4)
|
||||
|
||||
@@ -95,6 +111,11 @@ def auto_self_agent_update_task() -> None:
|
||||
if not core.agent_auto_update:
|
||||
return
|
||||
|
||||
try:
|
||||
codesigntoken = CodeSignToken.objects.first().token
|
||||
except:
|
||||
codesigntoken = None
|
||||
|
||||
q = Agent.objects.only("pk", "version")
|
||||
pks: list[int] = [
|
||||
i.pk
|
||||
@@ -105,7 +126,7 @@ def auto_self_agent_update_task() -> None:
|
||||
chunks = (pks[i : i + 30] for i in range(0, len(pks), 30))
|
||||
for chunk in chunks:
|
||||
for pk in chunk:
|
||||
agent_update(pk)
|
||||
agent_update(pk, codesigntoken)
|
||||
sleep(0.05)
|
||||
sleep(4)
|
||||
|
||||
@@ -190,6 +211,7 @@ def agent_outages_task() -> None:
|
||||
|
||||
agents = Agent.objects.only(
|
||||
"pk",
|
||||
"agent_id",
|
||||
"last_seen",
|
||||
"offline_time",
|
||||
"overdue_time",
|
||||
@@ -257,11 +279,32 @@ def run_script_email_results_task(
|
||||
logger.error(e)
|
||||
|
||||
|
||||
def _get_nats_config() -> dict:
|
||||
return {
|
||||
"key": settings.SECRET_KEY,
|
||||
"natsurl": f"tls://{settings.ALLOWED_HOSTS[0]}:4222",
|
||||
}
|
||||
@app.task
|
||||
def clear_faults_task(older_than_days: int) -> None:
|
||||
# https://github.com/wh1te909/tacticalrmm/issues/484
|
||||
agents = Agent.objects.exclude(last_seen__isnull=True).filter(
|
||||
last_seen__lt=djangotime.now() - djangotime.timedelta(days=older_than_days)
|
||||
)
|
||||
for agent in agents:
|
||||
if agent.agentchecks.exists():
|
||||
for check in agent.agentchecks.all():
|
||||
# reset check status
|
||||
check.status = "passing"
|
||||
check.save(update_fields=["status"])
|
||||
if check.alert.filter(resolved=False).exists():
|
||||
check.alert.get(resolved=False).resolve()
|
||||
|
||||
# reset overdue alerts
|
||||
agent.overdue_email_alert = False
|
||||
agent.overdue_text_alert = False
|
||||
agent.overdue_dashboard_alert = False
|
||||
agent.save(
|
||||
update_fields=[
|
||||
"overdue_email_alert",
|
||||
"overdue_text_alert",
|
||||
"overdue_dashboard_alert",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@app.task
|
||||
@@ -269,18 +312,8 @@ def monitor_agents_task() -> None:
|
||||
agents = Agent.objects.only(
|
||||
"pk", "agent_id", "last_seen", "overdue_time", "offline_time"
|
||||
)
|
||||
ret = [i.agent_id for i in agents if i.status != "online"]
|
||||
config = _get_nats_config()
|
||||
config["agents"] = ret
|
||||
with tempfile.NamedTemporaryFile() as fp:
|
||||
with open(fp.name, "w") as f:
|
||||
json.dump(config, f)
|
||||
|
||||
cmd = ["/usr/local/bin/nats-api", "-c", fp.name, "-m", "monitor"]
|
||||
try:
|
||||
subprocess.run(cmd, capture_output=True, timeout=30)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
ids = [i.agent_id for i in agents if i.status != "online"]
|
||||
run_nats_api_cmd("monitor", ids)
|
||||
|
||||
|
||||
@app.task
|
||||
@@ -288,15 +321,5 @@ def get_wmi_task() -> None:
|
||||
agents = Agent.objects.only(
|
||||
"pk", "agent_id", "last_seen", "overdue_time", "offline_time"
|
||||
)
|
||||
ret = [i.agent_id for i in agents if i.status == "online"]
|
||||
config = _get_nats_config()
|
||||
config["agents"] = ret
|
||||
with tempfile.NamedTemporaryFile() as fp:
|
||||
with open(fp.name, "w") as f:
|
||||
json.dump(config, f)
|
||||
|
||||
cmd = ["/usr/local/bin/nats-api", "-c", fp.name, "-m", "wmi"]
|
||||
try:
|
||||
subprocess.run(cmd, capture_output=True, timeout=30)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
ids = [i.agent_id for i in agents if i.status == "online"]
|
||||
run_nats_api_cmd("wmi", ids)
|
||||
|
||||
@@ -152,8 +152,9 @@ class TestAgentViews(TacticalTestCase):
|
||||
|
||||
self.check_not_authenticated("post", url)
|
||||
|
||||
@patch("time.sleep")
|
||||
@patch("agents.models.Agent.nats_cmd")
|
||||
def test_ping(self, nats_cmd):
|
||||
def test_ping(self, nats_cmd, mock_sleep):
|
||||
url = f"/agents/{self.agent.pk}/ping/"
|
||||
|
||||
nats_cmd.return_value = "timeout"
|
||||
@@ -753,7 +754,7 @@ class TestAgentViews(TacticalTestCase):
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertIn(self.agent.hostname, r.data) # type: ignore
|
||||
nats_cmd.assert_called_with(
|
||||
{"func": "recover", "payload": {"mode": "mesh"}}, timeout=45
|
||||
{"func": "recover", "payload": {"mode": "mesh"}}, timeout=90
|
||||
)
|
||||
|
||||
nats_cmd.return_value = "timeout"
|
||||
@@ -914,8 +915,9 @@ class TestAgentTasks(TacticalTestCase):
|
||||
self.authenticate()
|
||||
self.setup_coresettings()
|
||||
|
||||
@patch("agents.utils.get_exegen_url")
|
||||
@patch("agents.models.Agent.nats_cmd")
|
||||
def test_agent_update(self, nats_cmd):
|
||||
def test_agent_update(self, nats_cmd, get_exe):
|
||||
from agents.tasks import agent_update
|
||||
|
||||
agent_noarch = baker.make_recipe(
|
||||
@@ -926,63 +928,96 @@ class TestAgentTasks(TacticalTestCase):
|
||||
r = agent_update(agent_noarch.pk)
|
||||
self.assertEqual(r, "noarch")
|
||||
|
||||
agent_1111 = baker.make_recipe(
|
||||
"agents.agent",
|
||||
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
|
||||
version="1.1.11",
|
||||
)
|
||||
r = agent_update(agent_1111.pk)
|
||||
self.assertEqual(r, "not supported")
|
||||
|
||||
agent64_1112 = baker.make_recipe(
|
||||
"agents.agent",
|
||||
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
|
||||
version="1.1.12",
|
||||
)
|
||||
|
||||
r = agent_update(agent64_1112.pk)
|
||||
self.assertEqual(r, "created")
|
||||
action = PendingAction.objects.get(agent__pk=agent64_1112.pk)
|
||||
self.assertEqual(action.action_type, "agentupdate")
|
||||
self.assertEqual(action.status, "pending")
|
||||
self.assertEqual(
|
||||
action.details["url"],
|
||||
"https://github.com/wh1te909/rmmagent/releases/download/v1.3.0/winagent-v1.3.0.exe",
|
||||
)
|
||||
self.assertEqual(action.details["inno"], "winagent-v1.3.0.exe")
|
||||
self.assertEqual(action.details["version"], "1.3.0")
|
||||
nats_cmd.assert_called_with(
|
||||
{
|
||||
"func": "agentupdate",
|
||||
"payload": {
|
||||
"url": "https://github.com/wh1te909/rmmagent/releases/download/v1.3.0/winagent-v1.3.0.exe",
|
||||
"version": "1.3.0",
|
||||
"inno": "winagent-v1.3.0.exe",
|
||||
},
|
||||
},
|
||||
wait=False,
|
||||
)
|
||||
|
||||
agent_64_130 = baker.make_recipe(
|
||||
agent_130 = baker.make_recipe(
|
||||
"agents.agent",
|
||||
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
|
||||
version="1.3.0",
|
||||
)
|
||||
nats_cmd.return_value = "ok"
|
||||
r = agent_update(agent_64_130.pk)
|
||||
r = agent_update(agent_130.pk)
|
||||
self.assertEqual(r, "not supported")
|
||||
|
||||
# test __without__ code signing
|
||||
agent64_nosign = baker.make_recipe(
|
||||
"agents.agent",
|
||||
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
|
||||
version="1.4.14",
|
||||
)
|
||||
|
||||
r = agent_update(agent64_nosign.pk, None)
|
||||
self.assertEqual(r, "created")
|
||||
action = PendingAction.objects.get(agent__pk=agent64_nosign.pk)
|
||||
self.assertEqual(action.action_type, "agentupdate")
|
||||
self.assertEqual(action.status, "pending")
|
||||
self.assertEqual(
|
||||
action.details["url"],
|
||||
f"https://github.com/wh1te909/rmmagent/releases/download/v{settings.LATEST_AGENT_VER}/winagent-v{settings.LATEST_AGENT_VER}.exe",
|
||||
)
|
||||
self.assertEqual(
|
||||
action.details["inno"], f"winagent-v{settings.LATEST_AGENT_VER}.exe"
|
||||
)
|
||||
self.assertEqual(action.details["version"], settings.LATEST_AGENT_VER)
|
||||
nats_cmd.assert_called_with(
|
||||
{
|
||||
"func": "agentupdate",
|
||||
"payload": {
|
||||
"url": settings.DL_64,
|
||||
"url": f"https://github.com/wh1te909/rmmagent/releases/download/v{settings.LATEST_AGENT_VER}/winagent-v{settings.LATEST_AGENT_VER}.exe",
|
||||
"version": settings.LATEST_AGENT_VER,
|
||||
"inno": f"winagent-v{settings.LATEST_AGENT_VER}.exe",
|
||||
},
|
||||
},
|
||||
wait=False,
|
||||
)
|
||||
action = PendingAction.objects.get(agent__pk=agent_64_130.pk)
|
||||
|
||||
# test __with__ code signing (64 bit)
|
||||
codesign = baker.make("core.CodeSignToken", token="testtoken123")
|
||||
agent64_sign = baker.make_recipe(
|
||||
"agents.agent",
|
||||
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
|
||||
version="1.4.14",
|
||||
)
|
||||
|
||||
nats_cmd.return_value = "ok"
|
||||
get_exe.return_value = "https://exe.tacticalrmm.io"
|
||||
r = agent_update(agent64_sign.pk, codesign.token) # type: ignore
|
||||
self.assertEqual(r, "created")
|
||||
nats_cmd.assert_called_with(
|
||||
{
|
||||
"func": "agentupdate",
|
||||
"payload": {
|
||||
"url": f"https://exe.tacticalrmm.io/api/v1/winagents/?version={settings.LATEST_AGENT_VER}&arch=64&token=testtoken123", # type: ignore
|
||||
"version": settings.LATEST_AGENT_VER,
|
||||
"inno": f"winagent-v{settings.LATEST_AGENT_VER}.exe",
|
||||
},
|
||||
},
|
||||
wait=False,
|
||||
)
|
||||
action = PendingAction.objects.get(agent__pk=agent64_sign.pk)
|
||||
self.assertEqual(action.action_type, "agentupdate")
|
||||
self.assertEqual(action.status, "pending")
|
||||
|
||||
# test __with__ code signing (32 bit)
|
||||
agent32_sign = baker.make_recipe(
|
||||
"agents.agent",
|
||||
operating_system="Windows 10 Pro, 32 bit (build 19041.450)",
|
||||
version="1.4.14",
|
||||
)
|
||||
|
||||
nats_cmd.return_value = "ok"
|
||||
get_exe.return_value = "https://exe.tacticalrmm.io"
|
||||
r = agent_update(agent32_sign.pk, codesign.token) # type: ignore
|
||||
self.assertEqual(r, "created")
|
||||
nats_cmd.assert_called_with(
|
||||
{
|
||||
"func": "agentupdate",
|
||||
"payload": {
|
||||
"url": f"https://exe.tacticalrmm.io/api/v1/winagents/?version={settings.LATEST_AGENT_VER}&arch=32&token=testtoken123", # type: ignore
|
||||
"version": settings.LATEST_AGENT_VER,
|
||||
"inno": f"winagent-v{settings.LATEST_AGENT_VER}-x86.exe",
|
||||
},
|
||||
},
|
||||
wait=False,
|
||||
)
|
||||
action = PendingAction.objects.get(agent__pk=agent32_sign.pk)
|
||||
self.assertEqual(action.action_type, "agentupdate")
|
||||
self.assertEqual(action.status, "pending")
|
||||
|
||||
|
||||
37
api/tacticalrmm/agents/utils.py
Normal file
37
api/tacticalrmm/agents/utils.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import random
|
||||
import urllib.parse
|
||||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def get_exegen_url() -> str:
|
||||
urls: list[str] = settings.EXE_GEN_URLS
|
||||
for url in urls:
|
||||
try:
|
||||
r = requests.get(url, timeout=10)
|
||||
except:
|
||||
continue
|
||||
|
||||
if r.status_code == 200:
|
||||
return url
|
||||
|
||||
return random.choice(urls)
|
||||
|
||||
|
||||
def get_winagent_url(arch: str) -> str:
|
||||
from core.models import CodeSignToken
|
||||
|
||||
try:
|
||||
codetoken = CodeSignToken.objects.first().token
|
||||
base_url = get_exegen_url() + "/api/v1/winagents/?"
|
||||
params = {
|
||||
"version": settings.LATEST_AGENT_VER,
|
||||
"arch": arch,
|
||||
"token": codetoken,
|
||||
}
|
||||
dl_url = base_url + urllib.parse.urlencode(params)
|
||||
except:
|
||||
dl_url = settings.DL_64 if arch == "64" else settings.DL_32
|
||||
|
||||
return dl_url
|
||||
@@ -3,6 +3,7 @@ import datetime as dt
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
import time
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponse
|
||||
@@ -10,7 +11,8 @@ from django.shortcuts import get_object_or_404
|
||||
from loguru import logger
|
||||
from packaging import version as pyver
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
@@ -23,6 +25,20 @@ from winupdate.serializers import WinUpdatePolicySerializer
|
||||
from winupdate.tasks import bulk_check_for_updates_task, bulk_install_updates_task
|
||||
|
||||
from .models import Agent, AgentCustomField, Note, RecoveryAction
|
||||
from .permissions import (
|
||||
EditAgentPerms,
|
||||
EvtLogPerms,
|
||||
InstallAgentPerms,
|
||||
ManageNotesPerms,
|
||||
ManageProcPerms,
|
||||
MeshPerms,
|
||||
RebootAgentPerms,
|
||||
RunBulkPerms,
|
||||
RunScriptPerms,
|
||||
SendCMDPerms,
|
||||
UninstallPerms,
|
||||
UpdateAgentPerms,
|
||||
)
|
||||
from .serializers import (
|
||||
AgentCustomFieldSerializer,
|
||||
AgentEditSerializer,
|
||||
@@ -40,7 +56,7 @@ logger.configure(**settings.LOG_CONFIG)
|
||||
|
||||
@api_view()
|
||||
def get_agent_versions(request):
|
||||
agents = Agent.objects.only("pk")
|
||||
agents = Agent.objects.prefetch_related("site").only("pk", "hostname")
|
||||
return Response(
|
||||
{
|
||||
"versions": [settings.LATEST_AGENT_VER],
|
||||
@@ -50,6 +66,7 @@ def get_agent_versions(request):
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
@permission_classes([IsAuthenticated, UpdateAgentPerms])
|
||||
def update_agents(request):
|
||||
q = Agent.objects.filter(pk__in=request.data["pks"]).only("pk", "version")
|
||||
pks: list[int] = [
|
||||
@@ -62,21 +79,31 @@ def update_agents(request):
|
||||
|
||||
|
||||
@api_view()
|
||||
@permission_classes([IsAuthenticated, UninstallPerms])
|
||||
def ping(request, pk):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
status = "offline"
|
||||
r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=5))
|
||||
if r == "pong":
|
||||
status = "online"
|
||||
attempts = 0
|
||||
while 1:
|
||||
r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=2))
|
||||
if r == "pong":
|
||||
status = "online"
|
||||
break
|
||||
else:
|
||||
attempts += 1
|
||||
time.sleep(1)
|
||||
|
||||
if attempts >= 5:
|
||||
break
|
||||
|
||||
return Response({"name": agent.hostname, "status": status})
|
||||
|
||||
|
||||
@api_view(["DELETE"])
|
||||
@permission_classes([IsAuthenticated, UninstallPerms])
|
||||
def uninstall(request):
|
||||
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
||||
asyncio.run(agent.nats_cmd({"func": "uninstall"}, wait=False))
|
||||
|
||||
name = agent.hostname
|
||||
agent.delete()
|
||||
reload_nats()
|
||||
@@ -84,6 +111,7 @@ def uninstall(request):
|
||||
|
||||
|
||||
@api_view(["PATCH", "PUT"])
|
||||
@permission_classes([IsAuthenticated, EditAgentPerms])
|
||||
def edit_agent(request):
|
||||
agent = get_object_or_404(Agent, pk=request.data["id"])
|
||||
|
||||
@@ -126,6 +154,7 @@ def edit_agent(request):
|
||||
|
||||
|
||||
@api_view()
|
||||
@permission_classes([IsAuthenticated, MeshPerms])
|
||||
def meshcentral(request, pk):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
core = CoreSettings.objects.first()
|
||||
@@ -171,6 +200,7 @@ def get_processes(request, pk):
|
||||
|
||||
|
||||
@api_view()
|
||||
@permission_classes([IsAuthenticated, ManageProcPerms])
|
||||
def kill_proc(request, pk, pid):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
r = asyncio.run(
|
||||
@@ -186,6 +216,7 @@ def kill_proc(request, pk, pid):
|
||||
|
||||
|
||||
@api_view()
|
||||
@permission_classes([IsAuthenticated, EvtLogPerms])
|
||||
def get_event_log(request, pk, logtype, days):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
timeout = 180 if logtype == "Security" else 30
|
||||
@@ -205,6 +236,7 @@ def get_event_log(request, pk, logtype, days):
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
@permission_classes([IsAuthenticated, SendCMDPerms])
|
||||
def send_raw_cmd(request):
|
||||
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
||||
timeout = int(request.data["timeout"])
|
||||
@@ -300,6 +332,7 @@ def overdue_action(request):
|
||||
|
||||
|
||||
class Reboot(APIView):
|
||||
permission_classes = [IsAuthenticated, RebootAgentPerms]
|
||||
# reboot now
|
||||
def post(self, request):
|
||||
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
||||
@@ -352,9 +385,12 @@ class Reboot(APIView):
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
@permission_classes([IsAuthenticated, InstallAgentPerms])
|
||||
def install_agent(request):
|
||||
from knox.models import AuthToken
|
||||
|
||||
from agents.utils import get_winagent_url
|
||||
|
||||
client_id = request.data["client"]
|
||||
site_id = request.data["site"]
|
||||
version = settings.LATEST_AGENT_VER
|
||||
@@ -375,7 +411,7 @@ def install_agent(request):
|
||||
inno = (
|
||||
f"winagent-v{version}.exe" if arch == "64" else f"winagent-v{version}-x86.exe"
|
||||
)
|
||||
download_url = settings.DL_64 if arch == "64" else settings.DL_32
|
||||
download_url = get_winagent_url(arch)
|
||||
|
||||
_, token = AuthToken.objects.create(
|
||||
user=request.user, expiry=dt.timedelta(hours=request.data["expires"])
|
||||
@@ -522,6 +558,7 @@ def recover(request):
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
@permission_classes([IsAuthenticated, RunScriptPerms])
|
||||
def run_script(request):
|
||||
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
||||
script = get_object_or_404(Script, pk=request.data["scriptPK"])
|
||||
@@ -562,7 +599,7 @@ def run_script(request):
|
||||
def recover_mesh(request, pk):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
data = {"func": "recover", "payload": {"mode": "mesh"}}
|
||||
r = asyncio.run(agent.nats_cmd(data, timeout=45))
|
||||
r = asyncio.run(agent.nats_cmd(data, timeout=90))
|
||||
if r != "ok":
|
||||
return notify_error("Unable to contact the agent")
|
||||
|
||||
@@ -604,6 +641,8 @@ class GetAddNotes(APIView):
|
||||
|
||||
|
||||
class GetEditDeleteNote(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageNotesPerms]
|
||||
|
||||
def get(self, request, pk):
|
||||
note = get_object_or_404(Note, pk=pk)
|
||||
return Response(NoteSerializer(note).data)
|
||||
@@ -622,6 +661,7 @@ class GetEditDeleteNote(APIView):
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
@permission_classes([IsAuthenticated, RunBulkPerms])
|
||||
def bulk(request):
|
||||
if request.data["target"] == "agents" and not request.data["agentPKs"]:
|
||||
return notify_error("Must select at least 1 agent")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import TYPE_CHECKING, Union
|
||||
|
||||
from django.conf import settings
|
||||
@@ -297,7 +298,7 @@ class Alert(models.Model):
|
||||
if alert_template and alert_template.action and not alert.action_run:
|
||||
r = agent.run_script(
|
||||
scriptpk=alert_template.action.pk,
|
||||
args=alert_template.action_args,
|
||||
args=alert.parse_script_args(alert_template.action_args),
|
||||
timeout=alert_template.action_timeout,
|
||||
wait=True,
|
||||
full=True,
|
||||
@@ -406,7 +407,7 @@ class Alert(models.Model):
|
||||
):
|
||||
r = agent.run_script(
|
||||
scriptpk=alert_template.resolved_action.pk,
|
||||
args=alert_template.resolved_action_args,
|
||||
args=alert.parse_script_args(alert_template.resolved_action_args),
|
||||
timeout=alert_template.resolved_action_timeout,
|
||||
wait=True,
|
||||
full=True,
|
||||
@@ -428,6 +429,36 @@ class Alert(models.Model):
|
||||
f"Resolved action: {alert_template.action.name} failed to run on any agent for {agent.hostname} resolved alert"
|
||||
)
|
||||
|
||||
def parse_script_args(self, args: list[str]):
|
||||
|
||||
if not args:
|
||||
return []
|
||||
|
||||
temp_args = list()
|
||||
# pattern to match for injection
|
||||
pattern = re.compile(".*\\{\\{alert\\.(.*)\\}\\}.*")
|
||||
|
||||
for arg in args:
|
||||
match = pattern.match(arg)
|
||||
if match:
|
||||
name = match.group(1)
|
||||
|
||||
if hasattr(self, name):
|
||||
value = f"'{getattr(self, name)}'"
|
||||
else:
|
||||
continue
|
||||
|
||||
try:
|
||||
temp_args.append(re.sub("\\{\\{.*\\}\\}", value, arg)) # type: ignore
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
continue
|
||||
|
||||
else:
|
||||
temp_args.append(arg)
|
||||
|
||||
return temp_args
|
||||
|
||||
|
||||
class AlertTemplate(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
11
api/tacticalrmm/alerts/permissions.py
Normal file
11
api/tacticalrmm/alerts/permissions.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
from tacticalrmm.permissions import _has_perm
|
||||
|
||||
|
||||
class ManageAlertsPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
if r.method == "GET" or r.method == "PATCH":
|
||||
return True
|
||||
|
||||
return _has_perm(r, "can_manage_alerts")
|
||||
@@ -1387,3 +1387,14 @@ class TestAlertTasks(TacticalTestCase):
|
||||
self.assertEqual(alert.resolved_action_execution_time, "5.0000")
|
||||
self.assertEqual(alert.resolved_action_stdout, "success!")
|
||||
self.assertEqual(alert.resolved_action_stderr, "")
|
||||
|
||||
def test_parse_script_args(self):
|
||||
alert = baker.make("alerts.Alert")
|
||||
|
||||
args = ["-Parameter", "-Another {{alert.id}}"]
|
||||
|
||||
# test default value
|
||||
self.assertEqual(
|
||||
["-Parameter", f"-Another '{alert.id}'"], # type: ignore
|
||||
alert.parse_script_args(args=args), # type: ignore
|
||||
)
|
||||
|
||||
@@ -3,12 +3,14 @@ from datetime import datetime as dt
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils import timezone as djangotime
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from tacticalrmm.utils import notify_error
|
||||
|
||||
from .models import Alert, AlertTemplate
|
||||
from .permissions import ManageAlertsPerms
|
||||
from .serializers import (
|
||||
AlertSerializer,
|
||||
AlertTemplateRelationSerializer,
|
||||
@@ -18,6 +20,8 @@ from .tasks import cache_agents_alert_template
|
||||
|
||||
|
||||
class GetAddAlerts(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageAlertsPerms]
|
||||
|
||||
def patch(self, request):
|
||||
|
||||
# top 10 alerts for dashboard icon
|
||||
@@ -109,6 +113,8 @@ class GetAddAlerts(APIView):
|
||||
|
||||
|
||||
class GetUpdateDeleteAlert(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageAlertsPerms]
|
||||
|
||||
def get(self, request, pk):
|
||||
alert = get_object_or_404(Alert, pk=pk)
|
||||
|
||||
@@ -163,6 +169,8 @@ class GetUpdateDeleteAlert(APIView):
|
||||
|
||||
|
||||
class BulkAlerts(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageAlertsPerms]
|
||||
|
||||
def post(self, request):
|
||||
if request.data["bulk_action"] == "resolve":
|
||||
Alert.objects.filter(id__in=request.data["alerts"]).update(
|
||||
@@ -185,6 +193,8 @@ class BulkAlerts(APIView):
|
||||
|
||||
|
||||
class GetAddAlertTemplates(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageAlertsPerms]
|
||||
|
||||
def get(self, request):
|
||||
alert_templates = AlertTemplate.objects.all()
|
||||
|
||||
@@ -202,6 +212,8 @@ class GetAddAlertTemplates(APIView):
|
||||
|
||||
|
||||
class GetUpdateDeleteAlertTemplate(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageAlertsPerms]
|
||||
|
||||
def get(self, request, pk):
|
||||
alert_template = get_object_or_404(AlertTemplate, pk=pk)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from django.conf import settings
|
||||
from django.utils import timezone as djangotime
|
||||
from model_bakery import baker
|
||||
|
||||
from autotasks.models import AutomatedTask
|
||||
from tacticalrmm.test import TacticalTestCase
|
||||
|
||||
|
||||
@@ -203,3 +204,139 @@ class TestAPIv3(TacticalTestCase):
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(r.json(), {"mode": "rpc", "shellcmd": ""})
|
||||
reload_nats.assert_called_once()
|
||||
|
||||
def test_task_runner_get(self):
|
||||
from autotasks.serializers import TaskGOGetSerializer
|
||||
|
||||
r = self.client.get("/api/v3/500/asdf9df9dfdf/taskrunner/")
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
# setup data
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
script = baker.make_recipe("scripts.script")
|
||||
task = baker.make("autotasks.AutomatedTask", agent=agent, script=script)
|
||||
|
||||
url = f"/api/v3/{task.pk}/{agent.agent_id}/taskrunner/" # type: ignore
|
||||
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(TaskGOGetSerializer(task).data, r.data) # type: ignore
|
||||
|
||||
def test_task_runner_results(self):
|
||||
from agents.models import AgentCustomField
|
||||
|
||||
r = self.client.patch("/api/v3/500/asdf9df9dfdf/taskrunner/")
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
# setup data
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
task = baker.make("autotasks.AutomatedTask", agent=agent)
|
||||
|
||||
url = f"/api/v3/{task.pk}/{agent.agent_id}/taskrunner/" # type: ignore
|
||||
|
||||
# test passing task
|
||||
data = {
|
||||
"stdout": "test test \ntestest stdgsd\n",
|
||||
"stderr": "",
|
||||
"retcode": 0,
|
||||
"execution_time": 3.560,
|
||||
}
|
||||
|
||||
r = self.client.patch(url, data)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertTrue(AutomatedTask.objects.get(pk=task.pk).status == "passing") # type: ignore
|
||||
|
||||
# test failing task
|
||||
data = {
|
||||
"stdout": "test test \ntestest stdgsd\n",
|
||||
"stderr": "",
|
||||
"retcode": 1,
|
||||
"execution_time": 3.560,
|
||||
}
|
||||
|
||||
r = self.client.patch(url, data)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertTrue(AutomatedTask.objects.get(pk=task.pk).status == "failing") # type: ignore
|
||||
|
||||
# test collector task
|
||||
text = baker.make("core.CustomField", model="agent", type="text", name="Test")
|
||||
boolean = baker.make(
|
||||
"core.CustomField", model="agent", type="checkbox", name="Test1"
|
||||
)
|
||||
multiple = baker.make(
|
||||
"core.CustomField", model="agent", type="multiple", name="Test2"
|
||||
)
|
||||
|
||||
# test text fields
|
||||
task.custom_field = text # type: ignore
|
||||
task.save() # type: ignore
|
||||
|
||||
# test failing failing with stderr
|
||||
data = {
|
||||
"stdout": "test test \nthe last line",
|
||||
"stderr": "This is an error",
|
||||
"retcode": 1,
|
||||
"execution_time": 3.560,
|
||||
}
|
||||
|
||||
r = self.client.patch(url, data)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertTrue(AutomatedTask.objects.get(pk=task.pk).status == "failing") # type: ignore
|
||||
|
||||
# test saving to text field
|
||||
data = {
|
||||
"stdout": "test test \nthe last line",
|
||||
"stderr": "",
|
||||
"retcode": 0,
|
||||
"execution_time": 3.560,
|
||||
}
|
||||
|
||||
r = self.client.patch(url, data)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(AutomatedTask.objects.get(pk=task.pk).status, "passing") # type: ignore
|
||||
self.assertEqual(AgentCustomField.objects.get(field=text, agent=task.agent).value, "the last line") # type: ignore
|
||||
|
||||
# test saving to checkbox field
|
||||
task.custom_field = boolean # type: ignore
|
||||
task.save() # type: ignore
|
||||
|
||||
data = {
|
||||
"stdout": "1",
|
||||
"stderr": "",
|
||||
"retcode": 0,
|
||||
"execution_time": 3.560,
|
||||
}
|
||||
|
||||
r = self.client.patch(url, data)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(AutomatedTask.objects.get(pk=task.pk).status, "passing") # type: ignore
|
||||
self.assertTrue(AgentCustomField.objects.get(field=boolean, agent=task.agent).value) # type: ignore
|
||||
|
||||
# test saving to multiple field with commas
|
||||
task.custom_field = multiple # type: ignore
|
||||
task.save() # type: ignore
|
||||
|
||||
data = {
|
||||
"stdout": "this,is,an,array",
|
||||
"stderr": "",
|
||||
"retcode": 0,
|
||||
"execution_time": 3.560,
|
||||
}
|
||||
|
||||
r = self.client.patch(url, data)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(AutomatedTask.objects.get(pk=task.pk).status, "passing") # type: ignore
|
||||
self.assertEqual(AgentCustomField.objects.get(field=multiple, agent=task.agent).value, ["this", "is", "an", "array"]) # type: ignore
|
||||
|
||||
# test mutiple with a single value
|
||||
data = {
|
||||
"stdout": "this",
|
||||
"stderr": "",
|
||||
"retcode": 0,
|
||||
"execution_time": 3.560,
|
||||
}
|
||||
|
||||
r = self.client.patch(url, data)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(AutomatedTask.objects.get(pk=task.pk).status, "passing") # type: ignore
|
||||
self.assertEqual(AgentCustomField.objects.get(field=multiple, agent=task.agent).value, ["this"]) # type: ignore
|
||||
|
||||
@@ -15,7 +15,7 @@ from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from accounts.models import User
|
||||
from agents.models import Agent
|
||||
from agents.models import Agent, AgentCustomField
|
||||
from agents.serializers import WinAgentSerializer
|
||||
from autotasks.models import AutomatedTask
|
||||
from autotasks.serializers import TaskGOGetSerializer, TaskRunnerPatchSerializer
|
||||
@@ -65,9 +65,17 @@ class CheckIn(APIView):
|
||||
if Alert.objects.filter(agent=agent, resolved=False).exists():
|
||||
Alert.handle_alert_resolve(agent)
|
||||
|
||||
# get any pending actions
|
||||
if agent.pendingactions.filter(status="pending").exists(): # type: ignore
|
||||
agent.handle_pending_actions()
|
||||
# sync scheduled tasks
|
||||
if agent.autotasks.exclude(sync_status="synced").exists(): # type: ignore
|
||||
tasks = agent.autotasks.exclude(sync_status="synced") # type: ignore
|
||||
|
||||
for task in tasks:
|
||||
if task.sync_status == "pendingdeletion":
|
||||
task.delete_task_on_agent()
|
||||
elif task.sync_status == "initial":
|
||||
task.modify_task_on_agent()
|
||||
elif task.sync_status == "notsynced":
|
||||
task.create_task_on_agent()
|
||||
|
||||
return Response("ok")
|
||||
|
||||
@@ -296,10 +304,11 @@ class CheckRunner(APIView):
|
||||
< djangotime.now()
|
||||
- djangotime.timedelta(seconds=check.run_interval)
|
||||
)
|
||||
# if check interval isn't set, make sure the agent's check interval has passed before running
|
||||
)
|
||||
# if check interval isn't set, make sure the agent's check interval has passed before running
|
||||
or (
|
||||
check.last_run
|
||||
not check.run_interval
|
||||
and check.last_run
|
||||
< djangotime.now() - djangotime.timedelta(seconds=agent.check_interval)
|
||||
)
|
||||
]
|
||||
@@ -312,11 +321,16 @@ class CheckRunner(APIView):
|
||||
|
||||
def patch(self, request):
|
||||
check = get_object_or_404(Check, pk=request.data["id"])
|
||||
if pyver.parse(check.agent.version) < pyver.parse("1.5.7"):
|
||||
return notify_error("unsupported")
|
||||
|
||||
check.last_run = djangotime.now()
|
||||
check.save(update_fields=["last_run"])
|
||||
status = check.handle_checkv2(request.data)
|
||||
status = check.handle_check(request.data)
|
||||
if status == "failing" and check.assignedtask.exists(): # type: ignore
|
||||
check.handle_assigned_task()
|
||||
|
||||
return Response(status)
|
||||
return Response("ok")
|
||||
|
||||
|
||||
class CheckRunnerInterval(APIView):
|
||||
@@ -351,11 +365,51 @@ class TaskRunner(APIView):
|
||||
instance=task, data=request.data, partial=True
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save(last_run=djangotime.now())
|
||||
new_task = serializer.save(last_run=djangotime.now())
|
||||
|
||||
status = "failing" if task.retcode != 0 else "passing"
|
||||
# check if task is a collector and update the custom field
|
||||
if task.custom_field:
|
||||
if not task.stderr:
|
||||
|
||||
if AgentCustomField.objects.filter(
|
||||
field=task.custom_field, agent=task.agent
|
||||
).exists():
|
||||
agent_field = AgentCustomField.objects.get(
|
||||
field=task.custom_field, agent=task.agent
|
||||
)
|
||||
else:
|
||||
agent_field = AgentCustomField.objects.create(
|
||||
field=task.custom_field, agent=task.agent
|
||||
)
|
||||
|
||||
# get last line of stdout
|
||||
value = (
|
||||
new_task.stdout
|
||||
if task.collector_all_output
|
||||
else new_task.stdout.split("\n")[-1].strip()
|
||||
)
|
||||
|
||||
if task.custom_field.type in [
|
||||
"text",
|
||||
"number",
|
||||
"single",
|
||||
"datetime",
|
||||
]:
|
||||
agent_field.string_value = value
|
||||
agent_field.save()
|
||||
elif task.custom_field.type == "multiple":
|
||||
agent_field.multiple_value = value.split(",")
|
||||
agent_field.save()
|
||||
elif task.custom_field.type == "checkbox":
|
||||
agent_field.bool_value = bool(value)
|
||||
agent_field.save()
|
||||
|
||||
status = "passing"
|
||||
else:
|
||||
status = "failing"
|
||||
else:
|
||||
status = "failing" if task.retcode != 0 else "passing"
|
||||
|
||||
new_task: AutomatedTask = AutomatedTask.objects.get(pk=task.pk)
|
||||
new_task.status = status
|
||||
new_task.save()
|
||||
|
||||
@@ -393,7 +447,7 @@ class SysInfo(APIView):
|
||||
|
||||
|
||||
class MeshExe(APIView):
|
||||
""" Sends the mesh exe to the installer """
|
||||
"""Sends the mesh exe to the installer"""
|
||||
|
||||
def post(self, request):
|
||||
exe = "meshagent.exe" if request.data["arch"] == "64" else "meshagent-x86.exe"
|
||||
|
||||
@@ -29,7 +29,7 @@ class Policy(BaseAuditModel):
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
from alerts.tasks import cache_agents_alert_template
|
||||
from automation.tasks import generate_agent_checks_from_policies_task
|
||||
from automation.tasks import generate_agent_checks_task
|
||||
|
||||
# get old policy if exists
|
||||
old_policy = type(self).objects.get(pk=self.pk) if self.pk else None
|
||||
@@ -38,8 +38,8 @@ class Policy(BaseAuditModel):
|
||||
# generate agent checks only if active and enforced were changed
|
||||
if old_policy:
|
||||
if old_policy.active != self.active or old_policy.enforced != self.enforced:
|
||||
generate_agent_checks_from_policies_task.delay(
|
||||
policypk=self.pk,
|
||||
generate_agent_checks_task.delay(
|
||||
policy=self.pk,
|
||||
create_tasks=True,
|
||||
)
|
||||
|
||||
@@ -52,7 +52,10 @@ class Policy(BaseAuditModel):
|
||||
agents = list(self.related_agents().only("pk").values_list("pk", flat=True))
|
||||
super(BaseAuditModel, self).delete(*args, **kwargs)
|
||||
|
||||
generate_agent_checks_task.delay(agents, create_tasks=True)
|
||||
generate_agent_checks_task.delay(agents=agents, create_tasks=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def is_default_server_policy(self):
|
||||
@@ -62,9 +65,6 @@ class Policy(BaseAuditModel):
|
||||
def is_default_workstation_policy(self):
|
||||
return self.default_workstation_policy.exists() # type: ignore
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def is_agent_excluded(self, agent):
|
||||
return (
|
||||
agent in self.excluded_agents.all()
|
||||
@@ -94,20 +94,29 @@ class Policy(BaseAuditModel):
|
||||
|
||||
filtered_agents_pks = Policy.objects.none()
|
||||
|
||||
filtered_agents_pks |= Agent.objects.filter(
|
||||
site__in=[
|
||||
site
|
||||
for site in explicit_sites
|
||||
if site.client not in explicit_clients
|
||||
and site.client not in self.excluded_clients.all()
|
||||
],
|
||||
monitoring_type=mon_type,
|
||||
).values_list("pk", flat=True)
|
||||
filtered_agents_pks |= (
|
||||
Agent.objects.exclude(block_policy_inheritance=True)
|
||||
.filter(
|
||||
site__in=[
|
||||
site
|
||||
for site in explicit_sites
|
||||
if site.client not in explicit_clients
|
||||
and site.client not in self.excluded_clients.all()
|
||||
],
|
||||
monitoring_type=mon_type,
|
||||
)
|
||||
.values_list("pk", flat=True)
|
||||
)
|
||||
|
||||
filtered_agents_pks |= Agent.objects.filter(
|
||||
site__client__in=[client for client in explicit_clients],
|
||||
monitoring_type=mon_type,
|
||||
).values_list("pk", flat=True)
|
||||
filtered_agents_pks |= (
|
||||
Agent.objects.exclude(block_policy_inheritance=True)
|
||||
.exclude(site__block_policy_inheritance=True)
|
||||
.filter(
|
||||
site__client__in=[client for client in explicit_clients],
|
||||
monitoring_type=mon_type,
|
||||
)
|
||||
.values_list("pk", flat=True)
|
||||
)
|
||||
|
||||
return Agent.objects.filter(
|
||||
models.Q(pk__in=filtered_agents_pks)
|
||||
@@ -123,9 +132,6 @@ class Policy(BaseAuditModel):
|
||||
|
||||
@staticmethod
|
||||
def cascade_policy_tasks(agent):
|
||||
from autotasks.models import AutomatedTask
|
||||
from autotasks.tasks import delete_win_task_schedule
|
||||
from logs.models import PendingAction
|
||||
|
||||
# List of all tasks to be applied
|
||||
tasks = list()
|
||||
@@ -154,6 +160,17 @@ class Policy(BaseAuditModel):
|
||||
client_policy = client.workstation_policy
|
||||
site_policy = site.workstation_policy
|
||||
|
||||
# check if client/site/agent is blocking inheritance and blank out policies
|
||||
if agent.block_policy_inheritance:
|
||||
site_policy = None
|
||||
client_policy = None
|
||||
default_policy = None
|
||||
elif site.block_policy_inheritance:
|
||||
client_policy = None
|
||||
default_policy = None
|
||||
elif client.block_policy_inheritance:
|
||||
default_policy = None
|
||||
|
||||
if (
|
||||
agent_policy
|
||||
and agent_policy.active
|
||||
@@ -200,26 +217,16 @@ class Policy(BaseAuditModel):
|
||||
if taskpk not in added_task_pks
|
||||
]
|
||||
):
|
||||
delete_win_task_schedule.delay(task.pk)
|
||||
if task.sync_status == "initial":
|
||||
task.delete()
|
||||
else:
|
||||
task.sync_status = "pendingdeletion"
|
||||
task.save()
|
||||
|
||||
# handle matching tasks that haven't synced to agent yet or pending deletion due to agent being offline
|
||||
for action in agent.pendingactions.filter(action_type="taskaction").exclude(
|
||||
status="completed"
|
||||
):
|
||||
task = AutomatedTask.objects.get(pk=action.details["task_id"])
|
||||
if (
|
||||
task.parent_task in agent_tasks_parent_pks
|
||||
and task.parent_task in added_task_pks
|
||||
):
|
||||
agent.remove_matching_pending_task_actions(task.id)
|
||||
|
||||
PendingAction(
|
||||
agent=agent,
|
||||
action_type="taskaction",
|
||||
details={"action": "taskcreate", "task_id": task.id},
|
||||
).save()
|
||||
task.sync_status = "notsynced"
|
||||
task.save(update_fields=["sync_status"])
|
||||
# change tasks from pendingdeletion to notsynced if policy was added or changed
|
||||
agent.autotasks.filter(sync_status="pendingdeletion").filter(
|
||||
parent_task__in=[taskpk for taskpk in added_task_pks]
|
||||
).update(sync_status="notsynced")
|
||||
|
||||
return [task for task in tasks if task.pk not in agent_tasks_parent_pks]
|
||||
|
||||
@@ -251,6 +258,17 @@ class Policy(BaseAuditModel):
|
||||
client_policy = client.workstation_policy
|
||||
site_policy = site.workstation_policy
|
||||
|
||||
# check if client/site/agent is blocking inheritance and blank out policies
|
||||
if agent.block_policy_inheritance:
|
||||
site_policy = None
|
||||
client_policy = None
|
||||
default_policy = None
|
||||
elif site.block_policy_inheritance:
|
||||
client_policy = None
|
||||
default_policy = None
|
||||
elif client.block_policy_inheritance:
|
||||
default_policy = None
|
||||
|
||||
# Used to hold the policies that will be applied and the order in which they are applied
|
||||
# Enforced policies are applied first
|
||||
enforced_checks = list()
|
||||
@@ -412,11 +430,12 @@ class Policy(BaseAuditModel):
|
||||
|
||||
# remove policy checks from agent that fell out of policy scope
|
||||
agent.agentchecks.filter(
|
||||
managed_by_policy=True,
|
||||
parent_check__in=[
|
||||
checkpk
|
||||
for checkpk in agent_checks_parent_pks
|
||||
if checkpk not in [check.pk for check in final_list]
|
||||
]
|
||||
],
|
||||
).delete()
|
||||
|
||||
return [
|
||||
|
||||
11
api/tacticalrmm/automation/permissions.py
Normal file
11
api/tacticalrmm/automation/permissions.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
from tacticalrmm.permissions import _has_perm
|
||||
|
||||
|
||||
class AutomationPolicyPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
if r.method == "GET":
|
||||
return True
|
||||
|
||||
return _has_perm(r, "can_manage_automation_policies")
|
||||
@@ -1,169 +1,153 @@
|
||||
from agents.models import Agent
|
||||
from automation.models import Policy
|
||||
from autotasks.models import AutomatedTask
|
||||
from checks.models import Check
|
||||
from typing import Any, Dict, List, Union
|
||||
|
||||
from tacticalrmm.celery import app
|
||||
|
||||
|
||||
@app.task
|
||||
# generates policy checks on agents affected by a policy and optionally generate automated tasks
|
||||
def generate_agent_checks_from_policies_task(policypk, create_tasks=False):
|
||||
@app.task(retry_backoff=5, retry_jitter=True, retry_kwargs={"max_retries": 5})
|
||||
def generate_agent_checks_task(
|
||||
policy: int = None,
|
||||
site: int = None,
|
||||
client: int = None,
|
||||
agents: List[int] = list(),
|
||||
all: bool = False,
|
||||
create_tasks: bool = False,
|
||||
) -> Union[str, None]:
|
||||
from agents.models import Agent
|
||||
from automation.models import Policy
|
||||
|
||||
policy = Policy.objects.get(pk=policypk)
|
||||
p = Policy.objects.get(pk=policy) if policy else None
|
||||
|
||||
if policy.is_default_server_policy and policy.is_default_workstation_policy:
|
||||
agents = Agent.objects.prefetch_related("policy").only("pk", "monitoring_type")
|
||||
elif policy.is_default_server_policy:
|
||||
agents = Agent.objects.filter(monitoring_type="server").only(
|
||||
"pk", "monitoring_type"
|
||||
)
|
||||
elif policy.is_default_workstation_policy:
|
||||
agents = Agent.objects.filter(monitoring_type="workstation").only(
|
||||
# generate checks on all agents if all is specified or if policy is default server/workstation policy
|
||||
if (p and p.is_default_server_policy and p.is_default_workstation_policy) or all:
|
||||
a = Agent.objects.prefetch_related("policy").only("pk", "monitoring_type")
|
||||
|
||||
# generate checks on all servers if policy is a default servers policy
|
||||
elif p and p.is_default_server_policy:
|
||||
a = Agent.objects.filter(monitoring_type="server").only("pk", "monitoring_type")
|
||||
|
||||
# generate checks on all workstations if policy is a default workstations policy
|
||||
elif p and p.is_default_workstation_policy:
|
||||
a = Agent.objects.filter(monitoring_type="workstation").only(
|
||||
"pk", "monitoring_type"
|
||||
)
|
||||
|
||||
# generate checks on a list of supplied agents
|
||||
elif agents:
|
||||
a = Agent.objects.filter(pk__in=agents)
|
||||
|
||||
# generate checks on agents affected by supplied policy
|
||||
elif policy:
|
||||
a = p.related_agents().only("pk")
|
||||
|
||||
# generate checks that has specified site
|
||||
elif site:
|
||||
a = Agent.objects.filter(site_id=site)
|
||||
|
||||
# generate checks that has specified client
|
||||
elif client:
|
||||
a = Agent.objects.filter(site__client_id=client)
|
||||
else:
|
||||
agents = policy.related_agents().only("pk")
|
||||
a = []
|
||||
|
||||
for agent in agents:
|
||||
for agent in a:
|
||||
agent.generate_checks_from_policies()
|
||||
if create_tasks:
|
||||
agent.generate_tasks_from_policies()
|
||||
|
||||
|
||||
@app.task
|
||||
# generates policy checks on a list of agents and optionally generate automated tasks
|
||||
def generate_agent_checks_task(agentpks, create_tasks=False):
|
||||
for agent in Agent.objects.filter(pk__in=agentpks):
|
||||
agent.generate_checks_from_policies()
|
||||
|
||||
if create_tasks:
|
||||
agent.generate_tasks_from_policies()
|
||||
return "ok"
|
||||
|
||||
|
||||
@app.task
|
||||
# generates policy checks on agent servers or workstations within a certain client or site and optionally generate automated tasks
|
||||
def generate_agent_checks_by_location_task(location, mon_type, create_tasks=False):
|
||||
|
||||
for agent in Agent.objects.filter(**location).filter(monitoring_type=mon_type):
|
||||
agent.generate_checks_from_policies()
|
||||
|
||||
if create_tasks:
|
||||
agent.generate_tasks_from_policies()
|
||||
|
||||
|
||||
@app.task
|
||||
# generates policy checks on all agent servers or workstations and optionally generate automated tasks
|
||||
def generate_all_agent_checks_task(mon_type, create_tasks=False):
|
||||
for agent in Agent.objects.filter(monitoring_type=mon_type):
|
||||
agent.generate_checks_from_policies()
|
||||
|
||||
if create_tasks:
|
||||
agent.generate_tasks_from_policies()
|
||||
|
||||
|
||||
@app.task
|
||||
# deletes a policy managed check from all agents
|
||||
def delete_policy_check_task(checkpk):
|
||||
|
||||
Check.objects.filter(parent_check=checkpk).delete()
|
||||
|
||||
|
||||
@app.task
|
||||
@app.task(
|
||||
acks_late=True, retry_backoff=5, retry_jitter=True, retry_kwargs={"max_retries": 5}
|
||||
)
|
||||
# updates policy managed check fields on agents
|
||||
def update_policy_check_fields_task(checkpk):
|
||||
def update_policy_check_fields_task(check: int) -> str:
|
||||
from checks.models import Check
|
||||
|
||||
check = Check.objects.get(pk=checkpk)
|
||||
c: Check = Check.objects.get(pk=check)
|
||||
update_fields: Dict[Any, Any] = {}
|
||||
|
||||
Check.objects.filter(parent_check=checkpk).update(
|
||||
warning_threshold=check.warning_threshold,
|
||||
error_threshold=check.error_threshold,
|
||||
alert_severity=check.alert_severity,
|
||||
name=check.name,
|
||||
run_interval=check.run_interval,
|
||||
disk=check.disk,
|
||||
fails_b4_alert=check.fails_b4_alert,
|
||||
ip=check.ip,
|
||||
script=check.script,
|
||||
script_args=check.script_args,
|
||||
info_return_codes=check.info_return_codes,
|
||||
warning_return_codes=check.warning_return_codes,
|
||||
timeout=check.timeout,
|
||||
pass_if_start_pending=check.pass_if_start_pending,
|
||||
pass_if_svc_not_exist=check.pass_if_svc_not_exist,
|
||||
restart_if_stopped=check.restart_if_stopped,
|
||||
log_name=check.log_name,
|
||||
event_id=check.event_id,
|
||||
event_id_is_wildcard=check.event_id_is_wildcard,
|
||||
event_type=check.event_type,
|
||||
event_source=check.event_source,
|
||||
event_message=check.event_message,
|
||||
fail_when=check.fail_when,
|
||||
search_last_days=check.search_last_days,
|
||||
number_of_events_b4_alert=check.number_of_events_b4_alert,
|
||||
email_alert=check.email_alert,
|
||||
text_alert=check.text_alert,
|
||||
dashboard_alert=check.dashboard_alert,
|
||||
)
|
||||
for field in c.policy_fields_to_copy:
|
||||
update_fields[field] = getattr(c, field)
|
||||
|
||||
Check.objects.filter(parent_check=check).update(**update_fields)
|
||||
|
||||
return "ok"
|
||||
|
||||
|
||||
@app.task
|
||||
@app.task(retry_backoff=5, retry_jitter=True, retry_kwargs={"max_retries": 5})
|
||||
# generates policy tasks on agents affected by a policy
|
||||
def generate_agent_tasks_from_policies_task(policypk):
|
||||
def generate_agent_autotasks_task(policy: int = None) -> str:
|
||||
from agents.models import Agent
|
||||
from automation.models import Policy
|
||||
|
||||
policy = Policy.objects.get(pk=policypk)
|
||||
p: Policy = Policy.objects.get(pk=policy)
|
||||
|
||||
if policy.is_default_server_policy and policy.is_default_workstation_policy:
|
||||
if p and p.is_default_server_policy and p.is_default_workstation_policy:
|
||||
agents = Agent.objects.prefetch_related("policy").only("pk", "monitoring_type")
|
||||
elif policy.is_default_server_policy:
|
||||
elif p and p.is_default_server_policy:
|
||||
agents = Agent.objects.filter(monitoring_type="server").only(
|
||||
"pk", "monitoring_type"
|
||||
)
|
||||
elif policy.is_default_workstation_policy:
|
||||
elif p and p.is_default_workstation_policy:
|
||||
agents = Agent.objects.filter(monitoring_type="workstation").only(
|
||||
"pk", "monitoring_type"
|
||||
)
|
||||
else:
|
||||
agents = policy.related_agents().only("pk")
|
||||
agents = p.related_agents().only("pk")
|
||||
|
||||
for agent in agents:
|
||||
agent.generate_tasks_from_policies()
|
||||
|
||||
return "ok"
|
||||
|
||||
@app.task
|
||||
def delete_policy_autotask_task(taskpk):
|
||||
|
||||
@app.task(
|
||||
acks_late=True,
|
||||
retry_backoff=5,
|
||||
retry_jitter=True,
|
||||
retry_kwargs={"max_retries": 5},
|
||||
)
|
||||
def delete_policy_autotasks_task(task: int) -> str:
|
||||
from autotasks.models import AutomatedTask
|
||||
from autotasks.tasks import delete_win_task_schedule
|
||||
|
||||
for task in AutomatedTask.objects.filter(parent_task=taskpk):
|
||||
delete_win_task_schedule.delay(task.pk)
|
||||
for t in AutomatedTask.objects.filter(parent_task=task):
|
||||
t.delete_task_on_agent()
|
||||
|
||||
return "ok"
|
||||
|
||||
|
||||
@app.task
|
||||
def run_win_policy_autotask_task(task_pks):
|
||||
from autotasks.tasks import run_win_task
|
||||
def run_win_policy_autotasks_task(task: int) -> str:
|
||||
from autotasks.models import AutomatedTask
|
||||
|
||||
for task in task_pks:
|
||||
run_win_task.delay(task)
|
||||
for t in AutomatedTask.objects.filter(parent_task=task):
|
||||
t.run_win_task()
|
||||
|
||||
return "ok"
|
||||
|
||||
|
||||
@app.task
|
||||
def update_policy_task_fields_task(taskpk, update_agent=False):
|
||||
from autotasks.tasks import enable_or_disable_win_task
|
||||
@app.task(
|
||||
acks_late=True,
|
||||
retry_backoff=5,
|
||||
retry_jitter=True,
|
||||
retry_kwargs={"max_retries": 5},
|
||||
)
|
||||
def update_policy_autotasks_fields_task(task: int, update_agent: bool = False) -> str:
|
||||
from autotasks.models import AutomatedTask
|
||||
|
||||
task = AutomatedTask.objects.get(pk=taskpk)
|
||||
t = AutomatedTask.objects.get(pk=task)
|
||||
update_fields: Dict[str, Any] = {}
|
||||
|
||||
AutomatedTask.objects.filter(parent_task=taskpk).update(
|
||||
alert_severity=task.alert_severity,
|
||||
email_alert=task.email_alert,
|
||||
text_alert=task.text_alert,
|
||||
dashboard_alert=task.dashboard_alert,
|
||||
script=task.script,
|
||||
script_args=task.script_args,
|
||||
name=task.name,
|
||||
timeout=task.timeout,
|
||||
enabled=task.enabled,
|
||||
)
|
||||
for field in t.policy_fields_to_copy:
|
||||
update_fields[field] = getattr(t, field)
|
||||
|
||||
AutomatedTask.objects.filter(parent_task=task).update(**update_fields)
|
||||
|
||||
if update_agent:
|
||||
for task in AutomatedTask.objects.filter(parent_task=taskpk):
|
||||
enable_or_disable_win_task.delay(task.pk, task.enabled)
|
||||
for t in AutomatedTask.objects.filter(parent_task=task).exclude(
|
||||
sync_status="initial"
|
||||
):
|
||||
t.modify_task_on_agent()
|
||||
|
||||
return "ok"
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
from itertools import cycle
|
||||
from unittest.mock import patch
|
||||
|
||||
from model_bakery import baker, seq
|
||||
|
||||
from agents.models import Agent
|
||||
from core.models import CoreSettings
|
||||
from model_bakery import baker, seq
|
||||
from tacticalrmm.test import TacticalTestCase
|
||||
from winupdate.models import WinUpdatePolicy
|
||||
|
||||
@@ -52,7 +51,10 @@ class TestPolicyViews(TacticalTestCase):
|
||||
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
def test_add_policy(self):
|
||||
@patch("autotasks.models.AutomatedTask.create_task_on_agent")
|
||||
def test_add_policy(self, create_task):
|
||||
from automation.models import Policy
|
||||
|
||||
url = "/automation/policies/"
|
||||
|
||||
data = {
|
||||
@@ -71,8 +73,12 @@ class TestPolicyViews(TacticalTestCase):
|
||||
|
||||
# create policy with tasks and checks
|
||||
policy = baker.make("automation.Policy")
|
||||
self.create_checks(policy=policy)
|
||||
baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
|
||||
checks = self.create_checks(policy=policy)
|
||||
tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
|
||||
|
||||
# assign a task to a check
|
||||
tasks[0].assigned_check = checks[0] # type: ignore
|
||||
tasks[0].save() # type: ignore
|
||||
|
||||
# test copy tasks and checks to another policy
|
||||
data = {
|
||||
@@ -85,13 +91,21 @@ class TestPolicyViews(TacticalTestCase):
|
||||
|
||||
resp = self.client.post(f"/automation/policies/", data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(policy.autotasks.count(), 3) # type: ignore
|
||||
self.assertEqual(policy.policychecks.count(), 7) # type: ignore
|
||||
|
||||
copied_policy = Policy.objects.get(name=data["name"])
|
||||
|
||||
self.assertEqual(copied_policy.autotasks.count(), 3) # type: ignore
|
||||
self.assertEqual(copied_policy.policychecks.count(), 7) # type: ignore
|
||||
|
||||
# make sure correct task was assign to the check
|
||||
self.assertEqual(copied_policy.autotasks.get(name=tasks[0].name).assigned_check.check_type, checks[0].check_type) # type: ignore
|
||||
|
||||
create_task.assert_not_called()
|
||||
|
||||
self.check_not_authenticated("post", url)
|
||||
|
||||
@patch("automation.tasks.generate_agent_checks_from_policies_task.delay")
|
||||
def test_update_policy(self, generate_agent_checks_from_policies_task):
|
||||
@patch("automation.tasks.generate_agent_checks_task.delay")
|
||||
def test_update_policy(self, generate_agent_checks_task):
|
||||
# returns 404 for invalid policy pk
|
||||
resp = self.client.put("/automation/policies/500/", format="json")
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
@@ -109,8 +123,8 @@ class TestPolicyViews(TacticalTestCase):
|
||||
resp = self.client.put(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# only called if active or enforced are updated
|
||||
generate_agent_checks_from_policies_task.assert_not_called()
|
||||
# only called if active, enforced, or excluded objects are updated
|
||||
generate_agent_checks_task.assert_not_called()
|
||||
|
||||
data = {
|
||||
"name": "Test Policy Update",
|
||||
@@ -121,8 +135,25 @@ class TestPolicyViews(TacticalTestCase):
|
||||
|
||||
resp = self.client.put(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
generate_agent_checks_from_policies_task.assert_called_with(
|
||||
policypk=policy.pk, create_tasks=True # type: ignore
|
||||
generate_agent_checks_task.assert_called_with(
|
||||
policy=policy.pk, create_tasks=True # type: ignore
|
||||
)
|
||||
generate_agent_checks_task.reset_mock()
|
||||
|
||||
# make sure policies are re-evaluated when excluded changes
|
||||
agents = baker.make_recipe("agents.agent", _quantity=2)
|
||||
clients = baker.make("clients.Client", _quantity=2)
|
||||
sites = baker.make("clients.Site", _quantity=2)
|
||||
data = {
|
||||
"excluded_agents": [agent.pk for agent in agents], # type: ignore
|
||||
"excluded_sites": [site.pk for site in sites], # type: ignore
|
||||
"excluded_clients": [client.pk for client in clients], # type: ignore
|
||||
}
|
||||
|
||||
resp = self.client.put(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
generate_agent_checks_task.assert_called_with(
|
||||
policy=policy.pk, create_tasks=True # type: ignore
|
||||
)
|
||||
|
||||
self.check_not_authenticated("put", url)
|
||||
@@ -145,7 +176,7 @@ class TestPolicyViews(TacticalTestCase):
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
generate_agent_checks_task.assert_called_with(
|
||||
[agent.pk for agent in agents], create_tasks=True
|
||||
agents=[agent.pk for agent in agents], create_tasks=True
|
||||
)
|
||||
|
||||
self.check_not_authenticated("delete", url)
|
||||
@@ -271,7 +302,7 @@ class TestPolicyViews(TacticalTestCase):
|
||||
|
||||
self.check_not_authenticated("patch", url)
|
||||
|
||||
@patch("automation.tasks.run_win_policy_autotask_task.delay")
|
||||
@patch("automation.tasks.run_win_policy_autotasks_task.delay")
|
||||
def test_run_win_task(self, mock_task):
|
||||
|
||||
# create managed policy tasks
|
||||
@@ -281,11 +312,12 @@ class TestPolicyViews(TacticalTestCase):
|
||||
parent_task=1,
|
||||
_quantity=6,
|
||||
)
|
||||
|
||||
url = "/automation/runwintask/1/"
|
||||
resp = self.client.put(url, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
mock_task.assert_called_once_with([task.pk for task in tasks]) # type: ignore
|
||||
mock_task.assert_called() # type: ignore
|
||||
|
||||
self.check_not_authenticated("put", url)
|
||||
|
||||
@@ -426,7 +458,7 @@ class TestPolicyViews(TacticalTestCase):
|
||||
|
||||
self.check_not_authenticated("delete", url)
|
||||
|
||||
@patch("automation.tasks.generate_agent_checks_from_policies_task.delay")
|
||||
@patch("automation.tasks.generate_agent_checks_task.delay")
|
||||
def test_sync_policy(self, generate_checks):
|
||||
url = "/automation/sync/"
|
||||
|
||||
@@ -441,7 +473,7 @@ class TestPolicyViews(TacticalTestCase):
|
||||
|
||||
resp = self.client.post(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
generate_checks.assert_called_with(policy.pk, create_tasks=True) # type: ignore
|
||||
generate_checks.assert_called_with(policy=policy.pk, create_tasks=True) # type: ignore
|
||||
|
||||
self.check_not_authenticated("post", url)
|
||||
|
||||
@@ -497,7 +529,7 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
self.assertEquals(len(resp.data["agents"]), 10) # type: ignore
|
||||
|
||||
def test_generating_agent_policy_checks(self):
|
||||
from .tasks import generate_agent_checks_from_policies_task
|
||||
from .tasks import generate_agent_checks_task
|
||||
|
||||
# setup data
|
||||
policy = baker.make("automation.Policy", active=True)
|
||||
@@ -505,7 +537,7 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
agent = baker.make_recipe("agents.agent", policy=policy)
|
||||
|
||||
# test policy assigned to agent
|
||||
generate_agent_checks_from_policies_task(policy.id) # type: ignore
|
||||
generate_agent_checks_task(policy=policy.id) # type: ignore
|
||||
|
||||
# make sure all checks were created. should be 7
|
||||
agent_checks = Agent.objects.get(pk=agent.id).agentchecks.all()
|
||||
@@ -545,7 +577,7 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
self.assertEqual(check.event_type, checks[6].event_type)
|
||||
|
||||
def test_generating_agent_policy_checks_with_enforced(self):
|
||||
from .tasks import generate_agent_checks_from_policies_task
|
||||
from .tasks import generate_agent_checks_task
|
||||
|
||||
# setup data
|
||||
policy = baker.make("automation.Policy", active=True, enforced=True)
|
||||
@@ -555,7 +587,7 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
agent = baker.make_recipe("agents.agent", site=site, policy=policy)
|
||||
self.create_checks(agent=agent, script=script)
|
||||
|
||||
generate_agent_checks_from_policies_task(policy.id, create_tasks=True) # type: ignore
|
||||
generate_agent_checks_task(policy=policy.id, create_tasks=True) # type: ignore
|
||||
|
||||
# make sure each agent check says overriden_by_policy
|
||||
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 14)
|
||||
@@ -566,13 +598,12 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
7,
|
||||
)
|
||||
|
||||
@patch("automation.tasks.generate_agent_checks_by_location_task.delay")
|
||||
@patch("autotasks.models.AutomatedTask.create_task_on_agent")
|
||||
@patch("automation.tasks.generate_agent_checks_task.delay")
|
||||
def test_generating_agent_policy_checks_by_location(
|
||||
self, generate_agent_checks_by_location_task
|
||||
self, generate_agent_checks_mock, create_task
|
||||
):
|
||||
from automation.tasks import (
|
||||
generate_agent_checks_by_location_task as generate_agent_checks,
|
||||
)
|
||||
from automation.tasks import generate_agent_checks_task
|
||||
|
||||
# setup data
|
||||
policy = baker.make("automation.Policy", active=True)
|
||||
@@ -596,16 +627,14 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
workstation_agent.client.save()
|
||||
|
||||
# should trigger task in save method on core
|
||||
generate_agent_checks_by_location_task.assert_called_with(
|
||||
location={"site__client_id": workstation_agent.client.pk},
|
||||
mon_type="workstation",
|
||||
generate_agent_checks_mock.assert_called_with(
|
||||
client=workstation_agent.client.pk,
|
||||
create_tasks=True,
|
||||
)
|
||||
generate_agent_checks_by_location_task.reset_mock()
|
||||
generate_agent_checks_mock.reset_mock()
|
||||
|
||||
generate_agent_checks(
|
||||
location={"site__client_id": workstation_agent.client.pk},
|
||||
mon_type="workstation",
|
||||
generate_agent_checks_task(
|
||||
client=workstation_agent.client.pk,
|
||||
create_tasks=True,
|
||||
)
|
||||
|
||||
@@ -620,16 +649,14 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
workstation_agent.client.save()
|
||||
|
||||
# should trigger task in save method on core
|
||||
generate_agent_checks_by_location_task.assert_called_with(
|
||||
location={"site__client_id": workstation_agent.client.pk},
|
||||
mon_type="workstation",
|
||||
generate_agent_checks_mock.assert_called_with(
|
||||
client=workstation_agent.client.pk,
|
||||
create_tasks=True,
|
||||
)
|
||||
generate_agent_checks_by_location_task.reset_mock()
|
||||
generate_agent_checks_mock.reset_mock()
|
||||
|
||||
generate_agent_checks(
|
||||
location={"site__client_id": workstation_agent.client.pk},
|
||||
mon_type="workstation",
|
||||
generate_agent_checks_task(
|
||||
client=workstation_agent.client.pk,
|
||||
create_tasks=True,
|
||||
)
|
||||
|
||||
@@ -644,16 +671,14 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
server_agent.client.save()
|
||||
|
||||
# should trigger task in save method on core
|
||||
generate_agent_checks_by_location_task.assert_called_with(
|
||||
location={"site__client_id": server_agent.client.pk},
|
||||
mon_type="server",
|
||||
generate_agent_checks_mock.assert_called_with(
|
||||
client=server_agent.client.pk,
|
||||
create_tasks=True,
|
||||
)
|
||||
generate_agent_checks_by_location_task.reset_mock()
|
||||
generate_agent_checks_mock.reset_mock()
|
||||
|
||||
generate_agent_checks(
|
||||
location={"site__client_id": server_agent.client.pk},
|
||||
mon_type="server",
|
||||
generate_agent_checks_task(
|
||||
client=server_agent.client.pk,
|
||||
create_tasks=True,
|
||||
)
|
||||
|
||||
@@ -668,16 +693,14 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
server_agent.client.save()
|
||||
|
||||
# should trigger task in save method on core
|
||||
generate_agent_checks_by_location_task.assert_called_with(
|
||||
location={"site__client_id": server_agent.client.pk},
|
||||
mon_type="server",
|
||||
generate_agent_checks_mock.assert_called_with(
|
||||
client=server_agent.client.pk,
|
||||
create_tasks=True,
|
||||
)
|
||||
generate_agent_checks_by_location_task.reset_mock()
|
||||
generate_agent_checks_mock.reset_mock()
|
||||
|
||||
generate_agent_checks(
|
||||
location={"site__client_id": server_agent.client.pk},
|
||||
mon_type="server",
|
||||
generate_agent_checks_task(
|
||||
client=server_agent.client.pk,
|
||||
create_tasks=True,
|
||||
)
|
||||
|
||||
@@ -692,16 +715,14 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
workstation_agent.site.save()
|
||||
|
||||
# should trigger task in save method on core
|
||||
generate_agent_checks_by_location_task.assert_called_with(
|
||||
location={"site_id": workstation_agent.site.pk},
|
||||
mon_type="workstation",
|
||||
generate_agent_checks_mock.assert_called_with(
|
||||
site=workstation_agent.site.pk,
|
||||
create_tasks=True,
|
||||
)
|
||||
generate_agent_checks_by_location_task.reset_mock()
|
||||
generate_agent_checks_mock.reset_mock()
|
||||
|
||||
generate_agent_checks(
|
||||
location={"site_id": workstation_agent.site.pk},
|
||||
mon_type="workstation",
|
||||
generate_agent_checks_task(
|
||||
site=workstation_agent.site.pk,
|
||||
create_tasks=True,
|
||||
)
|
||||
|
||||
@@ -716,16 +737,14 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
workstation_agent.site.save()
|
||||
|
||||
# should trigger task in save method on core
|
||||
generate_agent_checks_by_location_task.assert_called_with(
|
||||
location={"site_id": workstation_agent.site.pk},
|
||||
mon_type="workstation",
|
||||
generate_agent_checks_mock.assert_called_with(
|
||||
site=workstation_agent.site.pk,
|
||||
create_tasks=True,
|
||||
)
|
||||
generate_agent_checks_by_location_task.reset_mock()
|
||||
generate_agent_checks_mock.reset_mock()
|
||||
|
||||
generate_agent_checks(
|
||||
location={"site_id": workstation_agent.site.pk},
|
||||
mon_type="workstation",
|
||||
generate_agent_checks_task(
|
||||
site=workstation_agent.site.pk,
|
||||
create_tasks=True,
|
||||
)
|
||||
|
||||
@@ -740,16 +759,14 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
server_agent.site.save()
|
||||
|
||||
# should trigger task in save method on core
|
||||
generate_agent_checks_by_location_task.assert_called_with(
|
||||
location={"site_id": server_agent.site.pk},
|
||||
mon_type="server",
|
||||
generate_agent_checks_mock.assert_called_with(
|
||||
site=server_agent.site.pk,
|
||||
create_tasks=True,
|
||||
)
|
||||
generate_agent_checks_by_location_task.reset_mock()
|
||||
generate_agent_checks_mock.reset_mock()
|
||||
|
||||
generate_agent_checks(
|
||||
location={"site_id": server_agent.site.pk},
|
||||
mon_type="server",
|
||||
generate_agent_checks_task(
|
||||
site=server_agent.site.pk,
|
||||
create_tasks=True,
|
||||
)
|
||||
|
||||
@@ -764,16 +781,14 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
server_agent.site.save()
|
||||
|
||||
# should trigger task in save method on core
|
||||
generate_agent_checks_by_location_task.assert_called_with(
|
||||
location={"site_id": server_agent.site.pk},
|
||||
mon_type="server",
|
||||
generate_agent_checks_mock.assert_called_with(
|
||||
site=server_agent.site.pk,
|
||||
create_tasks=True,
|
||||
)
|
||||
generate_agent_checks_by_location_task.reset_mock()
|
||||
generate_agent_checks_mock.reset_mock()
|
||||
|
||||
generate_agent_checks(
|
||||
location={"site_id": server_agent.site.pk},
|
||||
mon_type="server",
|
||||
generate_agent_checks_task(
|
||||
site=server_agent.site.pk,
|
||||
create_tasks=True,
|
||||
)
|
||||
|
||||
@@ -783,13 +798,11 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
Agent.objects.get(pk=workstation_agent.id).agentchecks.count(), 0
|
||||
)
|
||||
|
||||
@patch("automation.tasks.generate_all_agent_checks_task.delay")
|
||||
def test_generating_policy_checks_for_all_agents(
|
||||
self, generate_all_agent_checks_task
|
||||
):
|
||||
@patch("automation.tasks.generate_agent_checks_task.delay")
|
||||
def test_generating_policy_checks_for_all_agents(self, generate_agent_checks_mock):
|
||||
from core.models import CoreSettings
|
||||
|
||||
from .tasks import generate_all_agent_checks_task as generate_all_checks
|
||||
from .tasks import generate_agent_checks_task
|
||||
|
||||
# setup data
|
||||
policy = baker.make("automation.Policy", active=True)
|
||||
@@ -801,11 +814,9 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
core.server_policy = policy
|
||||
core.save()
|
||||
|
||||
generate_all_agent_checks_task.assert_called_with(
|
||||
mon_type="server", create_tasks=True
|
||||
)
|
||||
generate_all_agent_checks_task.reset_mock()
|
||||
generate_all_checks(mon_type="server", create_tasks=True)
|
||||
generate_agent_checks_mock.assert_called_with(all=True, create_tasks=True)
|
||||
generate_agent_checks_mock.reset_mock()
|
||||
generate_agent_checks_task(all=True, create_tasks=True)
|
||||
|
||||
# all servers should have 7 checks
|
||||
for agent in server_agents:
|
||||
@@ -818,15 +829,9 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
core.workstation_policy = policy
|
||||
core.save()
|
||||
|
||||
generate_all_agent_checks_task.assert_any_call(
|
||||
mon_type="workstation", create_tasks=True
|
||||
)
|
||||
generate_all_agent_checks_task.assert_any_call(
|
||||
mon_type="server", create_tasks=True
|
||||
)
|
||||
generate_all_agent_checks_task.reset_mock()
|
||||
generate_all_checks(mon_type="server", create_tasks=True)
|
||||
generate_all_checks(mon_type="workstation", create_tasks=True)
|
||||
generate_agent_checks_mock.assert_any_call(all=True, create_tasks=True)
|
||||
generate_agent_checks_mock.reset_mock()
|
||||
generate_agent_checks_task(all=True, create_tasks=True)
|
||||
|
||||
# all workstations should have 7 checks
|
||||
for agent in server_agents:
|
||||
@@ -838,11 +843,9 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
core.workstation_policy = None
|
||||
core.save()
|
||||
|
||||
generate_all_agent_checks_task.assert_called_with(
|
||||
mon_type="workstation", create_tasks=True
|
||||
)
|
||||
generate_all_agent_checks_task.reset_mock()
|
||||
generate_all_checks(mon_type="workstation", create_tasks=True)
|
||||
generate_agent_checks_mock.assert_called_with(all=True, create_tasks=True)
|
||||
generate_agent_checks_mock.reset_mock()
|
||||
generate_agent_checks_task(all=True, create_tasks=True)
|
||||
|
||||
# nothing should have the checks
|
||||
for agent in server_agents:
|
||||
@@ -851,31 +854,8 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
for agent in workstation_agents:
|
||||
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 0)
|
||||
|
||||
def test_delete_policy_check(self):
|
||||
from .models import Policy
|
||||
from .tasks import delete_policy_check_task
|
||||
|
||||
policy = baker.make("automation.Policy", active=True)
|
||||
self.create_checks(policy=policy)
|
||||
agent = baker.make_recipe("agents.server_agent", policy=policy)
|
||||
|
||||
# make sure agent has 7 checks
|
||||
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 7)
|
||||
|
||||
# pick a policy check and delete it from the agent
|
||||
policy_check_id = Policy.objects.get(pk=policy.id).policychecks.first().id # type: ignore
|
||||
|
||||
delete_policy_check_task(policy_check_id)
|
||||
|
||||
# make sure policy check doesn't exist on agent
|
||||
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 6)
|
||||
self.assertFalse(
|
||||
Agent.objects.get(pk=agent.id)
|
||||
.agentchecks.filter(parent_check=policy_check_id)
|
||||
.exists()
|
||||
)
|
||||
|
||||
def update_policy_check_fields(self):
|
||||
@patch("autotasks.models.AutomatedTask.create_task_on_agent")
|
||||
def update_policy_check_fields(self, create_task):
|
||||
from .models import Policy
|
||||
from .tasks import update_policy_check_fields_task
|
||||
|
||||
@@ -905,8 +885,9 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
"12.12.12.12",
|
||||
)
|
||||
|
||||
def test_generate_agent_tasks(self):
|
||||
from .tasks import generate_agent_tasks_from_policies_task
|
||||
@patch("autotasks.models.AutomatedTask.create_task_on_agent")
|
||||
def test_generate_agent_tasks(self, create_task):
|
||||
from .tasks import generate_agent_autotasks_task
|
||||
|
||||
# create test data
|
||||
policy = baker.make("automation.Policy", active=True)
|
||||
@@ -915,7 +896,7 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
)
|
||||
agent = baker.make_recipe("agents.server_agent", policy=policy)
|
||||
|
||||
generate_agent_tasks_from_policies_task(policy.id) # type: ignore
|
||||
generate_agent_autotasks_task(policy=policy.id) # type: ignore
|
||||
|
||||
agent_tasks = Agent.objects.get(pk=agent.id).autotasks.all()
|
||||
|
||||
@@ -934,56 +915,61 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
self.assertEqual(task.parent_task, tasks[2].id) # type: ignore
|
||||
self.assertEqual(task.name, tasks[2].name) # type: ignore
|
||||
|
||||
@patch("autotasks.tasks.delete_win_task_schedule.delay")
|
||||
def test_delete_policy_tasks(self, delete_win_task_schedule):
|
||||
from .tasks import delete_policy_autotask_task
|
||||
@patch("autotasks.models.AutomatedTask.create_task_on_agent")
|
||||
@patch("autotasks.models.AutomatedTask.delete_task_on_agent")
|
||||
def test_delete_policy_tasks(self, delete_task_on_agent, create_task):
|
||||
from .tasks import delete_policy_autotasks_task
|
||||
|
||||
policy = baker.make("automation.Policy", active=True)
|
||||
tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
|
||||
agent = baker.make_recipe("agents.server_agent", policy=policy)
|
||||
baker.make_recipe("agents.server_agent", policy=policy)
|
||||
|
||||
delete_policy_autotask_task(tasks[0].id) # type: ignore
|
||||
delete_policy_autotasks_task(task=tasks[0].id) # type: ignore
|
||||
|
||||
delete_win_task_schedule.assert_called_with(
|
||||
agent.autotasks.get(parent_task=tasks[0].id).id # type: ignore
|
||||
)
|
||||
delete_task_on_agent.assert_called()
|
||||
|
||||
@patch("autotasks.tasks.run_win_task.delay")
|
||||
def test_run_policy_task(self, run_win_task):
|
||||
from .tasks import run_win_policy_autotask_task
|
||||
@patch("autotasks.models.AutomatedTask.create_task_on_agent")
|
||||
@patch("autotasks.models.AutomatedTask.run_win_task")
|
||||
def test_run_policy_task(self, run_win_task, create_task):
|
||||
from .tasks import run_win_policy_autotasks_task
|
||||
|
||||
tasks = baker.make("autotasks.AutomatedTask", _quantity=3)
|
||||
policy = baker.make("automation.Policy", active=True)
|
||||
tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
|
||||
baker.make_recipe("agents.server_agent", policy=policy)
|
||||
|
||||
run_win_policy_autotask_task([task.id for task in tasks]) # type: ignore
|
||||
run_win_policy_autotasks_task(task=tasks[0].id) # type: ignore
|
||||
|
||||
run_win_task.side_effect = [task.id for task in tasks] # type: ignore
|
||||
self.assertEqual(run_win_task.call_count, 3)
|
||||
for task in tasks: # type: ignore
|
||||
run_win_task.assert_any_call(task.id) # type: ignore
|
||||
run_win_task.assert_called_once()
|
||||
|
||||
@patch("autotasks.tasks.enable_or_disable_win_task.delay")
|
||||
def test_update_policy_tasks(self, enable_or_disable_win_task):
|
||||
from .tasks import update_policy_task_fields_task
|
||||
@patch("autotasks.models.AutomatedTask.create_task_on_agent")
|
||||
@patch("autotasks.models.AutomatedTask.modify_task_on_agent")
|
||||
def test_update_policy_tasks(self, modify_task_on_agent, create_task):
|
||||
from .tasks import update_policy_autotasks_fields_task
|
||||
|
||||
# setup data
|
||||
policy = baker.make("automation.Policy", active=True)
|
||||
tasks = baker.make(
|
||||
"autotasks.AutomatedTask", enabled=True, policy=policy, _quantity=3
|
||||
"autotasks.AutomatedTask",
|
||||
enabled=True,
|
||||
policy=policy,
|
||||
_quantity=3,
|
||||
)
|
||||
agent = baker.make_recipe("agents.server_agent", policy=policy)
|
||||
|
||||
tasks[0].enabled = False # type: ignore
|
||||
tasks[0].save() # type: ignore
|
||||
|
||||
update_policy_task_fields_task(tasks[0].id) # type: ignore
|
||||
enable_or_disable_win_task.assert_not_called()
|
||||
update_policy_autotasks_fields_task(task=tasks[0].id) # type: ignore
|
||||
modify_task_on_agent.assert_not_called()
|
||||
|
||||
self.assertFalse(agent.autotasks.get(parent_task=tasks[0].id).enabled) # type: ignore
|
||||
|
||||
update_policy_task_fields_task(tasks[0].id, update_agent=True) # type: ignore
|
||||
enable_or_disable_win_task.assert_called_with(
|
||||
agent.autotasks.get(parent_task=tasks[0].id).id, False # type: ignore
|
||||
)
|
||||
update_policy_autotasks_fields_task(task=tasks[0].id, update_agent=True) # type: ignore
|
||||
modify_task_on_agent.assert_not_called()
|
||||
|
||||
agent.autotasks.update(sync_status="synced")
|
||||
update_policy_autotasks_fields_task(task=tasks[0].id, update_agent=True) # type: ignore
|
||||
modify_task_on_agent.assert_called_once()
|
||||
|
||||
@patch("agents.models.Agent.generate_tasks_from_policies")
|
||||
@patch("agents.models.Agent.generate_checks_from_policies")
|
||||
@@ -996,17 +982,19 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
generate_checks.reset_mock()
|
||||
generate_tasks.reset_mock()
|
||||
|
||||
generate_agent_checks_task([agent.pk for agent in agents])
|
||||
generate_agent_checks_task(agents=[agent.pk for agent in agents])
|
||||
self.assertEquals(generate_checks.call_count, 5)
|
||||
generate_tasks.assert_not_called()
|
||||
generate_checks.reset_mock()
|
||||
|
||||
generate_agent_checks_task([agent.pk for agent in agents], create_tasks=True)
|
||||
generate_agent_checks_task(
|
||||
agents=[agent.pk for agent in agents], create_tasks=True
|
||||
)
|
||||
self.assertEquals(generate_checks.call_count, 5)
|
||||
self.assertEquals(generate_checks.call_count, 5)
|
||||
|
||||
@patch("autotasks.tasks.delete_win_task_schedule.delay")
|
||||
def test_policy_exclusions(self, delete_task):
|
||||
@patch("autotasks.models.AutomatedTask.create_task_on_agent")
|
||||
def test_policy_exclusions(self, create_task):
|
||||
# setup data
|
||||
policy = baker.make("automation.Policy", active=True)
|
||||
baker.make_recipe("checks.memory_check", policy=policy)
|
||||
@@ -1028,8 +1016,6 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
|
||||
self.assertEqual(policy.related_agents().count(), 0) # type: ignore
|
||||
self.assertEqual(agent.agentchecks.count(), 0) # type: ignore
|
||||
delete_task.assert_called()
|
||||
delete_task.reset_mock()
|
||||
|
||||
# delete agent tasks
|
||||
agent.autotasks.all().delete()
|
||||
@@ -1051,8 +1037,6 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
|
||||
self.assertEqual(policy.related_agents().count(), 0) # type: ignore
|
||||
self.assertEqual(agent.agentchecks.count(), 0) # type: ignore
|
||||
delete_task.assert_called()
|
||||
delete_task.reset_mock()
|
||||
|
||||
# delete agent tasks and reset
|
||||
agent.autotasks.all().delete()
|
||||
@@ -1074,8 +1058,6 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
|
||||
self.assertEqual(policy.related_agents().count(), 0) # type: ignore
|
||||
self.assertEqual(agent.agentchecks.count(), 0) # type: ignore
|
||||
delete_task.assert_called()
|
||||
delete_task.reset_mock()
|
||||
|
||||
# delete agent tasks and reset
|
||||
agent.autotasks.all().delete()
|
||||
@@ -1103,11 +1085,82 @@ class TestPolicyTasks(TacticalTestCase):
|
||||
|
||||
self.assertEqual(policy.related_agents().count(), 0) # type: ignore
|
||||
self.assertEqual(agent.agentchecks.count(), 0) # type: ignore
|
||||
delete_task.assert_called()
|
||||
delete_task.reset_mock()
|
||||
|
||||
def test_removing_duplicate_pending_task_actions(self):
|
||||
pass
|
||||
@patch("autotasks.models.AutomatedTask.create_task_on_agent")
|
||||
def test_policy_inheritance_blocking(self, create_task):
|
||||
# setup data
|
||||
policy = baker.make("automation.Policy", active=True)
|
||||
baker.make_recipe("checks.memory_check", policy=policy)
|
||||
baker.make("autotasks.AutomatedTask", policy=policy)
|
||||
agent = baker.make_recipe("agents.agent", monitoring_type="server")
|
||||
|
||||
def test_creating_checks_with_assigned_tasks(self):
|
||||
pass
|
||||
core = CoreSettings.objects.first()
|
||||
core.server_policy = policy
|
||||
core.save()
|
||||
|
||||
agent.generate_checks_from_policies()
|
||||
agent.generate_tasks_from_policies()
|
||||
|
||||
# should get policies from default policy
|
||||
self.assertTrue(agent.autotasks.all())
|
||||
self.assertTrue(agent.agentchecks.all())
|
||||
|
||||
# test client blocking inheritance
|
||||
agent.site.client.block_policy_inheritance = True
|
||||
agent.site.client.save()
|
||||
|
||||
agent.generate_checks_from_policies()
|
||||
agent.generate_tasks_from_policies()
|
||||
|
||||
self.assertFalse(agent.autotasks.all())
|
||||
self.assertFalse(agent.agentchecks.all())
|
||||
|
||||
agent.site.client.server_policy = policy
|
||||
agent.site.client.save()
|
||||
|
||||
agent.generate_checks_from_policies()
|
||||
agent.generate_tasks_from_policies()
|
||||
|
||||
# should get policies from client policy
|
||||
self.assertTrue(agent.autotasks.all())
|
||||
self.assertTrue(agent.agentchecks.all())
|
||||
|
||||
# test site blocking inheritance
|
||||
agent.site.block_policy_inheritance = True
|
||||
agent.site.save()
|
||||
|
||||
agent.generate_checks_from_policies()
|
||||
agent.generate_tasks_from_policies()
|
||||
|
||||
self.assertFalse(agent.autotasks.all())
|
||||
self.assertFalse(agent.agentchecks.all())
|
||||
|
||||
agent.site.server_policy = policy
|
||||
agent.site.save()
|
||||
|
||||
agent.generate_checks_from_policies()
|
||||
agent.generate_tasks_from_policies()
|
||||
|
||||
# should get policies from site policy
|
||||
self.assertTrue(agent.autotasks.all())
|
||||
self.assertTrue(agent.agentchecks.all())
|
||||
|
||||
# test agent blocking inheritance
|
||||
agent.block_policy_inheritance = True
|
||||
agent.save()
|
||||
|
||||
agent.generate_checks_from_policies()
|
||||
agent.generate_tasks_from_policies()
|
||||
|
||||
self.assertFalse(agent.autotasks.all())
|
||||
self.assertFalse(agent.agentchecks.all())
|
||||
|
||||
agent.policy = policy
|
||||
agent.save()
|
||||
|
||||
agent.generate_checks_from_policies()
|
||||
agent.generate_tasks_from_policies()
|
||||
|
||||
# should get policies from agent policy
|
||||
self.assertTrue(agent.autotasks.all())
|
||||
self.assertTrue(agent.agentchecks.all())
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from agents.models import Agent
|
||||
from agents.serializers import AgentHostnameSerializer
|
||||
from autotasks.models import AutomatedTask
|
||||
from checks.models import Check
|
||||
from clients.models import Client
|
||||
from clients.serializers import ClientSerializer, SiteSerializer
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from tacticalrmm.utils import notify_error
|
||||
from winupdate.models import WinUpdatePolicy
|
||||
from winupdate.serializers import WinUpdatePolicySerializer
|
||||
|
||||
from .models import Policy
|
||||
from .permissions import AutomationPolicyPerms
|
||||
from .serializers import (
|
||||
AutoTasksFieldSerializer,
|
||||
PolicyCheckSerializer,
|
||||
@@ -22,10 +23,11 @@ from .serializers import (
|
||||
PolicyTableSerializer,
|
||||
PolicyTaskStatusSerializer,
|
||||
)
|
||||
from .tasks import run_win_policy_autotask_task
|
||||
|
||||
|
||||
class GetAddPolicies(APIView):
|
||||
permission_classes = [IsAuthenticated, AutomationPolicyPerms]
|
||||
|
||||
def get(self, request):
|
||||
policies = Policy.objects.all()
|
||||
|
||||
@@ -53,18 +55,30 @@ class GetAddPolicies(APIView):
|
||||
|
||||
|
||||
class GetUpdateDeletePolicy(APIView):
|
||||
permission_classes = [IsAuthenticated, AutomationPolicyPerms]
|
||||
|
||||
def get(self, request, pk):
|
||||
policy = get_object_or_404(Policy, pk=pk)
|
||||
|
||||
return Response(PolicySerializer(policy).data)
|
||||
|
||||
def put(self, request, pk):
|
||||
from .tasks import generate_agent_checks_task
|
||||
|
||||
policy = get_object_or_404(Policy, pk=pk)
|
||||
|
||||
serializer = PolicySerializer(instance=policy, data=request.data, partial=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
|
||||
# check for excluding objects and in the request and if present generate policies
|
||||
if (
|
||||
"excluded_sites" in request.data.keys()
|
||||
or "excluded_clients" in request.data.keys()
|
||||
or "excluded_agents" in request.data.keys()
|
||||
):
|
||||
generate_agent_checks_task.delay(policy=pk, create_tasks=True)
|
||||
|
||||
return Response("ok")
|
||||
|
||||
def delete(self, request, pk):
|
||||
@@ -76,10 +90,10 @@ class GetUpdateDeletePolicy(APIView):
|
||||
class PolicySync(APIView):
|
||||
def post(self, request):
|
||||
if "policy" in request.data.keys():
|
||||
from automation.tasks import generate_agent_checks_from_policies_task
|
||||
from automation.tasks import generate_agent_checks_task
|
||||
|
||||
generate_agent_checks_from_policies_task.delay(
|
||||
request.data["policy"], create_tasks=True
|
||||
generate_agent_checks_task.delay(
|
||||
policy=request.data["policy"], create_tasks=True
|
||||
)
|
||||
return Response("ok")
|
||||
|
||||
@@ -88,7 +102,7 @@ class PolicySync(APIView):
|
||||
|
||||
|
||||
class PolicyAutoTask(APIView):
|
||||
|
||||
permission_classes = [IsAuthenticated, AutomationPolicyPerms]
|
||||
# tasks associated with policy
|
||||
def get(self, request, pk):
|
||||
tasks = AutomatedTask.objects.filter(policy=pk)
|
||||
@@ -101,12 +115,15 @@ class PolicyAutoTask(APIView):
|
||||
|
||||
# bulk run win tasks associated with policy
|
||||
def put(self, request, task):
|
||||
tasks = AutomatedTask.objects.filter(parent_task=task)
|
||||
run_win_policy_autotask_task.delay([task.id for task in tasks])
|
||||
from .tasks import run_win_policy_autotasks_task
|
||||
|
||||
run_win_policy_autotasks_task.delay(task=task)
|
||||
return Response("Affected agent tasks will run shortly")
|
||||
|
||||
|
||||
class PolicyCheck(APIView):
|
||||
permission_classes = [IsAuthenticated, AutomationPolicyPerms]
|
||||
|
||||
def get(self, request, pk):
|
||||
checks = Check.objects.filter(policy__pk=pk, agent=None)
|
||||
return Response(PolicyCheckSerializer(checks, many=True).data)
|
||||
@@ -179,7 +196,7 @@ class GetRelated(APIView):
|
||||
|
||||
|
||||
class UpdatePatchPolicy(APIView):
|
||||
|
||||
permission_classes = [IsAuthenticated, AutomationPolicyPerms]
|
||||
# create new patch policy
|
||||
def post(self, request):
|
||||
policy = get_object_or_404(Policy, pk=request.data["policy"])
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Generated by Django 3.1.7 on 2021-04-04 00:32
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0019_globalkvstore'),
|
||||
('scripts', '0007_script_args'),
|
||||
('autotasks', '0018_automatedtask_run_asap_after_missed'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='automatedtask',
|
||||
name='custom_field',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='autotask', to='core.customfield'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='automatedtask',
|
||||
name='retvalue',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='automatedtask',
|
||||
name='script',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='autoscript', to='scripts.script'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.7 on 2021-04-21 02:26
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('autotasks', '0019_auto_20210404_0032'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='automatedtask',
|
||||
name='sync_status',
|
||||
field=models.CharField(choices=[('synced', 'Synced With Agent'), ('notsynced', 'Waiting On Agent Checkin'), ('pendingdeletion', 'Pending Deletion on Agent'), ('initial', 'Initial Task Sync')], default='initial', max_length=100),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 3.1.7 on 2021-04-27 14:11
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0021_customfield_hide_in_ui'),
|
||||
('autotasks', '0020_auto_20210421_0226'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='automatedtask',
|
||||
name='custom_field',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='autotasks', to='core.customfield'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.1 on 2021-05-29 03:26
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('autotasks', '0021_alter_automatedtask_custom_field'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='automatedtask',
|
||||
name='collector_all_output',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -1,16 +1,20 @@
|
||||
import asyncio
|
||||
import datetime as dt
|
||||
import random
|
||||
import string
|
||||
from typing import List
|
||||
|
||||
import pytz
|
||||
from alerts.models import SEVERITY_CHOICES
|
||||
from django.conf import settings
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.db import models
|
||||
from django.db.models.fields import DateTimeField
|
||||
from loguru import logger
|
||||
|
||||
from alerts.models import SEVERITY_CHOICES
|
||||
from django.db.utils import DatabaseError
|
||||
from django.utils import timezone as djangotime
|
||||
from logs.models import BaseAuditModel
|
||||
from loguru import logger
|
||||
from packaging import version as pyver
|
||||
from tacticalrmm.utils import bitdays_to_string
|
||||
|
||||
logger.configure(**settings.LOG_CONFIG)
|
||||
@@ -36,6 +40,7 @@ SYNC_STATUS_CHOICES = [
|
||||
("synced", "Synced With Agent"),
|
||||
("notsynced", "Waiting On Agent Checkin"),
|
||||
("pendingdeletion", "Pending Deletion on Agent"),
|
||||
("initial", "Initial Task Sync"),
|
||||
]
|
||||
|
||||
TASK_STATUS_CHOICES = [
|
||||
@@ -60,12 +65,19 @@ class AutomatedTask(BaseAuditModel):
|
||||
blank=True,
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
custom_field = models.ForeignKey(
|
||||
"core.CustomField",
|
||||
related_name="autotasks",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
script = models.ForeignKey(
|
||||
"scripts.Script",
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="autoscript",
|
||||
on_delete=models.CASCADE,
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
script_args = ArrayField(
|
||||
models.CharField(max_length=255, null=True, blank=True),
|
||||
@@ -93,6 +105,7 @@ class AutomatedTask(BaseAuditModel):
|
||||
task_type = models.CharField(
|
||||
max_length=100, choices=TASK_TYPE_CHOICES, default="manual"
|
||||
)
|
||||
collector_all_output = models.BooleanField(default=False)
|
||||
run_time_date = DateTimeField(null=True, blank=True)
|
||||
remove_if_not_scheduled = models.BooleanField(default=False)
|
||||
run_asap_after_missed = models.BooleanField(default=False) # added in agent v1.4.7
|
||||
@@ -100,6 +113,7 @@ class AutomatedTask(BaseAuditModel):
|
||||
parent_task = models.PositiveIntegerField(null=True, blank=True)
|
||||
win_task_name = models.CharField(max_length=255, null=True, blank=True)
|
||||
timeout = models.PositiveIntegerField(default=120)
|
||||
retvalue = models.TextField(null=True, blank=True)
|
||||
retcode = models.IntegerField(null=True, blank=True)
|
||||
stdout = models.TextField(null=True, blank=True)
|
||||
stderr = models.TextField(null=True, blank=True)
|
||||
@@ -110,7 +124,7 @@ class AutomatedTask(BaseAuditModel):
|
||||
max_length=30, choices=TASK_STATUS_CHOICES, default="pending"
|
||||
)
|
||||
sync_status = models.CharField(
|
||||
max_length=100, choices=SYNC_STATUS_CHOICES, default="notsynced"
|
||||
max_length=100, choices=SYNC_STATUS_CHOICES, default="initial"
|
||||
)
|
||||
alert_severity = models.CharField(
|
||||
max_length=30, choices=SEVERITY_CHOICES, default="info"
|
||||
@@ -147,6 +161,32 @@ class AutomatedTask(BaseAuditModel):
|
||||
|
||||
return self.last_run
|
||||
|
||||
# These fields will be duplicated on the agent tasks that are managed by a policy
|
||||
@property
|
||||
def policy_fields_to_copy(self) -> List[str]:
|
||||
return [
|
||||
"alert_severity",
|
||||
"email_alert",
|
||||
"text_alert",
|
||||
"dashboard_alert",
|
||||
"script",
|
||||
"script_args",
|
||||
"assigned_check",
|
||||
"name",
|
||||
"run_time_days",
|
||||
"run_time_minute",
|
||||
"run_time_bit_weekdays",
|
||||
"run_time_date",
|
||||
"task_type",
|
||||
"win_task_name",
|
||||
"timeout",
|
||||
"enabled",
|
||||
"remove_if_not_scheduled",
|
||||
"run_asap_after_missed",
|
||||
"custom_field",
|
||||
"collector_all_output",
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def generate_task_name():
|
||||
chars = string.ascii_letters
|
||||
@@ -159,69 +199,200 @@ class AutomatedTask(BaseAuditModel):
|
||||
|
||||
return TaskSerializer(task).data
|
||||
|
||||
def create_policy_task(self, agent=None, policy=None):
|
||||
from .tasks import create_win_task_schedule
|
||||
def create_policy_task(self, agent=None, policy=None, assigned_check=None):
|
||||
|
||||
# if policy is present, then this task is being copied to another policy
|
||||
# if agent is present, then this task is being created on an agent from a policy
|
||||
# exit if neither are set or if both are set
|
||||
if not agent and not policy or agent and policy:
|
||||
# also exit if assigned_check is set because this task will be created when the check is
|
||||
if (
|
||||
(not agent and not policy)
|
||||
or (agent and policy)
|
||||
or (self.assigned_check and not assigned_check)
|
||||
):
|
||||
return
|
||||
|
||||
assigned_check = None
|
||||
|
||||
# get correct assigned check to task if set
|
||||
if agent and self.assigned_check:
|
||||
# check if there is a matching check on the agent
|
||||
if agent.agentchecks.filter(parent_check=self.assigned_check.pk).exists():
|
||||
assigned_check = agent.agentchecks.filter(
|
||||
parent_check=self.assigned_check.pk
|
||||
).first()
|
||||
# check was overriden by agent and we need to use that agents check
|
||||
else:
|
||||
if agent.agentchecks.filter(
|
||||
check_type=self.assigned_check.check_type, overriden_by_policy=True
|
||||
).exists():
|
||||
assigned_check = agent.agentchecks.filter(
|
||||
check_type=self.assigned_check.check_type,
|
||||
overriden_by_policy=True,
|
||||
).first()
|
||||
elif policy and self.assigned_check:
|
||||
if policy.policychecks.filter(name=self.assigned_check.name).exists():
|
||||
assigned_check = policy.policychecks.filter(
|
||||
name=self.assigned_check.name
|
||||
).first()
|
||||
else:
|
||||
assigned_check = policy.policychecks.filter(
|
||||
check_type=self.assigned_check.check_type
|
||||
).first()
|
||||
|
||||
task = AutomatedTask.objects.create(
|
||||
agent=agent,
|
||||
policy=policy,
|
||||
managed_by_policy=bool(agent),
|
||||
parent_task=(self.pk if agent else None),
|
||||
alert_severity=self.alert_severity,
|
||||
email_alert=self.email_alert,
|
||||
text_alert=self.text_alert,
|
||||
dashboard_alert=self.dashboard_alert,
|
||||
script=self.script,
|
||||
script_args=self.script_args,
|
||||
assigned_check=assigned_check,
|
||||
name=self.name,
|
||||
run_time_days=self.run_time_days,
|
||||
run_time_minute=self.run_time_minute,
|
||||
run_time_bit_weekdays=self.run_time_bit_weekdays,
|
||||
run_time_date=self.run_time_date,
|
||||
task_type=self.task_type,
|
||||
win_task_name=self.win_task_name,
|
||||
timeout=self.timeout,
|
||||
enabled=self.enabled,
|
||||
remove_if_not_scheduled=self.remove_if_not_scheduled,
|
||||
run_asap_after_missed=self.run_asap_after_missed,
|
||||
)
|
||||
|
||||
create_win_task_schedule.delay(task.pk)
|
||||
for field in self.policy_fields_to_copy:
|
||||
if field != "assigned_check":
|
||||
setattr(task, field, getattr(self, field))
|
||||
|
||||
task.save()
|
||||
|
||||
if agent:
|
||||
task.create_task_on_agent()
|
||||
|
||||
def create_task_on_agent(self):
|
||||
from agents.models import Agent
|
||||
|
||||
agent = (
|
||||
Agent.objects.filter(pk=self.agent.pk)
|
||||
.only("pk", "version", "hostname", "agent_id")
|
||||
.first()
|
||||
)
|
||||
|
||||
if self.task_type == "scheduled":
|
||||
nats_data = {
|
||||
"func": "schedtask",
|
||||
"schedtaskpayload": {
|
||||
"type": "rmm",
|
||||
"trigger": "weekly",
|
||||
"weekdays": self.run_time_bit_weekdays,
|
||||
"pk": self.pk,
|
||||
"name": self.win_task_name,
|
||||
"hour": dt.datetime.strptime(self.run_time_minute, "%H:%M").hour,
|
||||
"min": dt.datetime.strptime(self.run_time_minute, "%H:%M").minute,
|
||||
},
|
||||
}
|
||||
|
||||
elif self.task_type == "runonce":
|
||||
# check if scheduled time is in the past
|
||||
agent_tz = pytz.timezone(agent.timezone)
|
||||
task_time_utc = self.run_time_date.replace(tzinfo=agent_tz).astimezone(
|
||||
pytz.utc
|
||||
)
|
||||
now = djangotime.now()
|
||||
if task_time_utc < now:
|
||||
self.run_time_date = now.astimezone(agent_tz).replace(
|
||||
tzinfo=pytz.utc
|
||||
) + djangotime.timedelta(minutes=5)
|
||||
self.save(update_fields=["run_time_date"])
|
||||
|
||||
nats_data = {
|
||||
"func": "schedtask",
|
||||
"schedtaskpayload": {
|
||||
"type": "rmm",
|
||||
"trigger": "once",
|
||||
"pk": self.pk,
|
||||
"name": self.win_task_name,
|
||||
"year": int(dt.datetime.strftime(self.run_time_date, "%Y")),
|
||||
"month": dt.datetime.strftime(self.run_time_date, "%B"),
|
||||
"day": int(dt.datetime.strftime(self.run_time_date, "%d")),
|
||||
"hour": int(dt.datetime.strftime(self.run_time_date, "%H")),
|
||||
"min": int(dt.datetime.strftime(self.run_time_date, "%M")),
|
||||
},
|
||||
}
|
||||
|
||||
if self.run_asap_after_missed and pyver.parse(agent.version) >= pyver.parse(
|
||||
"1.4.7"
|
||||
):
|
||||
nats_data["schedtaskpayload"]["run_asap_after_missed"] = True
|
||||
|
||||
if self.remove_if_not_scheduled:
|
||||
nats_data["schedtaskpayload"]["deleteafter"] = True
|
||||
|
||||
elif self.task_type == "checkfailure" or self.task_type == "manual":
|
||||
nats_data = {
|
||||
"func": "schedtask",
|
||||
"schedtaskpayload": {
|
||||
"type": "rmm",
|
||||
"trigger": "manual",
|
||||
"pk": self.pk,
|
||||
"name": self.win_task_name,
|
||||
},
|
||||
}
|
||||
else:
|
||||
return "error"
|
||||
|
||||
r = asyncio.run(agent.nats_cmd(nats_data, timeout=5))
|
||||
|
||||
if r != "ok":
|
||||
self.sync_status = "initial"
|
||||
self.save(update_fields=["sync_status"])
|
||||
logger.warning(
|
||||
f"Unable to create scheduled task {self.name} on {agent.hostname}. It will be created when the agent checks in."
|
||||
)
|
||||
return "timeout"
|
||||
else:
|
||||
self.sync_status = "synced"
|
||||
self.save(update_fields=["sync_status"])
|
||||
logger.info(f"{agent.hostname} task {self.name} was successfully created")
|
||||
|
||||
return "ok"
|
||||
|
||||
def modify_task_on_agent(self):
|
||||
from agents.models import Agent
|
||||
|
||||
agent = (
|
||||
Agent.objects.filter(pk=self.agent.pk)
|
||||
.only("pk", "version", "hostname", "agent_id")
|
||||
.first()
|
||||
)
|
||||
|
||||
nats_data = {
|
||||
"func": "enableschedtask",
|
||||
"schedtaskpayload": {
|
||||
"name": self.win_task_name,
|
||||
"enabled": self.enabled,
|
||||
},
|
||||
}
|
||||
r = asyncio.run(agent.nats_cmd(nats_data, timeout=5))
|
||||
|
||||
if r != "ok":
|
||||
self.sync_status = "notsynced"
|
||||
self.save(update_fields=["sync_status"])
|
||||
logger.warning(
|
||||
f"Unable to modify scheduled task {self.name} on {agent.hostname}. It will try again on next agent checkin"
|
||||
)
|
||||
return "timeout"
|
||||
else:
|
||||
self.sync_status = "synced"
|
||||
self.save(update_fields=["sync_status"])
|
||||
logger.info(f"{agent.hostname} task {self.name} was successfully modified")
|
||||
|
||||
return "ok"
|
||||
|
||||
def delete_task_on_agent(self):
|
||||
from agents.models import Agent
|
||||
|
||||
agent = (
|
||||
Agent.objects.filter(pk=self.agent.pk)
|
||||
.only("pk", "version", "hostname", "agent_id")
|
||||
.first()
|
||||
)
|
||||
|
||||
nats_data = {
|
||||
"func": "delschedtask",
|
||||
"schedtaskpayload": {"name": self.win_task_name},
|
||||
}
|
||||
r = asyncio.run(agent.nats_cmd(nats_data, timeout=10))
|
||||
|
||||
if r != "ok" and "The system cannot find the file specified" not in r:
|
||||
self.sync_status = "pendingdeletion"
|
||||
|
||||
try:
|
||||
self.save(update_fields=["sync_status"])
|
||||
except DatabaseError:
|
||||
pass
|
||||
|
||||
logger.warning(
|
||||
f"{agent.hostname} task {self.name} will be deleted on next checkin"
|
||||
)
|
||||
return "timeout"
|
||||
else:
|
||||
self.delete()
|
||||
logger.info(f"{agent.hostname} task {self.name} was deleted")
|
||||
|
||||
return "ok"
|
||||
|
||||
def run_win_task(self):
|
||||
from agents.models import Agent
|
||||
|
||||
agent = (
|
||||
Agent.objects.filter(pk=self.agent.pk)
|
||||
.only("pk", "version", "hostname", "agent_id")
|
||||
.first()
|
||||
)
|
||||
|
||||
asyncio.run(agent.nats_cmd({"func": "runtask", "taskpk": self.pk}, wait=False))
|
||||
return "ok"
|
||||
|
||||
def should_create_alert(self, alert_template=None):
|
||||
return (
|
||||
|
||||
16
api/tacticalrmm/autotasks/permissions.py
Normal file
16
api/tacticalrmm/autotasks/permissions.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
from tacticalrmm.permissions import _has_perm
|
||||
|
||||
|
||||
class ManageAutoTaskPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
if r.method == "GET":
|
||||
return True
|
||||
|
||||
return _has_perm(r, "can_manage_autotasks")
|
||||
|
||||
|
||||
class RunAutoTaskPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_run_autotasks")
|
||||
@@ -68,6 +68,12 @@ class TaskRunnerGetSerializer(serializers.ModelSerializer):
|
||||
|
||||
class TaskGOGetSerializer(serializers.ModelSerializer):
|
||||
script = ScriptCheckSerializer(read_only=True)
|
||||
script_args = serializers.SerializerMethodField()
|
||||
|
||||
def get_script_args(self, obj):
|
||||
return Script.parse_script_args(
|
||||
agent=obj.agent, shell=obj.script.shell, args=obj.script_args
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = AutomatedTask
|
||||
|
||||
@@ -4,207 +4,46 @@ import random
|
||||
from time import sleep
|
||||
from typing import Union
|
||||
|
||||
import pytz
|
||||
from django.conf import settings
|
||||
from django.utils import timezone as djangotime
|
||||
from loguru import logger
|
||||
from packaging import version as pyver
|
||||
|
||||
from logs.models import PendingAction
|
||||
from autotasks.models import AutomatedTask
|
||||
from tacticalrmm.celery import app
|
||||
|
||||
from .models import AutomatedTask
|
||||
|
||||
logger.configure(**settings.LOG_CONFIG)
|
||||
|
||||
|
||||
@app.task
|
||||
def create_win_task_schedule(pk, pending_action=False):
|
||||
def create_win_task_schedule(pk):
|
||||
task = AutomatedTask.objects.get(pk=pk)
|
||||
|
||||
if task.task_type == "scheduled":
|
||||
nats_data = {
|
||||
"func": "schedtask",
|
||||
"schedtaskpayload": {
|
||||
"type": "rmm",
|
||||
"trigger": "weekly",
|
||||
"weekdays": task.run_time_bit_weekdays,
|
||||
"pk": task.pk,
|
||||
"name": task.win_task_name,
|
||||
"hour": dt.datetime.strptime(task.run_time_minute, "%H:%M").hour,
|
||||
"min": dt.datetime.strptime(task.run_time_minute, "%H:%M").minute,
|
||||
},
|
||||
}
|
||||
|
||||
elif task.task_type == "runonce":
|
||||
# check if scheduled time is in the past
|
||||
agent_tz = pytz.timezone(task.agent.timezone)
|
||||
task_time_utc = task.run_time_date.replace(tzinfo=agent_tz).astimezone(pytz.utc)
|
||||
now = djangotime.now()
|
||||
if task_time_utc < now:
|
||||
task.run_time_date = now.astimezone(agent_tz).replace(
|
||||
tzinfo=pytz.utc
|
||||
) + djangotime.timedelta(minutes=5)
|
||||
task.save(update_fields=["run_time_date"])
|
||||
|
||||
nats_data = {
|
||||
"func": "schedtask",
|
||||
"schedtaskpayload": {
|
||||
"type": "rmm",
|
||||
"trigger": "once",
|
||||
"pk": task.pk,
|
||||
"name": task.win_task_name,
|
||||
"year": int(dt.datetime.strftime(task.run_time_date, "%Y")),
|
||||
"month": dt.datetime.strftime(task.run_time_date, "%B"),
|
||||
"day": int(dt.datetime.strftime(task.run_time_date, "%d")),
|
||||
"hour": int(dt.datetime.strftime(task.run_time_date, "%H")),
|
||||
"min": int(dt.datetime.strftime(task.run_time_date, "%M")),
|
||||
},
|
||||
}
|
||||
|
||||
if task.run_asap_after_missed and pyver.parse(
|
||||
task.agent.version
|
||||
) >= pyver.parse("1.4.7"):
|
||||
nats_data["schedtaskpayload"]["run_asap_after_missed"] = True
|
||||
|
||||
if task.remove_if_not_scheduled:
|
||||
nats_data["schedtaskpayload"]["deleteafter"] = True
|
||||
|
||||
elif task.task_type == "checkfailure" or task.task_type == "manual":
|
||||
nats_data = {
|
||||
"func": "schedtask",
|
||||
"schedtaskpayload": {
|
||||
"type": "rmm",
|
||||
"trigger": "manual",
|
||||
"pk": task.pk,
|
||||
"name": task.win_task_name,
|
||||
},
|
||||
}
|
||||
else:
|
||||
return "error"
|
||||
|
||||
r = asyncio.run(task.agent.nats_cmd(nats_data, timeout=10))
|
||||
|
||||
if r != "ok":
|
||||
# don't create pending action if this task was initiated by a pending action
|
||||
if not pending_action:
|
||||
|
||||
# complete any other pending actions on agent with same task_id
|
||||
task.agent.remove_matching_pending_task_actions(task.id)
|
||||
|
||||
PendingAction(
|
||||
agent=task.agent,
|
||||
action_type="taskaction",
|
||||
details={"action": "taskcreate", "task_id": task.id},
|
||||
).save()
|
||||
task.sync_status = "notsynced"
|
||||
task.save(update_fields=["sync_status"])
|
||||
|
||||
logger.error(
|
||||
f"Unable to create scheduled task {task.win_task_name} on {task.agent.hostname}. It will be created when the agent checks in."
|
||||
)
|
||||
return
|
||||
|
||||
# clear pending action since it was successful
|
||||
if pending_action:
|
||||
pendingaction = PendingAction.objects.get(pk=pending_action)
|
||||
pendingaction.status = "completed"
|
||||
pendingaction.save(update_fields=["status"])
|
||||
|
||||
task.sync_status = "synced"
|
||||
task.save(update_fields=["sync_status"])
|
||||
|
||||
logger.info(f"{task.agent.hostname} task {task.name} was successfully created")
|
||||
task.create_task_on_agent()
|
||||
|
||||
return "ok"
|
||||
|
||||
|
||||
@app.task
|
||||
def enable_or_disable_win_task(pk, action, pending_action=False):
|
||||
def enable_or_disable_win_task(pk):
|
||||
task = AutomatedTask.objects.get(pk=pk)
|
||||
|
||||
nats_data = {
|
||||
"func": "enableschedtask",
|
||||
"schedtaskpayload": {
|
||||
"name": task.win_task_name,
|
||||
"enabled": action,
|
||||
},
|
||||
}
|
||||
r = asyncio.run(task.agent.nats_cmd(nats_data))
|
||||
|
||||
if r != "ok":
|
||||
# don't create pending action if this task was initiated by a pending action
|
||||
if not pending_action:
|
||||
PendingAction(
|
||||
agent=task.agent,
|
||||
action_type="taskaction",
|
||||
details={
|
||||
"action": "tasktoggle",
|
||||
"value": action,
|
||||
"task_id": task.id,
|
||||
},
|
||||
).save()
|
||||
task.sync_status = "notsynced"
|
||||
task.save(update_fields=["sync_status"])
|
||||
|
||||
return
|
||||
|
||||
# clear pending action since it was successful
|
||||
if pending_action:
|
||||
pendingaction = PendingAction.objects.get(pk=pending_action)
|
||||
pendingaction.status = "completed"
|
||||
pendingaction.save(update_fields=["status"])
|
||||
|
||||
task.sync_status = "synced"
|
||||
task.save(update_fields=["sync_status"])
|
||||
task.modify_task_on_agent()
|
||||
|
||||
return "ok"
|
||||
|
||||
|
||||
@app.task
|
||||
def delete_win_task_schedule(pk, pending_action=False):
|
||||
def delete_win_task_schedule(pk):
|
||||
task = AutomatedTask.objects.get(pk=pk)
|
||||
|
||||
nats_data = {
|
||||
"func": "delschedtask",
|
||||
"schedtaskpayload": {"name": task.win_task_name},
|
||||
}
|
||||
r = asyncio.run(task.agent.nats_cmd(nats_data, timeout=10))
|
||||
|
||||
if r != "ok" and "The system cannot find the file specified" not in r:
|
||||
# don't create pending action if this task was initiated by a pending action
|
||||
if not pending_action:
|
||||
|
||||
# complete any other pending actions on agent with same task_id
|
||||
task.agent.remove_matching_pending_task_actions(task.id)
|
||||
|
||||
PendingAction(
|
||||
agent=task.agent,
|
||||
action_type="taskaction",
|
||||
details={"action": "taskdelete", "task_id": task.id},
|
||||
).save()
|
||||
task.sync_status = "pendingdeletion"
|
||||
task.save(update_fields=["sync_status"])
|
||||
|
||||
return "timeout"
|
||||
|
||||
# complete pending action since it was successful
|
||||
if pending_action:
|
||||
pendingaction = PendingAction.objects.get(pk=pending_action)
|
||||
pendingaction.status = "completed"
|
||||
pendingaction.save(update_fields=["status"])
|
||||
|
||||
# complete any other pending actions on agent with same task_id
|
||||
task.agent.remove_matching_pending_task_actions(task.id)
|
||||
|
||||
task.delete()
|
||||
task.delete_task_on_agent()
|
||||
return "ok"
|
||||
|
||||
|
||||
@app.task
|
||||
def run_win_task(pk):
|
||||
task = AutomatedTask.objects.get(pk=pk)
|
||||
asyncio.run(task.agent.nats_cmd({"func": "runtask", "taskpk": task.pk}, wait=False))
|
||||
task.run_win_task()
|
||||
return "ok"
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ from unittest.mock import call, patch
|
||||
from django.utils import timezone as djangotime
|
||||
from model_bakery import baker
|
||||
|
||||
from logs.models import PendingAction
|
||||
from tacticalrmm.test import TacticalTestCase
|
||||
|
||||
from .models import AutomatedTask
|
||||
@@ -17,10 +16,10 @@ class TestAutotaskViews(TacticalTestCase):
|
||||
self.authenticate()
|
||||
self.setup_coresettings()
|
||||
|
||||
@patch("automation.tasks.generate_agent_tasks_from_policies_task.delay")
|
||||
@patch("automation.tasks.generate_agent_autotasks_task.delay")
|
||||
@patch("autotasks.tasks.create_win_task_schedule.delay")
|
||||
def test_add_autotask(
|
||||
self, create_win_task_schedule, generate_agent_tasks_from_policies_task
|
||||
self, create_win_task_schedule, generate_agent_autotasks_task
|
||||
):
|
||||
url = "/tasks/automatedtasks/"
|
||||
|
||||
@@ -84,13 +83,13 @@ class TestAutotaskViews(TacticalTestCase):
|
||||
"task_type": "manual",
|
||||
"assigned_check": None,
|
||||
},
|
||||
"policy": policy.id,
|
||||
"policy": policy.id, # type: ignore
|
||||
}
|
||||
|
||||
resp = self.client.post(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
generate_agent_tasks_from_policies_task.assert_called_with(policy.id)
|
||||
generate_agent_autotasks_task.assert_called_with(policy=policy.id) # type: ignore
|
||||
|
||||
self.check_not_authenticated("post", url)
|
||||
|
||||
@@ -106,14 +105,14 @@ class TestAutotaskViews(TacticalTestCase):
|
||||
serializer = AutoTaskSerializer(agent)
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(resp.data, serializer.data)
|
||||
self.assertEqual(resp.data, serializer.data) # type: ignore
|
||||
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
@patch("autotasks.tasks.enable_or_disable_win_task.delay")
|
||||
@patch("automation.tasks.update_policy_task_fields_task.delay")
|
||||
@patch("automation.tasks.update_policy_autotasks_fields_task.delay")
|
||||
def test_update_autotask(
|
||||
self, update_policy_task_fields_task, enable_or_disable_win_task
|
||||
self, update_policy_autotasks_fields_task, enable_or_disable_win_task
|
||||
):
|
||||
# setup data
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
@@ -125,32 +124,32 @@ class TestAutotaskViews(TacticalTestCase):
|
||||
resp = self.client.patch("/tasks/500/automatedtasks/", format="json")
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
url = f"/tasks/{agent_task.id}/automatedtasks/"
|
||||
url = f"/tasks/{agent_task.id}/automatedtasks/" # type: ignore
|
||||
|
||||
# test editing agent task
|
||||
data = {"enableordisable": False}
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
enable_or_disable_win_task.assert_called_with(pk=agent_task.id, action=False)
|
||||
enable_or_disable_win_task.assert_called_with(pk=agent_task.id) # type: ignore
|
||||
|
||||
url = f"/tasks/{policy_task.id}/automatedtasks/"
|
||||
url = f"/tasks/{policy_task.id}/automatedtasks/" # type: ignore
|
||||
|
||||
# test editing policy task
|
||||
data = {"enableordisable": True}
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
update_policy_task_fields_task.assert_called_with(
|
||||
policy_task.id, update_agent=True
|
||||
update_policy_autotasks_fields_task.assert_called_with(
|
||||
task=policy_task.id, update_agent=True # type: ignore
|
||||
)
|
||||
|
||||
self.check_not_authenticated("patch", url)
|
||||
|
||||
@patch("autotasks.tasks.delete_win_task_schedule.delay")
|
||||
@patch("automation.tasks.delete_policy_autotask_task.delay")
|
||||
@patch("automation.tasks.delete_policy_autotasks_task.delay")
|
||||
def test_delete_autotask(
|
||||
self, delete_policy_autotask_task, delete_win_task_schedule
|
||||
self, delete_policy_autotasks_task, delete_win_task_schedule
|
||||
):
|
||||
# setup data
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
@@ -163,21 +162,22 @@ class TestAutotaskViews(TacticalTestCase):
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
# test delete agent task
|
||||
url = f"/tasks/{agent_task.id}/automatedtasks/"
|
||||
url = f"/tasks/{agent_task.id}/automatedtasks/" # type: ignore
|
||||
resp = self.client.delete(url, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
delete_win_task_schedule.assert_called_with(pk=agent_task.id)
|
||||
delete_win_task_schedule.assert_called_with(pk=agent_task.id) # type: ignore
|
||||
|
||||
# test delete policy task
|
||||
url = f"/tasks/{policy_task.id}/automatedtasks/"
|
||||
url = f"/tasks/{policy_task.id}/automatedtasks/" # type: ignore
|
||||
resp = self.client.delete(url, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
delete_policy_autotask_task.assert_called_with(policy_task.id)
|
||||
self.assertFalse(AutomatedTask.objects.filter(pk=policy_task.id)) # type: ignore
|
||||
delete_policy_autotasks_task.assert_called_with(task=policy_task.id) # type: ignore
|
||||
|
||||
self.check_not_authenticated("delete", url)
|
||||
|
||||
@patch("agents.models.Agent.nats_cmd")
|
||||
def test_run_autotask(self, nats_cmd):
|
||||
@patch("autotasks.tasks.run_win_task.delay")
|
||||
def test_run_autotask(self, run_win_task):
|
||||
# setup data
|
||||
agent = baker.make_recipe("agents.agent", version="1.1.0")
|
||||
task = baker.make("autotasks.AutomatedTask", agent=agent)
|
||||
@@ -187,11 +187,10 @@ class TestAutotaskViews(TacticalTestCase):
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
# test run agent task
|
||||
url = f"/tasks/runwintask/{task.id}/"
|
||||
url = f"/tasks/runwintask/{task.id}/" # type: ignore
|
||||
resp = self.client.get(url, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
nats_cmd.assert_called_with({"func": "runtask", "taskpk": task.id}, wait=False)
|
||||
nats_cmd.reset_mock()
|
||||
run_win_task.assert_called()
|
||||
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
@@ -284,9 +283,9 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
|
||||
run_time_bit_weekdays=127,
|
||||
run_time_minute="21:55",
|
||||
)
|
||||
self.assertEqual(self.task1.sync_status, "notsynced")
|
||||
self.assertEqual(self.task1.sync_status, "initial")
|
||||
nats_cmd.return_value = "ok"
|
||||
ret = create_win_task_schedule.s(pk=self.task1.pk, pending_action=False).apply()
|
||||
ret = create_win_task_schedule.s(pk=self.task1.pk).apply()
|
||||
self.assertEqual(nats_cmd.call_count, 1)
|
||||
nats_cmd.assert_called_with(
|
||||
{
|
||||
@@ -301,29 +300,16 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
|
||||
"min": 55,
|
||||
},
|
||||
},
|
||||
timeout=10,
|
||||
timeout=5,
|
||||
)
|
||||
self.task1 = AutomatedTask.objects.get(pk=self.task1.pk)
|
||||
self.assertEqual(self.task1.sync_status, "synced")
|
||||
|
||||
nats_cmd.return_value = "timeout"
|
||||
ret = create_win_task_schedule.s(pk=self.task1.pk, pending_action=False).apply()
|
||||
ret = create_win_task_schedule.s(pk=self.task1.pk).apply()
|
||||
self.assertEqual(ret.status, "SUCCESS")
|
||||
self.task1 = AutomatedTask.objects.get(pk=self.task1.pk)
|
||||
self.assertEqual(self.task1.sync_status, "notsynced")
|
||||
|
||||
# test pending action
|
||||
self.pending_action = PendingAction.objects.create(
|
||||
agent=self.agent, action_type="taskaction"
|
||||
)
|
||||
self.assertEqual(self.pending_action.status, "pending")
|
||||
nats_cmd.return_value = "ok"
|
||||
ret = create_win_task_schedule.s(
|
||||
pk=self.task1.pk, pending_action=self.pending_action.pk
|
||||
).apply()
|
||||
self.assertEqual(ret.status, "SUCCESS")
|
||||
self.pending_action = PendingAction.objects.get(pk=self.pending_action.pk)
|
||||
self.assertEqual(self.pending_action.status, "completed")
|
||||
self.assertEqual(self.task1.sync_status, "initial")
|
||||
|
||||
# test runonce with future date
|
||||
nats_cmd.reset_mock()
|
||||
@@ -337,7 +323,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
|
||||
run_time_date=run_time_date,
|
||||
)
|
||||
nats_cmd.return_value = "ok"
|
||||
ret = create_win_task_schedule.s(pk=self.task2.pk, pending_action=False).apply()
|
||||
ret = create_win_task_schedule.s(pk=self.task2.pk).apply()
|
||||
nats_cmd.assert_called_with(
|
||||
{
|
||||
"func": "schedtask",
|
||||
@@ -353,7 +339,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
|
||||
"min": int(dt.datetime.strftime(self.task2.run_time_date, "%M")),
|
||||
},
|
||||
},
|
||||
timeout=10,
|
||||
timeout=5,
|
||||
)
|
||||
self.assertEqual(ret.status, "SUCCESS")
|
||||
|
||||
@@ -369,7 +355,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
|
||||
run_time_date=run_time_date,
|
||||
)
|
||||
nats_cmd.return_value = "ok"
|
||||
ret = create_win_task_schedule.s(pk=self.task3.pk, pending_action=False).apply()
|
||||
ret = create_win_task_schedule.s(pk=self.task3.pk).apply()
|
||||
self.task3 = AutomatedTask.objects.get(pk=self.task3.pk)
|
||||
self.assertEqual(ret.status, "SUCCESS")
|
||||
|
||||
@@ -385,7 +371,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
|
||||
assigned_check=self.check,
|
||||
)
|
||||
nats_cmd.return_value = "ok"
|
||||
ret = create_win_task_schedule.s(pk=self.task4.pk, pending_action=False).apply()
|
||||
ret = create_win_task_schedule.s(pk=self.task4.pk).apply()
|
||||
nats_cmd.assert_called_with(
|
||||
{
|
||||
"func": "schedtask",
|
||||
@@ -396,7 +382,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
|
||||
"name": task_name,
|
||||
},
|
||||
},
|
||||
timeout=10,
|
||||
timeout=5,
|
||||
)
|
||||
self.assertEqual(ret.status, "SUCCESS")
|
||||
|
||||
@@ -410,7 +396,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
|
||||
task_type="manual",
|
||||
)
|
||||
nats_cmd.return_value = "ok"
|
||||
ret = create_win_task_schedule.s(pk=self.task5.pk, pending_action=False).apply()
|
||||
ret = create_win_task_schedule.s(pk=self.task5.pk).apply()
|
||||
nats_cmd.assert_called_with(
|
||||
{
|
||||
"func": "schedtask",
|
||||
@@ -421,6 +407,6 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
|
||||
"name": task_name,
|
||||
},
|
||||
},
|
||||
timeout=10,
|
||||
timeout=5,
|
||||
)
|
||||
self.assertEqual(ret.status, "SUCCESS")
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import asyncio
|
||||
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
@@ -11,18 +10,17 @@ from scripts.models import Script
|
||||
from tacticalrmm.utils import get_bit_days, get_default_timezone, notify_error
|
||||
|
||||
from .models import AutomatedTask
|
||||
from .permissions import ManageAutoTaskPerms, RunAutoTaskPerms
|
||||
from .serializers import AutoTaskSerializer, TaskSerializer
|
||||
from .tasks import (
|
||||
create_win_task_schedule,
|
||||
delete_win_task_schedule,
|
||||
enable_or_disable_win_task,
|
||||
)
|
||||
|
||||
|
||||
class AddAutoTask(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageAutoTaskPerms]
|
||||
|
||||
def post(self, request):
|
||||
from automation.models import Policy
|
||||
from automation.tasks import generate_agent_tasks_from_policies_task
|
||||
from automation.tasks import generate_agent_autotasks_task
|
||||
from autotasks.tasks import create_win_task_schedule
|
||||
|
||||
data = request.data
|
||||
script = get_object_or_404(Script, pk=data["autotask"]["script"])
|
||||
@@ -47,7 +45,7 @@ class AddAutoTask(APIView):
|
||||
del data["autotask"]["run_time_days"]
|
||||
serializer = TaskSerializer(data=data["autotask"], partial=True, context=parent)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
obj = serializer.save(
|
||||
task = serializer.save(
|
||||
**parent,
|
||||
script=script,
|
||||
win_task_name=AutomatedTask.generate_task_name(),
|
||||
@@ -55,16 +53,18 @@ class AddAutoTask(APIView):
|
||||
run_time_bit_weekdays=bit_weekdays,
|
||||
)
|
||||
|
||||
if not "policy" in data:
|
||||
create_win_task_schedule.delay(pk=obj.pk)
|
||||
if task.agent:
|
||||
create_win_task_schedule.delay(pk=task.pk)
|
||||
|
||||
if "policy" in data:
|
||||
generate_agent_tasks_from_policies_task.delay(data["policy"])
|
||||
elif task.policy:
|
||||
generate_agent_autotasks_task.delay(policy=task.policy.pk)
|
||||
|
||||
return Response("Task will be created shortly!")
|
||||
|
||||
|
||||
class AutoTask(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageAutoTaskPerms]
|
||||
|
||||
def get(self, request, pk):
|
||||
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
@@ -75,7 +75,7 @@ class AutoTask(APIView):
|
||||
return Response(AutoTaskSerializer(agent, context=ctx).data)
|
||||
|
||||
def put(self, request, pk):
|
||||
from automation.tasks import update_policy_task_fields_task
|
||||
from automation.tasks import update_policy_autotasks_fields_task
|
||||
|
||||
task = get_object_or_404(AutomatedTask, pk=pk)
|
||||
|
||||
@@ -84,46 +84,54 @@ class AutoTask(APIView):
|
||||
serializer.save()
|
||||
|
||||
if task.policy:
|
||||
update_policy_task_fields_task.delay(task.pk)
|
||||
update_policy_autotasks_fields_task.delay(task=task.pk)
|
||||
|
||||
return Response("ok")
|
||||
|
||||
def patch(self, request, pk):
|
||||
from automation.tasks import update_policy_task_fields_task
|
||||
from automation.tasks import update_policy_autotasks_fields_task
|
||||
from autotasks.tasks import enable_or_disable_win_task
|
||||
|
||||
task = get_object_or_404(AutomatedTask, pk=pk)
|
||||
|
||||
if "enableordisable" in request.data:
|
||||
action = request.data["enableordisable"]
|
||||
|
||||
if not task.policy:
|
||||
enable_or_disable_win_task.delay(pk=task.pk, action=action)
|
||||
|
||||
else:
|
||||
update_policy_task_fields_task.delay(task.pk, update_agent=True)
|
||||
|
||||
task.enabled = action
|
||||
task.save(update_fields=["enabled"])
|
||||
action = "enabled" if action else "disabled"
|
||||
|
||||
if task.policy:
|
||||
update_policy_autotasks_fields_task.delay(
|
||||
task=task.pk, update_agent=True
|
||||
)
|
||||
elif task.agent:
|
||||
enable_or_disable_win_task.delay(pk=task.pk)
|
||||
|
||||
return Response(f"Task will be {action} shortly")
|
||||
|
||||
else:
|
||||
return notify_error("The request was invalid")
|
||||
|
||||
def delete(self, request, pk):
|
||||
from automation.tasks import delete_policy_autotask_task
|
||||
from automation.tasks import delete_policy_autotasks_task
|
||||
from autotasks.tasks import delete_win_task_schedule
|
||||
|
||||
task = get_object_or_404(AutomatedTask, pk=pk)
|
||||
|
||||
if not task.policy:
|
||||
if task.agent:
|
||||
delete_win_task_schedule.delay(pk=task.pk)
|
||||
|
||||
if task.policy:
|
||||
delete_policy_autotask_task.delay(task.pk)
|
||||
elif task.policy:
|
||||
delete_policy_autotasks_task.delay(task=task.pk)
|
||||
task.delete()
|
||||
|
||||
return Response(f"{task.name} will be deleted shortly")
|
||||
|
||||
|
||||
@api_view()
|
||||
@permission_classes([IsAuthenticated, RunAutoTaskPerms])
|
||||
def run_task(request, pk):
|
||||
from autotasks.tasks import run_win_task
|
||||
|
||||
task = get_object_or_404(AutomatedTask, pk=pk)
|
||||
asyncio.run(task.agent.nats_cmd({"func": "runtask", "taskpk": task.pk}, wait=False))
|
||||
run_win_task.delay(pk=pk)
|
||||
return Response(f"{task.name} will now be run on {task.agent.hostname}")
|
||||
|
||||
22
api/tacticalrmm/checks/migrations/0024_auto_20210606_1632.py
Normal file
22
api/tacticalrmm/checks/migrations/0024_auto_20210606_1632.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 3.2.1 on 2021-06-06 16:32
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('checks', '0023_check_run_interval'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='checkhistory',
|
||||
name='check_history',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='checkhistory',
|
||||
name='check_id',
|
||||
field=models.PositiveIntegerField(default=0),
|
||||
),
|
||||
]
|
||||
@@ -1,4 +1,3 @@
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import string
|
||||
@@ -6,17 +5,15 @@ from statistics import mean
|
||||
from typing import Any
|
||||
|
||||
import pytz
|
||||
from alerts.models import SEVERITY_CHOICES
|
||||
from core.models import CoreSettings
|
||||
from django.conf import settings
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from logs.models import BaseAuditModel
|
||||
from loguru import logger
|
||||
|
||||
from alerts.models import SEVERITY_CHOICES
|
||||
from core.models import CoreSettings
|
||||
from logs.models import BaseAuditModel
|
||||
|
||||
from .utils import bytes2human
|
||||
|
||||
logger.configure(**settings.LOG_CONFIG)
|
||||
|
||||
@@ -263,6 +260,42 @@ class Check(BaseAuditModel):
|
||||
"modified_time",
|
||||
]
|
||||
|
||||
@property
|
||||
def policy_fields_to_copy(self) -> list[str]:
|
||||
return [
|
||||
"warning_threshold",
|
||||
"error_threshold",
|
||||
"alert_severity",
|
||||
"name",
|
||||
"run_interval",
|
||||
"disk",
|
||||
"fails_b4_alert",
|
||||
"ip",
|
||||
"script",
|
||||
"script_args",
|
||||
"info_return_codes",
|
||||
"warning_return_codes",
|
||||
"timeout",
|
||||
"svc_name",
|
||||
"svc_display_name",
|
||||
"svc_policy_mode",
|
||||
"pass_if_start_pending",
|
||||
"pass_if_svc_not_exist",
|
||||
"restart_if_stopped",
|
||||
"log_name",
|
||||
"event_id",
|
||||
"event_id_is_wildcard",
|
||||
"event_type",
|
||||
"event_source",
|
||||
"event_message",
|
||||
"fail_when",
|
||||
"search_last_days",
|
||||
"number_of_events_b4_alert",
|
||||
"email_alert",
|
||||
"text_alert",
|
||||
"dashboard_alert",
|
||||
]
|
||||
|
||||
def should_create_alert(self, alert_template=None):
|
||||
|
||||
return (
|
||||
@@ -280,9 +313,9 @@ class Check(BaseAuditModel):
|
||||
)
|
||||
|
||||
def add_check_history(self, value: int, more_info: Any = None) -> None:
|
||||
CheckHistory.objects.create(check_history=self, y=value, results=more_info)
|
||||
CheckHistory.objects.create(check_id=self.pk, y=value, results=more_info)
|
||||
|
||||
def handle_checkv2(self, data):
|
||||
def handle_check(self, data):
|
||||
from alerts.models import Alert
|
||||
|
||||
# cpuload or mem checks
|
||||
@@ -313,9 +346,6 @@ class Check(BaseAuditModel):
|
||||
elif self.check_type == "diskspace":
|
||||
if data["exists"]:
|
||||
percent_used = round(data["percent_used"])
|
||||
total = bytes2human(data["total"])
|
||||
free = bytes2human(data["free"])
|
||||
|
||||
if self.error_threshold and (100 - percent_used) < self.error_threshold:
|
||||
self.status = "failing"
|
||||
self.alert_severity = "error"
|
||||
@@ -329,7 +359,7 @@ class Check(BaseAuditModel):
|
||||
else:
|
||||
self.status = "passing"
|
||||
|
||||
self.more_info = f"Total: {total}B, Free: {free}B"
|
||||
self.more_info = data["more_info"]
|
||||
|
||||
# add check history
|
||||
self.add_check_history(100 - percent_used)
|
||||
@@ -345,12 +375,7 @@ class Check(BaseAuditModel):
|
||||
self.stdout = data["stdout"]
|
||||
self.stderr = data["stderr"]
|
||||
self.retcode = data["retcode"]
|
||||
try:
|
||||
# python agent
|
||||
self.execution_time = "{:.4f}".format(data["stop"] - data["start"])
|
||||
except:
|
||||
# golang agent
|
||||
self.execution_time = "{:.4f}".format(data["runtime"])
|
||||
self.execution_time = "{:.4f}".format(data["runtime"])
|
||||
|
||||
if data["retcode"] in self.info_return_codes:
|
||||
self.alert_severity = "info"
|
||||
@@ -386,18 +411,8 @@ class Check(BaseAuditModel):
|
||||
|
||||
# ping checks
|
||||
elif self.check_type == "ping":
|
||||
success = ["Reply", "bytes", "time", "TTL"]
|
||||
output = data["output"]
|
||||
|
||||
if data["has_stdout"]:
|
||||
if all(x in output for x in success):
|
||||
self.status = "passing"
|
||||
else:
|
||||
self.status = "failing"
|
||||
elif data["has_stderr"]:
|
||||
self.status = "failing"
|
||||
|
||||
self.more_info = output
|
||||
self.status = data["status"]
|
||||
self.more_info = data["output"]
|
||||
self.save(update_fields=["more_info"])
|
||||
|
||||
self.add_check_history(
|
||||
@@ -406,41 +421,8 @@ class Check(BaseAuditModel):
|
||||
|
||||
# windows service checks
|
||||
elif self.check_type == "winsvc":
|
||||
svc_stat = data["status"]
|
||||
self.more_info = f"Status {svc_stat.upper()}"
|
||||
|
||||
if data["exists"]:
|
||||
if svc_stat == "running":
|
||||
self.status = "passing"
|
||||
elif svc_stat == "start_pending" and self.pass_if_start_pending:
|
||||
self.status = "passing"
|
||||
else:
|
||||
if self.agent and self.restart_if_stopped:
|
||||
nats_data = {
|
||||
"func": "winsvcaction",
|
||||
"payload": {"name": self.svc_name, "action": "start"},
|
||||
}
|
||||
r = asyncio.run(self.agent.nats_cmd(nats_data, timeout=32))
|
||||
if r == "timeout" or r == "natsdown":
|
||||
self.status = "failing"
|
||||
elif not r["success"] and r["errormsg"]:
|
||||
self.status = "failing"
|
||||
elif r["success"]:
|
||||
self.status = "passing"
|
||||
self.more_info = f"Status RUNNING"
|
||||
else:
|
||||
self.status = "failing"
|
||||
else:
|
||||
self.status = "failing"
|
||||
|
||||
else:
|
||||
if self.pass_if_svc_not_exist:
|
||||
self.status = "passing"
|
||||
else:
|
||||
self.status = "failing"
|
||||
|
||||
self.more_info = f"Service {self.svc_name} does not exist"
|
||||
|
||||
self.status = data["status"]
|
||||
self.more_info = data["more_info"]
|
||||
self.save(update_fields=["more_info"])
|
||||
|
||||
self.add_check_history(
|
||||
@@ -448,49 +430,7 @@ class Check(BaseAuditModel):
|
||||
)
|
||||
|
||||
elif self.check_type == "eventlog":
|
||||
log = []
|
||||
is_wildcard = self.event_id_is_wildcard
|
||||
eventType = self.event_type
|
||||
eventID = self.event_id
|
||||
source = self.event_source
|
||||
message = self.event_message
|
||||
r = data["log"]
|
||||
|
||||
for i in r:
|
||||
if i["eventType"] == eventType:
|
||||
if not is_wildcard and not int(i["eventID"]) == eventID:
|
||||
continue
|
||||
|
||||
if not source and not message:
|
||||
if is_wildcard:
|
||||
log.append(i)
|
||||
elif int(i["eventID"]) == eventID:
|
||||
log.append(i)
|
||||
continue
|
||||
|
||||
if source and message:
|
||||
if is_wildcard:
|
||||
if source in i["source"] and message in i["message"]:
|
||||
log.append(i)
|
||||
|
||||
elif int(i["eventID"]) == eventID:
|
||||
if source in i["source"] and message in i["message"]:
|
||||
log.append(i)
|
||||
|
||||
continue
|
||||
|
||||
if source and source in i["source"]:
|
||||
if is_wildcard:
|
||||
log.append(i)
|
||||
elif int(i["eventID"]) == eventID:
|
||||
log.append(i)
|
||||
|
||||
if message and message in i["message"]:
|
||||
if is_wildcard:
|
||||
log.append(i)
|
||||
elif int(i["eventID"]) == eventID:
|
||||
log.append(i)
|
||||
|
||||
log = data["log"]
|
||||
if self.fail_when == "contains":
|
||||
if log and len(log) >= self.number_of_events_b4_alert:
|
||||
self.status = "failing"
|
||||
@@ -527,6 +467,11 @@ class Check(BaseAuditModel):
|
||||
|
||||
return self.status
|
||||
|
||||
def handle_assigned_task(self) -> None:
|
||||
for task in self.assignedtask.all(): # type: ignore
|
||||
if task.enabled:
|
||||
task.run_win_task()
|
||||
|
||||
@staticmethod
|
||||
def serialize(check):
|
||||
# serializes the check and returns json
|
||||
@@ -551,49 +496,31 @@ class Check(BaseAuditModel):
|
||||
|
||||
def create_policy_check(self, agent=None, policy=None):
|
||||
|
||||
if not agent and not policy or agent and policy:
|
||||
if (not agent and not policy) or (agent and policy):
|
||||
return
|
||||
|
||||
Check.objects.create(
|
||||
check = Check.objects.create(
|
||||
agent=agent,
|
||||
policy=policy,
|
||||
managed_by_policy=bool(agent),
|
||||
parent_check=(self.pk if agent else None),
|
||||
name=self.name,
|
||||
alert_severity=self.alert_severity,
|
||||
check_type=self.check_type,
|
||||
email_alert=self.email_alert,
|
||||
dashboard_alert=self.dashboard_alert,
|
||||
text_alert=self.text_alert,
|
||||
fails_b4_alert=self.fails_b4_alert,
|
||||
extra_details=self.extra_details,
|
||||
run_interval=self.run_interval,
|
||||
error_threshold=self.error_threshold,
|
||||
warning_threshold=self.warning_threshold,
|
||||
disk=self.disk,
|
||||
ip=self.ip,
|
||||
script=self.script,
|
||||
script_args=self.script_args,
|
||||
timeout=self.timeout,
|
||||
info_return_codes=self.info_return_codes,
|
||||
warning_return_codes=self.warning_return_codes,
|
||||
svc_name=self.svc_name,
|
||||
svc_display_name=self.svc_display_name,
|
||||
pass_if_start_pending=self.pass_if_start_pending,
|
||||
pass_if_svc_not_exist=self.pass_if_svc_not_exist,
|
||||
restart_if_stopped=self.restart_if_stopped,
|
||||
svc_policy_mode=self.svc_policy_mode,
|
||||
log_name=self.log_name,
|
||||
event_id=self.event_id,
|
||||
event_id_is_wildcard=self.event_id_is_wildcard,
|
||||
event_type=self.event_type,
|
||||
event_source=self.event_source,
|
||||
event_message=self.event_message,
|
||||
fail_when=self.fail_when,
|
||||
search_last_days=self.search_last_days,
|
||||
number_of_events_b4_alert=self.number_of_events_b4_alert,
|
||||
)
|
||||
|
||||
for task in self.assignedtask.all(): # type: ignore
|
||||
if policy or (
|
||||
agent and not agent.autotasks.filter(parent_task=task.pk).exists()
|
||||
):
|
||||
task.create_policy_task(
|
||||
agent=agent, policy=policy, assigned_check=check
|
||||
)
|
||||
|
||||
for field in self.policy_fields_to_copy:
|
||||
setattr(check, field, getattr(self, field))
|
||||
|
||||
check.save()
|
||||
|
||||
def is_duplicate(self, check):
|
||||
if self.check_type == "diskspace":
|
||||
return self.disk == check.disk
|
||||
@@ -633,12 +560,15 @@ class Check(BaseAuditModel):
|
||||
if self.error_threshold:
|
||||
text += f" Error Threshold: {self.error_threshold}%"
|
||||
|
||||
percent_used = [
|
||||
d["percent"] for d in self.agent.disks if d["device"] == self.disk
|
||||
][0]
|
||||
percent_free = 100 - percent_used
|
||||
try:
|
||||
percent_used = [
|
||||
d["percent"] for d in self.agent.disks if d["device"] == self.disk
|
||||
][0]
|
||||
percent_free = 100 - percent_used
|
||||
|
||||
body = subject + f" - Free: {percent_free}%, {text}"
|
||||
body = subject + f" - Free: {percent_free}%, {text}"
|
||||
except:
|
||||
body = subject + f" - Disk {self.disk} does not exist"
|
||||
|
||||
elif self.check_type == "script":
|
||||
|
||||
@@ -710,11 +640,15 @@ class Check(BaseAuditModel):
|
||||
if self.error_threshold:
|
||||
text += f" Error Threshold: {self.error_threshold}%"
|
||||
|
||||
percent_used = [
|
||||
d["percent"] for d in self.agent.disks if d["device"] == self.disk
|
||||
][0]
|
||||
percent_free = 100 - percent_used
|
||||
body = subject + f" - Free: {percent_free}%, {text}"
|
||||
try:
|
||||
percent_used = [
|
||||
d["percent"] for d in self.agent.disks if d["device"] == self.disk
|
||||
][0]
|
||||
percent_free = 100 - percent_used
|
||||
body = subject + f" - Free: {percent_free}%, {text}"
|
||||
except:
|
||||
body = subject + f" - Disk {self.disk} does not exist"
|
||||
|
||||
elif self.check_type == "script":
|
||||
body = subject + f" - Return code: {self.retcode}"
|
||||
elif self.check_type == "ping":
|
||||
@@ -754,14 +688,10 @@ class Check(BaseAuditModel):
|
||||
|
||||
|
||||
class CheckHistory(models.Model):
|
||||
check_history = models.ForeignKey(
|
||||
Check,
|
||||
related_name="check_history",
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
check_id = models.PositiveIntegerField(default=0)
|
||||
x = models.DateTimeField(auto_now_add=True)
|
||||
y = models.PositiveIntegerField(null=True, blank=True, default=None)
|
||||
results = models.JSONField(null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.check_history.readable_desc
|
||||
return self.x
|
||||
|
||||
16
api/tacticalrmm/checks/permissions.py
Normal file
16
api/tacticalrmm/checks/permissions.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
from tacticalrmm.permissions import _has_perm
|
||||
|
||||
|
||||
class ManageChecksPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
if r.method == "GET":
|
||||
return True
|
||||
|
||||
return _has_perm(r, "can_manage_checks")
|
||||
|
||||
|
||||
class RunChecksPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_run_checks")
|
||||
@@ -158,14 +158,8 @@ class AssignedTaskCheckRunnerField(serializers.ModelSerializer):
|
||||
|
||||
class CheckRunnerGetSerializer(serializers.ModelSerializer):
|
||||
# only send data needed for agent to run a check
|
||||
assigned_tasks = serializers.SerializerMethodField()
|
||||
script = ScriptCheckSerializer(read_only=True)
|
||||
|
||||
def get_assigned_tasks(self, obj):
|
||||
if obj.assignedtask.exists():
|
||||
tasks = obj.assignedtask.all()
|
||||
return AssignedTaskCheckRunnerField(tasks, many=True).data
|
||||
|
||||
class Meta:
|
||||
model = Check
|
||||
exclude = [
|
||||
@@ -193,6 +187,7 @@ class CheckRunnerGetSerializer(serializers.ModelSerializer):
|
||||
"modified_by",
|
||||
"modified_time",
|
||||
"history",
|
||||
"dashboard_alert",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -14,6 +14,22 @@ class TestCheckViews(TacticalTestCase):
|
||||
self.authenticate()
|
||||
self.setup_coresettings()
|
||||
|
||||
def test_delete_agent_check(self):
|
||||
# setup data
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
check = baker.make_recipe("checks.diskspace_check", agent=agent)
|
||||
|
||||
resp = self.client.delete("/checks/500/check/", format="json")
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
url = f"/checks/{check.pk}/check/"
|
||||
|
||||
resp = self.client.delete(url, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertFalse(agent.agentchecks.all())
|
||||
|
||||
self.check_not_authenticated("delete", url)
|
||||
|
||||
def test_get_disk_check(self):
|
||||
# setup data
|
||||
disk_check = baker.make_recipe("checks.diskspace_check")
|
||||
@@ -347,10 +363,10 @@ class TestCheckViews(TacticalTestCase):
|
||||
# setup data
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
check = baker.make_recipe("checks.diskspace_check", agent=agent)
|
||||
baker.make("checks.CheckHistory", check_history=check, _quantity=30)
|
||||
baker.make("checks.CheckHistory", check_id=check.id, _quantity=30)
|
||||
check_history_data = baker.make(
|
||||
"checks.CheckHistory",
|
||||
check_history=check,
|
||||
check_id=check.id,
|
||||
_quantity=30,
|
||||
)
|
||||
|
||||
@@ -384,17 +400,17 @@ class TestCheckTasks(TacticalTestCase):
|
||||
def setUp(self):
|
||||
self.authenticate()
|
||||
self.setup_coresettings()
|
||||
self.agent = baker.make_recipe("agents.agent")
|
||||
self.agent = baker.make_recipe("agents.agent", version="1.5.7")
|
||||
|
||||
def test_prune_check_history(self):
|
||||
from .tasks import prune_check_history
|
||||
|
||||
# setup data
|
||||
check = baker.make_recipe("checks.diskspace_check")
|
||||
baker.make("checks.CheckHistory", check_history=check, _quantity=30)
|
||||
baker.make("checks.CheckHistory", check_id=check.id, _quantity=30)
|
||||
check_history_data = baker.make(
|
||||
"checks.CheckHistory",
|
||||
check_history=check,
|
||||
check_id=check.id,
|
||||
_quantity=30,
|
||||
)
|
||||
|
||||
@@ -510,6 +526,7 @@ class TestCheckTasks(TacticalTestCase):
|
||||
"percent_used": 85,
|
||||
"total": 500,
|
||||
"free": 400,
|
||||
"more_info": "More info",
|
||||
}
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
@@ -527,6 +544,7 @@ class TestCheckTasks(TacticalTestCase):
|
||||
"percent_used": 95,
|
||||
"total": 500,
|
||||
"free": 400,
|
||||
"more_info": "More info",
|
||||
}
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
@@ -557,6 +575,7 @@ class TestCheckTasks(TacticalTestCase):
|
||||
"percent_used": 95,
|
||||
"total": 500,
|
||||
"free": 400,
|
||||
"more_info": "More info",
|
||||
}
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
@@ -576,6 +595,7 @@ class TestCheckTasks(TacticalTestCase):
|
||||
"percent_used": 95,
|
||||
"total": 500,
|
||||
"free": 400,
|
||||
"more_info": "More info",
|
||||
}
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
@@ -592,6 +612,7 @@ class TestCheckTasks(TacticalTestCase):
|
||||
"percent_used": 50,
|
||||
"total": 500,
|
||||
"free": 400,
|
||||
"more_info": "More info",
|
||||
}
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
@@ -775,12 +796,7 @@ class TestCheckTasks(TacticalTestCase):
|
||||
)
|
||||
|
||||
# test failing info
|
||||
data = {
|
||||
"id": ping.id,
|
||||
"output": "Reply from 192.168.1.27: Destination host unreachable",
|
||||
"has_stdout": True,
|
||||
"has_stderr": False,
|
||||
}
|
||||
data = {"id": ping.id, "status": "failing", "output": "reply from a.com"}
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
@@ -790,13 +806,6 @@ class TestCheckTasks(TacticalTestCase):
|
||||
self.assertEqual(new_check.alert_severity, "info")
|
||||
|
||||
# test failing warning
|
||||
data = {
|
||||
"id": ping.id,
|
||||
"output": "Reply from 192.168.1.27: Destination host unreachable",
|
||||
"has_stdout": True,
|
||||
"has_stderr": False,
|
||||
}
|
||||
|
||||
ping.alert_severity = "warning"
|
||||
ping.save()
|
||||
|
||||
@@ -808,13 +817,6 @@ class TestCheckTasks(TacticalTestCase):
|
||||
self.assertEqual(new_check.alert_severity, "warning")
|
||||
|
||||
# test failing error
|
||||
data = {
|
||||
"id": ping.id,
|
||||
"output": "Reply from 192.168.1.27: Destination host unreachable",
|
||||
"has_stdout": True,
|
||||
"has_stderr": False,
|
||||
}
|
||||
|
||||
ping.alert_severity = "error"
|
||||
ping.save()
|
||||
|
||||
@@ -826,13 +828,6 @@ class TestCheckTasks(TacticalTestCase):
|
||||
self.assertEqual(new_check.alert_severity, "error")
|
||||
|
||||
# test failing error
|
||||
data = {
|
||||
"id": ping.id,
|
||||
"output": "some output",
|
||||
"has_stdout": False,
|
||||
"has_stderr": True,
|
||||
}
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
@@ -841,12 +836,7 @@ class TestCheckTasks(TacticalTestCase):
|
||||
self.assertEqual(new_check.alert_severity, "error")
|
||||
|
||||
# test passing
|
||||
data = {
|
||||
"id": ping.id,
|
||||
"output": "Reply from 192.168.1.1: bytes=32 time<1ms TTL=64",
|
||||
"has_stdout": True,
|
||||
"has_stderr": False,
|
||||
}
|
||||
data = {"id": ping.id, "status": "passing", "output": "reply from a.com"}
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
@@ -865,7 +855,7 @@ class TestCheckTasks(TacticalTestCase):
|
||||
)
|
||||
|
||||
# test passing running
|
||||
data = {"id": winsvc.id, "exists": True, "status": "running"}
|
||||
data = {"id": winsvc.id, "status": "passing", "more_info": "ok"}
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
@@ -873,20 +863,8 @@ class TestCheckTasks(TacticalTestCase):
|
||||
new_check = Check.objects.get(pk=winsvc.id)
|
||||
self.assertEqual(new_check.status, "passing")
|
||||
|
||||
# test passing start pending
|
||||
winsvc.pass_if_start_pending = True
|
||||
winsvc.save()
|
||||
|
||||
data = {"id": winsvc.id, "exists": True, "status": "start_pending"}
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
new_check = Check.objects.get(pk=winsvc.id)
|
||||
self.assertEqual(new_check.status, "passing")
|
||||
|
||||
# test failing no start
|
||||
data = {"id": winsvc.id, "exists": True, "status": "not running"}
|
||||
# test failing
|
||||
data = {"id": winsvc.id, "status": "failing", "more_info": "ok"}
|
||||
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
@@ -895,7 +873,7 @@ class TestCheckTasks(TacticalTestCase):
|
||||
self.assertEqual(new_check.status, "failing")
|
||||
self.assertEqual(new_check.alert_severity, "info")
|
||||
|
||||
# test failing and attempt start
|
||||
""" # test failing and attempt start
|
||||
winsvc.restart_if_stopped = True
|
||||
winsvc.alert_severity = "warning"
|
||||
winsvc.save()
|
||||
@@ -960,9 +938,9 @@ class TestCheckTasks(TacticalTestCase):
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
new_check = Check.objects.get(pk=winsvc.id)
|
||||
self.assertEqual(new_check.status, "passing")
|
||||
self.assertEqual(new_check.status, "passing") """
|
||||
|
||||
def test_handle_eventlog_check(self):
|
||||
""" def test_handle_eventlog_check(self):
|
||||
from checks.models import Check
|
||||
|
||||
url = "/api/v3/checkrunner/"
|
||||
@@ -1164,4 +1142,4 @@ class TestCheckTasks(TacticalTestCase):
|
||||
|
||||
new_check = Check.objects.get(pk=eventlog.id)
|
||||
|
||||
self.assertEquals(new_check.status, "passing")
|
||||
self.assertEquals(new_check.status, "passing") """
|
||||
|
||||
@@ -8,5 +8,5 @@ urlpatterns = [
|
||||
path("<pk>/loadchecks/", views.load_checks),
|
||||
path("getalldisks/", views.get_disks_for_policies),
|
||||
path("runchecks/<pk>/", views.run_checks),
|
||||
path("history/<int:checkpk>/", views.CheckHistory.as_view()),
|
||||
path("history/<int:checkpk>/", views.GetCheckHistory.as_view()),
|
||||
]
|
||||
|
||||
@@ -5,26 +5,27 @@ from django.db.models import Q
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils import timezone as djangotime
|
||||
from packaging import version as pyver
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from agents.models import Agent
|
||||
from automation.models import Policy
|
||||
from automation.tasks import (
|
||||
delete_policy_check_task,
|
||||
generate_agent_checks_from_policies_task,
|
||||
update_policy_check_fields_task,
|
||||
)
|
||||
from scripts.models import Script
|
||||
from tacticalrmm.utils import notify_error
|
||||
|
||||
from .models import Check
|
||||
from .models import Check, CheckHistory
|
||||
from .permissions import ManageChecksPerms, RunChecksPerms
|
||||
from .serializers import CheckHistorySerializer, CheckSerializer
|
||||
|
||||
|
||||
class AddCheck(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageChecksPerms]
|
||||
|
||||
def post(self, request):
|
||||
from automation.tasks import generate_agent_checks_task
|
||||
|
||||
policy = None
|
||||
agent = None
|
||||
|
||||
@@ -53,40 +54,49 @@ class AddCheck(APIView):
|
||||
data=request.data["check"], partial=True, context=parent
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
obj = serializer.save(**parent, script=script)
|
||||
new_check = serializer.save(**parent, script=script)
|
||||
|
||||
# Generate policy Checks
|
||||
if policy:
|
||||
generate_agent_checks_from_policies_task.delay(policypk=policy.pk)
|
||||
generate_agent_checks_task.delay(policy=policy.pk)
|
||||
elif agent:
|
||||
checks = agent.agentchecks.filter( # type: ignore
|
||||
check_type=obj.check_type, managed_by_policy=True
|
||||
check_type=new_check.check_type, managed_by_policy=True
|
||||
)
|
||||
|
||||
# Should only be one
|
||||
duplicate_check = [check for check in checks if check.is_duplicate(obj)]
|
||||
duplicate_check = [
|
||||
check for check in checks if check.is_duplicate(new_check)
|
||||
]
|
||||
|
||||
if duplicate_check:
|
||||
policy = Check.objects.get(pk=duplicate_check[0].parent_check).policy
|
||||
if policy.enforced:
|
||||
obj.overriden_by_policy = True
|
||||
obj.save()
|
||||
new_check.overriden_by_policy = True
|
||||
new_check.save()
|
||||
else:
|
||||
duplicate_check[0].delete()
|
||||
|
||||
return Response(f"{obj.readable_desc} was added!")
|
||||
return Response(f"{new_check.readable_desc} was added!")
|
||||
|
||||
|
||||
class GetUpdateDeleteCheck(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageChecksPerms]
|
||||
|
||||
def get(self, request, pk):
|
||||
check = get_object_or_404(Check, pk=pk)
|
||||
return Response(CheckSerializer(check).data)
|
||||
|
||||
def patch(self, request, pk):
|
||||
from automation.tasks import update_policy_check_fields_task
|
||||
|
||||
check = get_object_or_404(Check, pk=pk)
|
||||
|
||||
# remove fields that should not be changed when editing a check from the frontend
|
||||
if "check_alert" not in request.data.keys():
|
||||
if (
|
||||
"check_alert" not in request.data.keys()
|
||||
and "check_reset" not in request.data.keys()
|
||||
):
|
||||
[request.data.pop(i) for i in check.non_editable_fields]
|
||||
|
||||
# set event id to 0 if wildcard because it needs to be an integer field for db
|
||||
@@ -102,31 +112,32 @@ class GetUpdateDeleteCheck(APIView):
|
||||
|
||||
serializer = CheckSerializer(instance=check, data=request.data, partial=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
obj = serializer.save()
|
||||
check = serializer.save()
|
||||
|
||||
# resolve any alerts that are open
|
||||
if "check_reset" in request.data.keys():
|
||||
if check.alert.filter(resolved=False).exists():
|
||||
check.alert.get(resolved=False).resolve()
|
||||
|
||||
# Update policy check fields
|
||||
if check.policy:
|
||||
update_policy_check_fields_task(checkpk=pk)
|
||||
update_policy_check_fields_task.delay(check=check.pk)
|
||||
|
||||
return Response(f"{obj.readable_desc} was edited!")
|
||||
return Response(f"{check.readable_desc} was edited!")
|
||||
|
||||
def delete(self, request, pk):
|
||||
check = get_object_or_404(Check, pk=pk)
|
||||
from automation.tasks import generate_agent_checks_task
|
||||
|
||||
check_pk = check.pk
|
||||
policy_pk = None
|
||||
if check.policy:
|
||||
policy_pk = check.policy.pk
|
||||
check = get_object_or_404(Check, pk=pk)
|
||||
|
||||
check.delete()
|
||||
|
||||
# Policy check deleted
|
||||
if check.policy:
|
||||
delete_policy_check_task.delay(checkpk=check_pk)
|
||||
Check.objects.filter(managed_by_policy=True, parent_check=pk).delete()
|
||||
|
||||
# Re-evaluate agent checks is policy was enforced
|
||||
if check.policy.enforced:
|
||||
generate_agent_checks_from_policies_task.delay(policypk=policy_pk)
|
||||
generate_agent_checks_task.delay(policy=check.policy)
|
||||
|
||||
# Agent check deleted
|
||||
elif check.agent:
|
||||
@@ -135,7 +146,7 @@ class GetUpdateDeleteCheck(APIView):
|
||||
return Response(f"{check.readable_desc} was deleted!")
|
||||
|
||||
|
||||
class CheckHistory(APIView):
|
||||
class GetCheckHistory(APIView):
|
||||
def patch(self, request, checkpk):
|
||||
check = get_object_or_404(Check, pk=checkpk)
|
||||
|
||||
@@ -149,7 +160,7 @@ class CheckHistory(APIView):
|
||||
- djangotime.timedelta(days=request.data["timeFilter"]),
|
||||
)
|
||||
|
||||
check_history = check.check_history.filter(timeFilter).order_by("-x") # type: ignore
|
||||
check_history = CheckHistory.objects.filter(check_id=checkpk).filter(timeFilter).order_by("-x") # type: ignore
|
||||
|
||||
return Response(
|
||||
CheckHistorySerializer(
|
||||
@@ -159,6 +170,7 @@ class CheckHistory(APIView):
|
||||
|
||||
|
||||
@api_view()
|
||||
@permission_classes([IsAuthenticated, RunChecksPerms])
|
||||
def run_checks(request, pk):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.1.7 on 2021-04-17 01:25
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('clients', '0016_auto_20210329_1827'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='client',
|
||||
name='block_policy_inheritance',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='site',
|
||||
name='block_policy_inheritance',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -9,6 +9,7 @@ from logs.models import BaseAuditModel
|
||||
|
||||
class Client(BaseAuditModel):
|
||||
name = models.CharField(max_length=255, unique=True)
|
||||
block_policy_inheritance = models.BooleanField(default=False)
|
||||
workstation_policy = models.ForeignKey(
|
||||
"automation.Policy",
|
||||
related_name="workstation_clients",
|
||||
@@ -34,30 +35,29 @@ class Client(BaseAuditModel):
|
||||
|
||||
def save(self, *args, **kw):
|
||||
from alerts.tasks import cache_agents_alert_template
|
||||
from automation.tasks import generate_agent_checks_by_location_task
|
||||
from automation.tasks import generate_agent_checks_task
|
||||
|
||||
# get old client if exists
|
||||
old_client = type(self).objects.get(pk=self.pk) if self.pk else None
|
||||
super(BaseAuditModel, self).save(*args, **kw)
|
||||
|
||||
# check if server polcies have changed and initiate task to reapply policies if so
|
||||
if old_client and old_client.server_policy != self.server_policy:
|
||||
generate_agent_checks_by_location_task.delay(
|
||||
location={"site__client_id": self.pk},
|
||||
mon_type="server",
|
||||
create_tasks=True,
|
||||
)
|
||||
# check if polcies have changed and initiate task to reapply policies if so
|
||||
if old_client:
|
||||
if (
|
||||
(old_client.server_policy != self.server_policy)
|
||||
or (old_client.workstation_policy != self.workstation_policy)
|
||||
or (
|
||||
old_client.block_policy_inheritance != self.block_policy_inheritance
|
||||
)
|
||||
):
|
||||
|
||||
# check if workstation polcies have changed and initiate task to reapply policies if so
|
||||
if old_client and old_client.workstation_policy != self.workstation_policy:
|
||||
generate_agent_checks_by_location_task.delay(
|
||||
location={"site__client_id": self.pk},
|
||||
mon_type="workstation",
|
||||
create_tasks=True,
|
||||
)
|
||||
generate_agent_checks_task.delay(
|
||||
client=self.pk,
|
||||
create_tasks=True,
|
||||
)
|
||||
|
||||
if old_client and old_client.alert_template != self.alert_template:
|
||||
cache_agents_alert_template.delay()
|
||||
if old_client.alert_template != self.alert_template:
|
||||
cache_agents_alert_template.delay()
|
||||
|
||||
class Meta:
|
||||
ordering = ("name",)
|
||||
@@ -65,6 +65,10 @@ class Client(BaseAuditModel):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def agent_count(self) -> int:
|
||||
return Agent.objects.filter(site__client=self).count()
|
||||
|
||||
@property
|
||||
def has_maintenanace_mode_agents(self):
|
||||
return (
|
||||
@@ -86,16 +90,24 @@ class Client(BaseAuditModel):
|
||||
.prefetch_related("agentchecks")
|
||||
)
|
||||
|
||||
failing = 0
|
||||
data = {"error": False, "warning": False}
|
||||
|
||||
for agent in agents:
|
||||
if agent.checks["has_failing_checks"]:
|
||||
failing += 1
|
||||
|
||||
if agent.checks["warning"]:
|
||||
data["warning"] = True
|
||||
|
||||
if agent.checks["failing"]:
|
||||
data["error"] = True
|
||||
break
|
||||
|
||||
if agent.overdue_email_alert or agent.overdue_text_alert:
|
||||
if agent.status == "overdue":
|
||||
failing += 1
|
||||
data["error"] = True
|
||||
break
|
||||
|
||||
return failing > 0
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def serialize(client):
|
||||
@@ -108,6 +120,7 @@ class Client(BaseAuditModel):
|
||||
class Site(BaseAuditModel):
|
||||
client = models.ForeignKey(Client, related_name="sites", on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=255)
|
||||
block_policy_inheritance = models.BooleanField(default=False)
|
||||
workstation_policy = models.ForeignKey(
|
||||
"automation.Policy",
|
||||
related_name="workstation_sites",
|
||||
@@ -133,30 +146,24 @@ class Site(BaseAuditModel):
|
||||
|
||||
def save(self, *args, **kw):
|
||||
from alerts.tasks import cache_agents_alert_template
|
||||
from automation.tasks import generate_agent_checks_by_location_task
|
||||
from automation.tasks import generate_agent_checks_task
|
||||
|
||||
# get old client if exists
|
||||
old_site = type(self).objects.get(pk=self.pk) if self.pk else None
|
||||
super(Site, self).save(*args, **kw)
|
||||
|
||||
# check if server polcies have changed and initiate task to reapply policies if so
|
||||
if old_site and old_site.server_policy != self.server_policy:
|
||||
generate_agent_checks_by_location_task.delay(
|
||||
location={"site_id": self.pk},
|
||||
mon_type="server",
|
||||
create_tasks=True,
|
||||
)
|
||||
# check if polcies have changed and initiate task to reapply policies if so
|
||||
if old_site:
|
||||
if (
|
||||
(old_site.server_policy != self.server_policy)
|
||||
or (old_site.workstation_policy != self.workstation_policy)
|
||||
or (old_site.block_policy_inheritance != self.block_policy_inheritance)
|
||||
):
|
||||
|
||||
# check if workstation polcies have changed and initiate task to reapply policies if so
|
||||
if old_site and old_site.workstation_policy != self.workstation_policy:
|
||||
generate_agent_checks_by_location_task.delay(
|
||||
location={"site_id": self.pk},
|
||||
mon_type="workstation",
|
||||
create_tasks=True,
|
||||
)
|
||||
generate_agent_checks_task.delay(site=self.pk, create_tasks=True)
|
||||
|
||||
if old_site and old_site.alert_template != self.alert_template:
|
||||
cache_agents_alert_template.delay()
|
||||
if old_site.alert_template != self.alert_template:
|
||||
cache_agents_alert_template.delay()
|
||||
|
||||
class Meta:
|
||||
ordering = ("name",)
|
||||
@@ -165,6 +172,10 @@ class Site(BaseAuditModel):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def agent_count(self) -> int:
|
||||
return Agent.objects.filter(site=self).count()
|
||||
|
||||
@property
|
||||
def has_maintenanace_mode_agents(self):
|
||||
return Agent.objects.filter(site=self, maintenance_mode=True).count() > 0
|
||||
@@ -184,16 +195,24 @@ class Site(BaseAuditModel):
|
||||
.prefetch_related("agentchecks")
|
||||
)
|
||||
|
||||
failing = 0
|
||||
data = {"error": False, "warning": False}
|
||||
|
||||
for agent in agents:
|
||||
|
||||
if agent.checks["has_failing_checks"]:
|
||||
failing += 1
|
||||
if agent.checks["warning"]:
|
||||
data["warning"] = True
|
||||
|
||||
if agent.checks["failing"]:
|
||||
data["error"] = True
|
||||
break
|
||||
|
||||
if agent.overdue_email_alert or agent.overdue_text_alert:
|
||||
if agent.status == "overdue":
|
||||
failing += 1
|
||||
data["error"] = True
|
||||
break
|
||||
|
||||
return failing > 0
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def serialize(site):
|
||||
|
||||
27
api/tacticalrmm/clients/permissions.py
Normal file
27
api/tacticalrmm/clients/permissions.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
from tacticalrmm.permissions import _has_perm
|
||||
|
||||
|
||||
class ManageClientsPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
if r.method == "GET":
|
||||
return True
|
||||
|
||||
return _has_perm(r, "can_manage_clients")
|
||||
|
||||
|
||||
class ManageSitesPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
if r.method == "GET":
|
||||
return True
|
||||
|
||||
return _has_perm(r, "can_manage_sites")
|
||||
|
||||
|
||||
class ManageDeploymentPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
if r.method == "GET":
|
||||
return True
|
||||
|
||||
return _has_perm(r, "can_manage_deployments")
|
||||
@@ -25,6 +25,7 @@ class SiteCustomFieldSerializer(ModelSerializer):
|
||||
class SiteSerializer(ModelSerializer):
|
||||
client_name = ReadOnlyField(source="client.name")
|
||||
custom_fields = SiteCustomFieldSerializer(many=True, read_only=True)
|
||||
agent_count = ReadOnlyField()
|
||||
|
||||
class Meta:
|
||||
model = Site
|
||||
@@ -37,6 +38,8 @@ class SiteSerializer(ModelSerializer):
|
||||
"client_name",
|
||||
"client",
|
||||
"custom_fields",
|
||||
"agent_count",
|
||||
"block_policy_inheritance",
|
||||
)
|
||||
|
||||
def validate(self, val):
|
||||
@@ -68,6 +71,7 @@ class ClientCustomFieldSerializer(ModelSerializer):
|
||||
class ClientSerializer(ModelSerializer):
|
||||
sites = SiteSerializer(many=True, read_only=True)
|
||||
custom_fields = ClientCustomFieldSerializer(many=True, read_only=True)
|
||||
agent_count = ReadOnlyField()
|
||||
|
||||
class Meta:
|
||||
model = Client
|
||||
@@ -77,8 +81,10 @@ class ClientSerializer(ModelSerializer):
|
||||
"server_policy",
|
||||
"workstation_policy",
|
||||
"alert_template",
|
||||
"block_policy_inheritance",
|
||||
"sites",
|
||||
"custom_fields",
|
||||
"agent_count",
|
||||
)
|
||||
|
||||
def validate(self, val):
|
||||
@@ -95,7 +101,6 @@ class SiteTreeSerializer(ModelSerializer):
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = "__all__"
|
||||
ordering = ("failing_checks",)
|
||||
|
||||
|
||||
class ClientTreeSerializer(ModelSerializer):
|
||||
@@ -106,7 +111,6 @@ class ClientTreeSerializer(ModelSerializer):
|
||||
class Meta:
|
||||
model = Client
|
||||
fields = "__all__"
|
||||
ordering = ("failing_checks",)
|
||||
|
||||
|
||||
class DeploymentSerializer(ModelSerializer):
|
||||
|
||||
@@ -179,13 +179,9 @@ class TestClientViews(TacticalTestCase):
|
||||
|
||||
self.check_not_authenticated("put", url)
|
||||
|
||||
@patch("automation.tasks.generate_all_agent_checks_task.delay")
|
||||
@patch("automation.tasks.generate_all_agent_checks_task.delay")
|
||||
def test_delete_client(self, task1, task2):
|
||||
def test_delete_client(self):
|
||||
from agents.models import Agent
|
||||
|
||||
task1.return_value = "ok"
|
||||
task2.return_value = "ok"
|
||||
# setup data
|
||||
client_to_delete = baker.make("clients.Client")
|
||||
client_to_move = baker.make("clients.Client")
|
||||
@@ -352,13 +348,9 @@ class TestClientViews(TacticalTestCase):
|
||||
|
||||
self.check_not_authenticated("put", url)
|
||||
|
||||
@patch("automation.tasks.generate_all_agent_checks_task.delay")
|
||||
@patch("automation.tasks.generate_all_agent_checks_task.delay")
|
||||
def test_delete_site(self, task1, task2):
|
||||
def test_delete_site(self):
|
||||
from agents.models import Agent
|
||||
|
||||
task1.return_value = "ok"
|
||||
task2.return_value = "ok"
|
||||
# setup data
|
||||
client = baker.make("clients.Client")
|
||||
site_to_delete = baker.make("clients.Site", client=client)
|
||||
|
||||
@@ -7,7 +7,7 @@ from django.conf import settings
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils import timezone as djangotime
|
||||
from loguru import logger
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
@@ -16,6 +16,7 @@ from core.models import CoreSettings
|
||||
from tacticalrmm.utils import notify_error
|
||||
|
||||
from .models import Client, ClientCustomField, Deployment, Site, SiteCustomField
|
||||
from .permissions import ManageClientsPerms, ManageDeploymentPerms, ManageSitesPerms
|
||||
from .serializers import (
|
||||
ClientCustomFieldSerializer,
|
||||
ClientSerializer,
|
||||
@@ -29,6 +30,8 @@ logger.configure(**settings.LOG_CONFIG)
|
||||
|
||||
|
||||
class GetAddClients(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageClientsPerms]
|
||||
|
||||
def get(self, request):
|
||||
clients = Client.objects.all()
|
||||
return Response(ClientSerializer(clients, many=True).data)
|
||||
@@ -72,6 +75,8 @@ class GetAddClients(APIView):
|
||||
|
||||
|
||||
class GetUpdateClient(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageClientsPerms]
|
||||
|
||||
def get(self, request, pk):
|
||||
client = get_object_or_404(Client, pk=pk)
|
||||
return Response(ClientSerializer(client).data)
|
||||
@@ -110,8 +115,10 @@ class GetUpdateClient(APIView):
|
||||
|
||||
|
||||
class DeleteClient(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageClientsPerms]
|
||||
|
||||
def delete(self, request, pk, sitepk):
|
||||
from automation.tasks import generate_all_agent_checks_task
|
||||
from automation.tasks import generate_agent_checks_task
|
||||
|
||||
client = get_object_or_404(Client, pk=pk)
|
||||
agents = Agent.objects.filter(site__client=client)
|
||||
@@ -124,8 +131,7 @@ class DeleteClient(APIView):
|
||||
site = get_object_or_404(Site, pk=sitepk)
|
||||
agents.update(site=site)
|
||||
|
||||
generate_all_agent_checks_task.delay("workstation", create_tasks=True)
|
||||
generate_all_agent_checks_task.delay("server", create_tasks=True)
|
||||
generate_agent_checks_task.delay(all=True, create_tasks=True)
|
||||
|
||||
client.delete()
|
||||
return Response(f"{client.name} was deleted!")
|
||||
@@ -138,6 +144,8 @@ class GetClientTree(APIView):
|
||||
|
||||
|
||||
class GetAddSites(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageSitesPerms]
|
||||
|
||||
def get(self, request):
|
||||
sites = Site.objects.all()
|
||||
return Response(SiteSerializer(sites, many=True).data)
|
||||
@@ -163,6 +171,8 @@ class GetAddSites(APIView):
|
||||
|
||||
|
||||
class GetUpdateSite(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageSitesPerms]
|
||||
|
||||
def get(self, request, pk):
|
||||
site = get_object_or_404(Site, pk=pk)
|
||||
return Response(SiteSerializer(site).data)
|
||||
@@ -206,8 +216,10 @@ class GetUpdateSite(APIView):
|
||||
|
||||
|
||||
class DeleteSite(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageSitesPerms]
|
||||
|
||||
def delete(self, request, pk, sitepk):
|
||||
from automation.tasks import generate_all_agent_checks_task
|
||||
from automation.tasks import generate_agent_checks_task
|
||||
|
||||
site = get_object_or_404(Site, pk=pk)
|
||||
if site.client.sites.count() == 1:
|
||||
@@ -224,14 +236,15 @@ class DeleteSite(APIView):
|
||||
|
||||
agents.update(site=agent_site)
|
||||
|
||||
generate_all_agent_checks_task.delay("workstation", create_tasks=True)
|
||||
generate_all_agent_checks_task.delay("server", create_tasks=True)
|
||||
generate_agent_checks_task.delay(all=True, create_tasks=True)
|
||||
|
||||
site.delete()
|
||||
return Response(f"{site.name} was deleted!")
|
||||
|
||||
|
||||
class AgentDeployment(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageDeploymentPerms]
|
||||
|
||||
def get(self, request):
|
||||
deps = Deployment.objects.all()
|
||||
return Response(DeploymentSerializer(deps, many=True).data)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import CoreSettings, CustomField
|
||||
from .models import CodeSignToken, CoreSettings, CustomField
|
||||
|
||||
admin.site.register(CoreSettings)
|
||||
admin.site.register(CustomField)
|
||||
admin.site.register(CodeSignToken)
|
||||
|
||||
@@ -9,6 +9,7 @@ $rdp = rdpchange
|
||||
$ping = pingchange
|
||||
$auth = '"tokenchange"'
|
||||
$downloadlink = 'downloadchange'
|
||||
$apilink = $downloadlink.split('/')
|
||||
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
|
||||
@@ -47,24 +48,35 @@ If (Get-Service $serviceName -ErrorAction SilentlyContinue) {
|
||||
# pass
|
||||
}
|
||||
|
||||
Try
|
||||
{
|
||||
Invoke-WebRequest -Uri $downloadlink -OutFile $OutPath\$output
|
||||
Start-Process -FilePath $OutPath\$output -ArgumentList ('/VERYSILENT /SUPPRESSMSGBOXES') -Wait
|
||||
write-host ('Extracting...')
|
||||
Start-Sleep -s 5
|
||||
Start-Process -FilePath "C:\Program Files\TacticalAgent\tacticalrmm.exe" -ArgumentList $installArgs -Wait
|
||||
exit 0
|
||||
}
|
||||
Catch
|
||||
{
|
||||
$ErrorMessage = $_.Exception.Message
|
||||
$FailedItem = $_.Exception.ItemName
|
||||
Write-Error -Message "$ErrorMessage $FailedItem"
|
||||
exit 1
|
||||
}
|
||||
Finally
|
||||
{
|
||||
Remove-Item -Path $OutPath\$output
|
||||
$X = 0
|
||||
do {
|
||||
Write-Output "Waiting for network"
|
||||
Start-Sleep -s 5
|
||||
$X += 1
|
||||
} until(($connectreult = Test-NetConnection $apilink[2] -Port 443 | ? { $_.TcpTestSucceeded }) -or $X -eq 3)
|
||||
|
||||
if ($connectreult.TcpTestSucceeded -eq $true){
|
||||
Try
|
||||
{
|
||||
Invoke-WebRequest -Uri $downloadlink -OutFile $OutPath\$output
|
||||
Start-Process -FilePath $OutPath\$output -ArgumentList ('/VERYSILENT /SUPPRESSMSGBOXES') -Wait
|
||||
write-host ('Extracting...')
|
||||
Start-Sleep -s 5
|
||||
Start-Process -FilePath "C:\Program Files\TacticalAgent\tacticalrmm.exe" -ArgumentList $installArgs -Wait
|
||||
exit 0
|
||||
}
|
||||
Catch
|
||||
{
|
||||
$ErrorMessage = $_.Exception.Message
|
||||
$FailedItem = $_.Exception.ItemName
|
||||
Write-Error -Message "$ErrorMessage $FailedItem"
|
||||
exit 1
|
||||
}
|
||||
Finally
|
||||
{
|
||||
Remove-Item -Path $OutPath\$output
|
||||
}
|
||||
} else {
|
||||
Write-Output "Unable to connect to server"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from agents.models import Agent
|
||||
from logs.models import PendingAction
|
||||
from scripts.models import Script
|
||||
|
||||
|
||||
@@ -13,21 +8,8 @@ class Command(BaseCommand):
|
||||
help = "Collection of tasks to run after updating the rmm, after migrations"
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
# 10-16-2020 changed the type of the agent's 'disks' model field
|
||||
# from a dict of dicts, to a list of disks in the golang agent
|
||||
# the following will convert dicts to lists for agent's still on the python agent
|
||||
agents = Agent.objects.only("pk", "disks")
|
||||
for agent in agents:
|
||||
if agent.disks is not None and isinstance(agent.disks, dict):
|
||||
new = []
|
||||
for k, v in agent.disks.items():
|
||||
new.append(v)
|
||||
|
||||
agent.disks = new
|
||||
agent.save(update_fields=["disks"])
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f"Migrated disks on {agent.hostname}")
|
||||
)
|
||||
# remove task pending actions. deprecated 4/20/2021
|
||||
PendingAction.objects.filter(action_type="taskaction").delete()
|
||||
|
||||
# load community scripts into the db
|
||||
Script.load_community_scripts()
|
||||
|
||||
20
api/tacticalrmm/core/migrations/0019_codesigntoken.py
Normal file
20
api/tacticalrmm/core/migrations/0019_codesigntoken.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 3.2 on 2021-04-13 05:41
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0018_auto_20210329_1709'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CodeSignToken',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('token', models.CharField(blank=True, max_length=255, null=True)),
|
||||
],
|
||||
),
|
||||
]
|
||||
21
api/tacticalrmm/core/migrations/0019_globalkvstore.py
Normal file
21
api/tacticalrmm/core/migrations/0019_globalkvstore.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# Generated by Django 3.1.7 on 2021-04-04 00:32
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0018_auto_20210329_1709'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='GlobalKVStore',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=25)),
|
||||
('value', models.TextField()),
|
||||
],
|
||||
),
|
||||
]
|
||||
14
api/tacticalrmm/core/migrations/0020_merge_20210415_0132.py
Normal file
14
api/tacticalrmm/core/migrations/0020_merge_20210415_0132.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# Generated by Django 3.1.7 on 2021-04-15 01:32
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0019_codesigntoken'),
|
||||
('core', '0019_globalkvstore'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.7 on 2021-04-24 23:56
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0020_merge_20210415_0132'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='customfield',
|
||||
name='hide_in_ui',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
22
api/tacticalrmm/core/migrations/0022_urlaction.py
Normal file
22
api/tacticalrmm/core/migrations/0022_urlaction.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 3.1.7 on 2021-05-02 02:13
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0021_customfield_hide_in_ui'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='URLAction',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=25)),
|
||||
('desc', models.CharField(blank=True, max_length=100, null=True)),
|
||||
('pattern', models.TextField()),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.2 on 2021-05-14 04:30
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0022_urlaction'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='coresettings',
|
||||
name='clear_faults_days',
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
]
|
||||
@@ -51,6 +51,7 @@ class CoreSettings(BaseAuditModel):
|
||||
)
|
||||
# removes check history older than days
|
||||
check_history_prune_days = models.PositiveIntegerField(default=30)
|
||||
clear_faults_days = models.IntegerField(default=0)
|
||||
mesh_token = models.CharField(max_length=255, null=True, blank=True, default="")
|
||||
mesh_username = models.CharField(max_length=255, null=True, blank=True, default="")
|
||||
mesh_site = models.CharField(max_length=255, null=True, blank=True, default="")
|
||||
@@ -79,7 +80,7 @@ class CoreSettings(BaseAuditModel):
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
from alerts.tasks import cache_agents_alert_template
|
||||
from automation.tasks import generate_all_agent_checks_task
|
||||
from automation.tasks import generate_agent_checks_task
|
||||
|
||||
if not self.pk and CoreSettings.objects.exists():
|
||||
raise ValidationError("There can only be one CoreSettings instance")
|
||||
@@ -97,14 +98,10 @@ class CoreSettings(BaseAuditModel):
|
||||
super(BaseAuditModel, self).save(*args, **kwargs)
|
||||
|
||||
# check if server polcies have changed and initiate task to reapply policies if so
|
||||
if old_settings and old_settings.server_policy != self.server_policy:
|
||||
generate_all_agent_checks_task.delay(mon_type="server", create_tasks=True)
|
||||
|
||||
# check if workstation polcies have changed and initiate task to reapply policies if so
|
||||
if old_settings and old_settings.workstation_policy != self.workstation_policy:
|
||||
generate_all_agent_checks_task.delay(
|
||||
mon_type="workstation", create_tasks=True
|
||||
)
|
||||
if (old_settings and old_settings.server_policy != self.server_policy) or (
|
||||
old_settings and old_settings.workstation_policy != self.workstation_policy
|
||||
):
|
||||
generate_agent_checks_task.delay(all=True, create_tasks=True)
|
||||
|
||||
if old_settings and old_settings.alert_template != self.alert_template:
|
||||
cache_agents_alert_template.delay()
|
||||
@@ -251,6 +248,7 @@ class CustomField(models.Model):
|
||||
blank=True,
|
||||
default=list,
|
||||
)
|
||||
hide_in_ui = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
unique_together = (("model", "name"),)
|
||||
@@ -266,3 +264,75 @@ class CustomField(models.Model):
|
||||
return self.default_value_bool
|
||||
else:
|
||||
return self.default_value_string
|
||||
|
||||
|
||||
class CodeSignToken(models.Model):
|
||||
token = models.CharField(max_length=255, null=True, blank=True)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.pk and CodeSignToken.objects.exists():
|
||||
raise ValidationError("There can only be one CodeSignToken instance")
|
||||
|
||||
super(CodeSignToken, self).save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return "Code signing token"
|
||||
|
||||
|
||||
class GlobalKVStore(models.Model):
|
||||
name = models.CharField(max_length=25)
|
||||
value = models.TextField()
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class URLAction(models.Model):
|
||||
name = models.CharField(max_length=25)
|
||||
desc = models.CharField(max_length=100, null=True, blank=True)
|
||||
pattern = models.TextField()
|
||||
|
||||
|
||||
RUN_ON_CHOICES = (
|
||||
("client", "Client"),
|
||||
("site", "Site"),
|
||||
("agent", "Agent"),
|
||||
("once", "Once"),
|
||||
)
|
||||
|
||||
SCHEDULE_CHOICES = (("daily", "Daily"), ("weekly", "Weekly"), ("monthly", "Monthly"))
|
||||
|
||||
|
||||
""" class GlobalTask(models.Model):
|
||||
script = models.ForeignKey(
|
||||
"scripts.Script",
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="script",
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
script_args = ArrayField(
|
||||
models.CharField(max_length=255, null=True, blank=True),
|
||||
null=True,
|
||||
blank=True,
|
||||
default=list,
|
||||
)
|
||||
custom_field = models.OneToOneField(
|
||||
"core.CustomField",
|
||||
related_name="globaltask",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
timeout = models.PositiveIntegerField(default=120)
|
||||
retcode = models.IntegerField(null=True, blank=True)
|
||||
retvalue = models.TextField(null=True, blank=True)
|
||||
stdout = models.TextField(null=True, blank=True)
|
||||
stderr = models.TextField(null=True, blank=True)
|
||||
execution_time = models.CharField(max_length=100, default="0.0000")
|
||||
run_schedule = models.CharField(
|
||||
max_length=25, choices=SCHEDULE_CHOICES, default="once"
|
||||
)
|
||||
run_on = models.CharField(
|
||||
max_length=25, choices=RUN_ON_CHOICES, default="once"
|
||||
) """
|
||||
|
||||
18
api/tacticalrmm/core/permissions.py
Normal file
18
api/tacticalrmm/core/permissions.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
from tacticalrmm.permissions import _has_perm
|
||||
|
||||
|
||||
class EditCoreSettingsPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_edit_core_settings")
|
||||
|
||||
|
||||
class ServerMaintPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_do_server_maint")
|
||||
|
||||
|
||||
class CodeSignPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_code_sign")
|
||||
@@ -1,7 +1,7 @@
|
||||
import pytz
|
||||
from rest_framework import serializers
|
||||
|
||||
from .models import CoreSettings, CustomField
|
||||
from .models import CodeSignToken, CoreSettings, CustomField, GlobalKVStore, URLAction
|
||||
|
||||
|
||||
class CoreSettingsSerializer(serializers.ModelSerializer):
|
||||
@@ -27,3 +27,21 @@ class CustomFieldSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = CustomField
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class CodeSignTokenSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = CodeSignToken
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class KeyStoreSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = GlobalKVStore
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class URLActionSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = URLAction
|
||||
fields = "__all__"
|
||||
|
||||
@@ -6,6 +6,7 @@ from loguru import logger
|
||||
from autotasks.models import AutomatedTask
|
||||
from autotasks.tasks import delete_win_task_schedule
|
||||
from checks.tasks import prune_check_history
|
||||
from agents.tasks import clear_faults_task
|
||||
from core.models import CoreSettings
|
||||
from tacticalrmm.celery import app
|
||||
|
||||
@@ -28,6 +29,11 @@ def core_maintenance_tasks():
|
||||
if now > task_time_utc:
|
||||
delete_win_task_schedule.delay(task.pk)
|
||||
|
||||
core = CoreSettings.objects.first()
|
||||
|
||||
# remove old CheckHistory data
|
||||
older_than = CoreSettings.objects.first().check_history_prune_days
|
||||
prune_check_history.delay(older_than)
|
||||
if core.check_history_prune_days > 0:
|
||||
prune_check_history.delay(core.check_history_prune_days)
|
||||
# clear faults
|
||||
if core.clear_faults_days > 0:
|
||||
clear_faults_task.delay(core.clear_faults_days)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
import requests
|
||||
from channels.db import database_sync_to_async
|
||||
from channels.testing import WebsocketCommunicator
|
||||
from model_bakery import baker
|
||||
@@ -7,11 +8,33 @@ from model_bakery import baker
|
||||
from tacticalrmm.test import TacticalTestCase
|
||||
|
||||
from .consumers import DashInfo
|
||||
from .models import CoreSettings, CustomField
|
||||
from .serializers import CustomFieldSerializer
|
||||
from .models import CoreSettings, CustomField, GlobalKVStore, URLAction
|
||||
from .serializers import CustomFieldSerializer, KeyStoreSerializer, URLActionSerializer
|
||||
from .tasks import core_maintenance_tasks
|
||||
|
||||
|
||||
class TestCodeSign(TacticalTestCase):
|
||||
def setUp(self):
|
||||
self.setup_coresettings()
|
||||
self.authenticate()
|
||||
self.url = "/core/codesign/"
|
||||
|
||||
def test_get_codesign(self):
|
||||
r = self.client.get(self.url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
self.check_not_authenticated("get", self.url)
|
||||
|
||||
@patch("requests.post")
|
||||
def test_edit_codesign_timeout(self, mock_post):
|
||||
mock_post.side_effect = requests.exceptions.ConnectionError()
|
||||
data = {"token": "token123"}
|
||||
r = self.client.patch(self.url, data, format="json")
|
||||
self.assertEqual(r.status_code, 400)
|
||||
|
||||
self.check_not_authenticated("patch", self.url)
|
||||
|
||||
|
||||
class TestConsumers(TacticalTestCase):
|
||||
def setUp(self):
|
||||
self.setup_coresettings()
|
||||
@@ -65,8 +88,8 @@ class TestCoreTasks(TacticalTestCase):
|
||||
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
@patch("automation.tasks.generate_all_agent_checks_task.delay")
|
||||
def test_edit_coresettings(self, generate_all_agent_checks_task):
|
||||
@patch("automation.tasks.generate_agent_checks_task.delay")
|
||||
def test_edit_coresettings(self, generate_agent_checks_task):
|
||||
url = "/core/editsettings/"
|
||||
|
||||
# setup
|
||||
@@ -83,7 +106,7 @@ class TestCoreTasks(TacticalTestCase):
|
||||
)
|
||||
self.assertEqual(CoreSettings.objects.first().mesh_token, data["mesh_token"])
|
||||
|
||||
generate_all_agent_checks_task.assert_not_called()
|
||||
generate_agent_checks_task.assert_not_called()
|
||||
|
||||
# test adding policy
|
||||
data = {
|
||||
@@ -97,9 +120,9 @@ class TestCoreTasks(TacticalTestCase):
|
||||
CoreSettings.objects.first().workstation_policy.id, policies[0].id # type: ignore
|
||||
)
|
||||
|
||||
self.assertEqual(generate_all_agent_checks_task.call_count, 2)
|
||||
generate_agent_checks_task.assert_called_once()
|
||||
|
||||
generate_all_agent_checks_task.reset_mock()
|
||||
generate_agent_checks_task.reset_mock()
|
||||
|
||||
# test remove policy
|
||||
data = {
|
||||
@@ -109,7 +132,7 @@ class TestCoreTasks(TacticalTestCase):
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(CoreSettings.objects.first().workstation_policy, None)
|
||||
|
||||
self.assertEqual(generate_all_agent_checks_task.call_count, 1)
|
||||
self.assertEqual(generate_agent_checks_task.call_count, 1)
|
||||
|
||||
self.check_not_authenticated("patch", url)
|
||||
|
||||
@@ -250,3 +273,147 @@ class TestCoreTasks(TacticalTestCase):
|
||||
self.assertFalse(CustomField.objects.filter(pk=custom_field.id).exists()) # type: ignore
|
||||
|
||||
self.check_not_authenticated("delete", url)
|
||||
|
||||
def test_get_keystore(self):
|
||||
url = "/core/keystore/"
|
||||
|
||||
# setup
|
||||
keys = baker.make("core.GlobalKVStore", _quantity=2)
|
||||
|
||||
r = self.client.get(url)
|
||||
serializer = KeyStoreSerializer(keys, many=True)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(len(r.data), 2) # type: ignore
|
||||
self.assertEqual(r.data, serializer.data) # type: ignore
|
||||
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
def test_add_keystore(self):
|
||||
url = "/core/keystore/"
|
||||
|
||||
data = {"name": "test", "value": "text"}
|
||||
r = self.client.post(url, data)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
self.check_not_authenticated("post", url)
|
||||
|
||||
def test_update_keystore(self):
|
||||
# setup
|
||||
key = baker.make("core.GlobalKVStore")
|
||||
|
||||
# test not found
|
||||
r = self.client.put("/core/keystore/500/")
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
url = f"/core/keystore/{key.id}/" # type: ignore
|
||||
data = {"name": "test", "value": "text"}
|
||||
r = self.client.put(url, data)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
new_key = GlobalKVStore.objects.get(pk=key.id) # type: ignore
|
||||
self.assertEqual(new_key.name, data["name"])
|
||||
self.assertEqual(new_key.value, data["value"])
|
||||
|
||||
self.check_not_authenticated("put", url)
|
||||
|
||||
def test_delete_keystore(self):
|
||||
# setup
|
||||
key = baker.make("core.GlobalKVStore")
|
||||
|
||||
# test not found
|
||||
r = self.client.delete("/core/keystore/500/")
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
url = f"/core/keystore/{key.id}/" # type: ignore
|
||||
r = self.client.delete(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
self.assertFalse(GlobalKVStore.objects.filter(pk=key.id).exists()) # type: ignore
|
||||
|
||||
self.check_not_authenticated("delete", url)
|
||||
|
||||
def test_get_urlaction(self):
|
||||
url = "/core/urlaction/"
|
||||
|
||||
# setup
|
||||
action = baker.make("core.URLAction", _quantity=2)
|
||||
|
||||
r = self.client.get(url)
|
||||
serializer = URLActionSerializer(action, many=True)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(len(r.data), 2) # type: ignore
|
||||
self.assertEqual(r.data, serializer.data) # type: ignore
|
||||
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
def test_add_urlaction(self):
|
||||
url = "/core/urlaction/"
|
||||
|
||||
data = {"name": "name", "desc": "desc", "pattern": "pattern"}
|
||||
r = self.client.post(url, data)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
self.check_not_authenticated("post", url)
|
||||
|
||||
def test_update_urlaction(self):
|
||||
# setup
|
||||
action = baker.make("core.URLAction")
|
||||
|
||||
# test not found
|
||||
r = self.client.put("/core/urlaction/500/")
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
url = f"/core/urlaction/{action.id}/" # type: ignore
|
||||
data = {"name": "test", "pattern": "text"}
|
||||
r = self.client.put(url, data)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
new_action = URLAction.objects.get(pk=action.id) # type: ignore
|
||||
self.assertEqual(new_action.name, data["name"])
|
||||
self.assertEqual(new_action.pattern, data["pattern"])
|
||||
|
||||
self.check_not_authenticated("put", url)
|
||||
|
||||
def test_delete_urlaction(self):
|
||||
# setup
|
||||
action = baker.make("core.URLAction")
|
||||
|
||||
# test not found
|
||||
r = self.client.delete("/core/urlaction/500/")
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
url = f"/core/urlaction/{action.id}/" # type: ignore
|
||||
r = self.client.delete(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
self.assertFalse(URLAction.objects.filter(pk=action.id).exists()) # type: ignore
|
||||
|
||||
self.check_not_authenticated("delete", url)
|
||||
|
||||
def test_run_url_action(self):
|
||||
self.maxDiff = None
|
||||
# setup
|
||||
agent = baker.make_recipe(
|
||||
"agents.agent", agent_id="123123-assdss4s-343-sds545-45dfdf|DESKTOP"
|
||||
)
|
||||
baker.make("core.GlobalKVStore", name="Test Name", value="value with space")
|
||||
action = baker.make(
|
||||
"core.URLAction",
|
||||
pattern="https://remote.example.com/connect?globalstore={{global.Test Name}}&client_name={{client.name}}&site id={{site.id}}&agent_id={{agent.agent_id}}",
|
||||
)
|
||||
|
||||
url = "/core/urlaction/run/"
|
||||
# test not found
|
||||
r = self.client.patch(url, {"agent": 500, "action": 500})
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
data = {"agent": agent.id, "action": action.id} # type: ignore
|
||||
r = self.client.patch(url, data)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
self.assertEqual(
|
||||
r.data, # type: ignore
|
||||
f"https://remote.example.com/connect?globalstore=value%20with%20space&client_name={agent.client.name}&site%20id={agent.site.id}&agent_id=123123-assdss4s-343-sds545-45dfdf%7CDESKTOP",
|
||||
)
|
||||
|
||||
self.check_not_authenticated("patch", url)
|
||||
|
||||
@@ -12,4 +12,10 @@ urlpatterns = [
|
||||
path("servermaintenance/", views.server_maintenance),
|
||||
path("customfields/", views.GetAddCustomFields.as_view()),
|
||||
path("customfields/<int:pk>/", views.GetUpdateDeleteCustomFields.as_view()),
|
||||
path("codesign/", views.CodeSign.as_view()),
|
||||
path("keystore/", views.GetAddKeyStore.as_view()),
|
||||
path("keystore/<int:pk>/", views.UpdateDeleteKeyStore.as_view()),
|
||||
path("urlaction/", views.GetAddURLAction.as_view()),
|
||||
path("urlaction/<int:pk>/", views.UpdateDeleteURLAction.as_view()),
|
||||
path("urlaction/run/", views.RunURLAction.as_view()),
|
||||
]
|
||||
|
||||
@@ -1,21 +1,32 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.exceptions import ParseError
|
||||
from rest_framework.parsers import FileUploadParser
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from agents.permissions import MeshPerms
|
||||
from tacticalrmm.utils import notify_error
|
||||
|
||||
from .models import CoreSettings, CustomField
|
||||
from .serializers import CoreSettingsSerializer, CustomFieldSerializer
|
||||
from .models import CodeSignToken, CoreSettings, CustomField, GlobalKVStore, URLAction
|
||||
from .permissions import CodeSignPerms, EditCoreSettingsPerms, ServerMaintPerms
|
||||
from .serializers import (
|
||||
CodeSignTokenSerializer,
|
||||
CoreSettingsSerializer,
|
||||
CustomFieldSerializer,
|
||||
KeyStoreSerializer,
|
||||
URLActionSerializer,
|
||||
)
|
||||
|
||||
|
||||
class UploadMeshAgent(APIView):
|
||||
permission_classes = [IsAuthenticated, MeshPerms]
|
||||
parser_class = (FileUploadParser,)
|
||||
|
||||
def put(self, request, format=None):
|
||||
@@ -41,6 +52,7 @@ def get_core_settings(request):
|
||||
|
||||
|
||||
@api_view(["PATCH"])
|
||||
@permission_classes([IsAuthenticated, EditCoreSettingsPerms])
|
||||
def edit_settings(request):
|
||||
coresettings = CoreSettings.objects.first()
|
||||
serializer = CoreSettingsSerializer(instance=coresettings, data=request.data)
|
||||
@@ -57,14 +69,23 @@ def version(request):
|
||||
|
||||
@api_view()
|
||||
def dashboard_info(request):
|
||||
from tacticalrmm.utils import get_latest_trmm_ver
|
||||
|
||||
return Response(
|
||||
{
|
||||
"trmm_version": settings.TRMM_VERSION,
|
||||
"latest_trmm_ver": get_latest_trmm_ver(),
|
||||
"dark_mode": request.user.dark_mode,
|
||||
"show_community_scripts": request.user.show_community_scripts,
|
||||
"dbl_click_action": request.user.agent_dblclick_action,
|
||||
"default_agent_tbl_tab": request.user.default_agent_tbl_tab,
|
||||
"url_action": request.user.url_action.id
|
||||
if request.user.url_action
|
||||
else None,
|
||||
"client_tree_sort": request.user.client_tree_sort,
|
||||
"client_tree_splitter": request.user.client_tree_splitter,
|
||||
"loading_bar_color": request.user.loading_bar_color,
|
||||
"hosted": hasattr(settings, "HOSTED") and settings.HOSTED,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -83,6 +104,7 @@ def email_test(request):
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
@permission_classes([IsAuthenticated, ServerMaintPerms])
|
||||
def server_maintenance(request):
|
||||
from tacticalrmm.utils import reload_nats
|
||||
|
||||
@@ -137,6 +159,8 @@ def server_maintenance(request):
|
||||
|
||||
|
||||
class GetAddCustomFields(APIView):
|
||||
permission_classes = [IsAuthenticated, EditCoreSettingsPerms]
|
||||
|
||||
def get(self, request):
|
||||
fields = CustomField.objects.all()
|
||||
return Response(CustomFieldSerializer(fields, many=True).data)
|
||||
@@ -157,6 +181,8 @@ class GetAddCustomFields(APIView):
|
||||
|
||||
|
||||
class GetUpdateDeleteCustomFields(APIView):
|
||||
permission_classes = [IsAuthenticated, EditCoreSettingsPerms]
|
||||
|
||||
def get(self, request, pk):
|
||||
custom_field = get_object_or_404(CustomField, pk=pk)
|
||||
|
||||
@@ -177,3 +203,153 @@ class GetUpdateDeleteCustomFields(APIView):
|
||||
get_object_or_404(CustomField, pk=pk).delete()
|
||||
|
||||
return Response("ok")
|
||||
|
||||
|
||||
class CodeSign(APIView):
|
||||
permission_classes = [IsAuthenticated, CodeSignPerms]
|
||||
|
||||
def get(self, request):
|
||||
token = CodeSignToken.objects.first()
|
||||
return Response(CodeSignTokenSerializer(token).data)
|
||||
|
||||
def patch(self, request):
|
||||
import requests
|
||||
|
||||
errors = []
|
||||
for url in settings.EXE_GEN_URLS:
|
||||
try:
|
||||
r = requests.post(
|
||||
f"{url}/api/v1/checktoken",
|
||||
json={"token": request.data["token"]},
|
||||
headers={"Content-type": "application/json"},
|
||||
timeout=15,
|
||||
)
|
||||
except Exception as e:
|
||||
errors.append(str(e))
|
||||
else:
|
||||
errors = []
|
||||
break
|
||||
|
||||
if errors:
|
||||
return notify_error(", ".join(errors))
|
||||
|
||||
if r.status_code == 400 or r.status_code == 401: # type: ignore
|
||||
return notify_error(r.json()["ret"]) # type: ignore
|
||||
elif r.status_code == 200: # type: ignore
|
||||
t = CodeSignToken.objects.first()
|
||||
if t is None:
|
||||
CodeSignToken.objects.create(token=request.data["token"])
|
||||
else:
|
||||
serializer = CodeSignTokenSerializer(instance=t, data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return Response("Token was saved")
|
||||
|
||||
try:
|
||||
ret = r.json()["ret"] # type: ignore
|
||||
except:
|
||||
ret = "Something went wrong"
|
||||
return notify_error(ret)
|
||||
|
||||
def post(self, request):
|
||||
from agents.models import Agent
|
||||
from agents.tasks import force_code_sign
|
||||
|
||||
err = "A valid token must be saved first"
|
||||
try:
|
||||
t = CodeSignToken.objects.first().token
|
||||
except:
|
||||
return notify_error(err)
|
||||
|
||||
if t is None or t == "":
|
||||
return notify_error(err)
|
||||
|
||||
pks: list[int] = list(Agent.objects.only("pk").values_list("pk", flat=True))
|
||||
force_code_sign.delay(pks=pks)
|
||||
return Response("Agents will be code signed shortly")
|
||||
|
||||
|
||||
class GetAddKeyStore(APIView):
|
||||
permission_classes = [IsAuthenticated, EditCoreSettingsPerms]
|
||||
|
||||
def get(self, request):
|
||||
keys = GlobalKVStore.objects.all()
|
||||
return Response(KeyStoreSerializer(keys, many=True).data)
|
||||
|
||||
def post(self, request):
|
||||
serializer = KeyStoreSerializer(data=request.data, partial=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
|
||||
return Response("ok")
|
||||
|
||||
|
||||
class UpdateDeleteKeyStore(APIView):
|
||||
permission_classes = [IsAuthenticated, EditCoreSettingsPerms]
|
||||
|
||||
def put(self, request, pk):
|
||||
key = get_object_or_404(GlobalKVStore, pk=pk)
|
||||
|
||||
serializer = KeyStoreSerializer(instance=key, data=request.data, partial=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
|
||||
return Response("ok")
|
||||
|
||||
def delete(self, request, pk):
|
||||
get_object_or_404(GlobalKVStore, pk=pk).delete()
|
||||
|
||||
return Response("ok")
|
||||
|
||||
|
||||
class GetAddURLAction(APIView):
|
||||
def get(self, request):
|
||||
actions = URLAction.objects.all()
|
||||
return Response(URLActionSerializer(actions, many=True).data)
|
||||
|
||||
def post(self, request):
|
||||
serializer = URLActionSerializer(data=request.data, partial=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
|
||||
return Response("ok")
|
||||
|
||||
|
||||
class UpdateDeleteURLAction(APIView):
|
||||
def put(self, request, pk):
|
||||
action = get_object_or_404(URLAction, pk=pk)
|
||||
|
||||
serializer = URLActionSerializer(
|
||||
instance=action, data=request.data, partial=True
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
|
||||
return Response("ok")
|
||||
|
||||
def delete(self, request, pk):
|
||||
get_object_or_404(URLAction, pk=pk).delete()
|
||||
|
||||
return Response("ok")
|
||||
|
||||
|
||||
class RunURLAction(APIView):
|
||||
def patch(self, request):
|
||||
from requests.utils import requote_uri
|
||||
|
||||
from agents.models import Agent
|
||||
from tacticalrmm.utils import replace_db_values
|
||||
|
||||
agent = get_object_or_404(Agent, pk=request.data["agent"])
|
||||
action = get_object_or_404(URLAction, pk=request.data["action"])
|
||||
|
||||
pattern = re.compile("\\{\\{([\\w\\s]+\\.[\\w\\s]+)\\}\\}")
|
||||
|
||||
url_pattern = action.pattern
|
||||
|
||||
for string in re.findall(pattern, action.pattern):
|
||||
value = replace_db_values(string=string, agent=agent, quotes=False)
|
||||
|
||||
url_pattern = re.sub("\\{\\{" + string + "\\}\\}", str(value), url_pattern)
|
||||
|
||||
return Response(requote_uri(url_pattern))
|
||||
|
||||
@@ -7,7 +7,7 @@ from tacticalrmm.middleware import get_debug_info, get_username
|
||||
|
||||
ACTION_TYPE_CHOICES = [
|
||||
("schedreboot", "Scheduled Reboot"),
|
||||
("taskaction", "Scheduled Task Action"),
|
||||
("taskaction", "Scheduled Task Action"), # deprecated
|
||||
("agentupdate", "Agent Update"),
|
||||
("chocoinstall", "Chocolatey Software Install"),
|
||||
]
|
||||
@@ -42,13 +42,6 @@ AUDIT_OBJECT_TYPE_CHOICES = [
|
||||
("bulk", "Bulk"),
|
||||
]
|
||||
|
||||
# taskaction details format
|
||||
# {
|
||||
# "action": "taskcreate" | "taskdelete" | "tasktoggle",
|
||||
# "value": "Enable" | "Disable" # only needed for task toggle,
|
||||
# "task_id": 1
|
||||
# }
|
||||
|
||||
STATUS_CHOICES = [
|
||||
("pending", "Pending"),
|
||||
("completed", "Completed"),
|
||||
@@ -250,8 +243,6 @@ class PendingAction(models.Model):
|
||||
if self.action_type == "schedreboot":
|
||||
obj = dt.datetime.strptime(self.details["time"], "%Y-%m-%d %H:%M:%S")
|
||||
return dt.datetime.strftime(obj, "%B %d, %Y at %I:%M %p")
|
||||
elif self.action_type == "taskaction":
|
||||
return "Next agent check-in"
|
||||
elif self.action_type == "agentupdate":
|
||||
return "Next update cycle"
|
||||
elif self.action_type == "chocoinstall":
|
||||
@@ -268,20 +259,6 @@ class PendingAction(models.Model):
|
||||
elif self.action_type == "chocoinstall":
|
||||
return f"{self.details['name']} software install"
|
||||
|
||||
elif self.action_type == "taskaction":
|
||||
if self.details["action"] == "taskdelete":
|
||||
return "Device pending task deletion"
|
||||
elif self.details["action"] == "taskcreate":
|
||||
return "Device pending task creation"
|
||||
elif self.details["action"] == "tasktoggle":
|
||||
# value is bool
|
||||
if self.details["value"]:
|
||||
action = "enable"
|
||||
else:
|
||||
action = "disable"
|
||||
|
||||
return f"Device pending task {action}"
|
||||
|
||||
|
||||
class BaseAuditModel(models.Model):
|
||||
# abstract base class for auditing models
|
||||
|
||||
21
api/tacticalrmm/logs/permissions.py
Normal file
21
api/tacticalrmm/logs/permissions.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
from tacticalrmm.permissions import _has_perm
|
||||
|
||||
|
||||
class AuditLogPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_view_auditlogs")
|
||||
|
||||
|
||||
class ManagePendingActionPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
if r.method == "PATCH":
|
||||
return True
|
||||
|
||||
return _has_perm(r, "can_manage_pendingactions")
|
||||
|
||||
|
||||
class DebugLogPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_view_debuglogs")
|
||||
@@ -9,7 +9,8 @@ from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils import timezone as djangotime
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
@@ -20,10 +21,13 @@ from agents.serializers import AgentHostnameSerializer
|
||||
from tacticalrmm.utils import notify_error
|
||||
|
||||
from .models import AuditLog, PendingAction
|
||||
from .permissions import AuditLogPerms, DebugLogPerms, ManagePendingActionPerms
|
||||
from .serializers import AuditLogSerializer, PendingActionSerializer
|
||||
|
||||
|
||||
class GetAuditLogs(APIView):
|
||||
permission_classes = [IsAuthenticated, AuditLogPerms]
|
||||
|
||||
def patch(self, request):
|
||||
from agents.models import Agent
|
||||
from clients.models import Client
|
||||
@@ -92,6 +96,8 @@ class GetAuditLogs(APIView):
|
||||
|
||||
|
||||
class FilterOptionsAuditLog(APIView):
|
||||
permission_classes = [IsAuthenticated, AuditLogPerms]
|
||||
|
||||
def post(self, request):
|
||||
if request.data["type"] == "agent":
|
||||
agents = Agent.objects.filter(hostname__icontains=request.data["pattern"])
|
||||
@@ -107,6 +113,8 @@ class FilterOptionsAuditLog(APIView):
|
||||
|
||||
|
||||
class PendingActions(APIView):
|
||||
permission_classes = [IsAuthenticated, ManagePendingActionPerms]
|
||||
|
||||
def patch(self, request):
|
||||
status_filter = "completed" if request.data["showCompleted"] else "pending"
|
||||
if "agentPK" in request.data.keys():
|
||||
@@ -149,6 +157,7 @@ class PendingActions(APIView):
|
||||
|
||||
|
||||
@api_view()
|
||||
@permission_classes([IsAuthenticated, DebugLogPerms])
|
||||
def debug_log(request, mode, hostname, order):
|
||||
log_file = settings.LOG_CONFIG["handlers"][0]["sink"]
|
||||
|
||||
@@ -191,6 +200,7 @@ def debug_log(request, mode, hostname, order):
|
||||
|
||||
|
||||
@api_view()
|
||||
@permission_classes([IsAuthenticated, DebugLogPerms])
|
||||
def download_log(request):
|
||||
log_file = settings.LOG_CONFIG["handlers"][0]["sink"]
|
||||
if settings.DEBUG:
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
asgiref==3.3.4
|
||||
asyncio-nats-client==0.11.4
|
||||
celery==5.0.5
|
||||
certifi==2020.12.5
|
||||
celery==5.1.0
|
||||
certifi==2021.5.30
|
||||
cffi==1.14.5
|
||||
channels==3.0.3
|
||||
channels_redis==3.2.0
|
||||
chardet==4.0.0
|
||||
cryptography==3.4.7
|
||||
daphne==3.0.2
|
||||
Django==3.2.0
|
||||
Django==3.2.4
|
||||
django-cors-headers==3.7.0
|
||||
django-rest-knox==4.1.0
|
||||
djangorestframework==3.12.4
|
||||
future==0.18.2
|
||||
kombu==5.0.2
|
||||
loguru==0.5.3
|
||||
msgpack==1.0.2
|
||||
packaging==20.9
|
||||
@@ -25,10 +25,10 @@ pytz==2021.1
|
||||
qrcode==6.1
|
||||
redis==3.5.3
|
||||
requests==2.25.1
|
||||
six==1.15.0
|
||||
six==1.16.0
|
||||
sqlparse==0.4.1
|
||||
twilio==6.56.0
|
||||
urllib3==1.26.4
|
||||
twilio==6.59.1
|
||||
urllib3==1.26.5
|
||||
uWSGI==2.0.19.1
|
||||
validators==0.18.2
|
||||
vine==5.0.0
|
||||
|
||||
@@ -1,63 +1,70 @@
|
||||
[
|
||||
{
|
||||
"guid": "6820cb5e-5a7f-4d9b-8c22-d54677e3cc04",
|
||||
"filename": "Win_Clear_Firefox_Cache.ps1",
|
||||
"filename": "Win_Firefox_Clear_Cache.ps1",
|
||||
"submittedBy": "https://github.com/Omnicef",
|
||||
"name": "Clear Firefox Cache",
|
||||
"name": "Firefox - Clean Cache",
|
||||
"description": "This script will clean up Mozilla Firefox for all users.",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Browsers"
|
||||
"category": "TRMM (Win):Browsers",
|
||||
"default_timeout": "300"
|
||||
|
||||
},
|
||||
{
|
||||
"guid": "3ff6a386-11d1-4f9d-8cca-1b0563bb6443",
|
||||
"filename": "Win_Clear_Google_Chrome_Cache.ps1",
|
||||
"filename": "Win_Google_Chrome_Clear_Cache.ps1",
|
||||
"submittedBy": "https://github.com/Omnicef",
|
||||
"name": "Clear Google Chrome Cache",
|
||||
"name": "Chrome - Clear Cache for All Users",
|
||||
"description": "This script will clean up Google Chrome for all users.",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Browsers"
|
||||
"category": "TRMM (Win):Browsers",
|
||||
"default_timeout": "300"
|
||||
},
|
||||
{
|
||||
"guid": "be1de837-f677-4ac5-aa0c-37a0fc9991fc",
|
||||
"filename": "Win_Install_Adobe_Reader.ps1",
|
||||
"submittedBy": "https://github.com/Omnicef",
|
||||
"name": "Install Adobe Reader DC",
|
||||
"name": "Adobe Reader DC - Install",
|
||||
"description": "Installs Adobe Reader DC.",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):3rd Party Software>Chocolatey"
|
||||
"category": "TRMM (Win):3rd Party Software>Chocolatey",
|
||||
"default_timeout": "300"
|
||||
},
|
||||
{
|
||||
"guid": "2ee134d5-76aa-4160-b334-a1efbc62079f",
|
||||
"filename": "Win_Install_Duplicati.ps1",
|
||||
"submittedBy": "https://github.com/Omnicef",
|
||||
"name": "Install Duplicati",
|
||||
"name": "Duplicati - Install",
|
||||
"description": "This script installs Duplicati 2.0.5.1 as a service.",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):3rd Party Software"
|
||||
"category": "TRMM (Win):3rd Party Software",
|
||||
"default_timeout": "300"
|
||||
},
|
||||
{
|
||||
"guid": "81cc5bcb-01bf-4b0c-89b9-0ac0f3fe0c04",
|
||||
"filename": "Win_Reset_Windows_Update.ps1",
|
||||
"submittedBy": "https://github.com/Omnicef",
|
||||
"name": "Reset Windows Update",
|
||||
"name": "Windows Update - Reset",
|
||||
"description": "This script will reset all of the Windows Updates components to DEFAULT SETTINGS.",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Updates"
|
||||
"category": "TRMM (Win):Updates",
|
||||
"default_timeout": "300"
|
||||
},
|
||||
{
|
||||
"guid": "8db87ff0-a9b4-4d9d-bc55-377bbcb85b6d",
|
||||
"filename": "Win_Start_Cleanup.ps1",
|
||||
"submittedBy": "https://github.com/Omnicef",
|
||||
"name": "Cleanup C: drive",
|
||||
"name": "Disk - Cleanup C: drive",
|
||||
"description": "Cleans the C: drive's Window Temperary files, Windows SoftwareDistribution folder, the local users Temperary folder, IIS logs (if applicable) and empties the recycling bin. All deleted files will go into a log transcript in $env:TEMP. By default this script leaves files that are newer than 7 days old however this variable can be edited.",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Other"
|
||||
"category": "TRMM (Win):Maintenance",
|
||||
"default_timeout": "25000"
|
||||
},
|
||||
{
|
||||
"guid": "2f28e8c1-ae0f-4b46-a826-f513974526a3",
|
||||
"filename": "Win_Defender_FullScan_Background.ps1",
|
||||
"submittedBy": "https://github.com/Omnicef",
|
||||
"name": "Windows Defender Full Scan",
|
||||
"name": "Defender - Full Scan",
|
||||
"description": "Runs a Windows Defender Full background scan.",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Security>Antivirus"
|
||||
@@ -66,7 +73,7 @@
|
||||
"guid": "adf81ddb-3b77-415c-a89b-2ccc826b5aa7",
|
||||
"filename": "Win_Defender_QuickScan_Background.ps1",
|
||||
"submittedBy": "https://github.com/Omnicef",
|
||||
"name": "Windows Defender Quick Scan",
|
||||
"name": "Defender - Quick Scan",
|
||||
"description": "Runs a Quick Scan using Windows Defender in the Background.",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Security>Antivirus"
|
||||
@@ -75,16 +82,17 @@
|
||||
"guid": "3c46290b-85db-4cd2-93a2-943c8c93b3b1",
|
||||
"filename": "Speedtest.py",
|
||||
"submittedBy": "https://github.com/wh1te909",
|
||||
"name": "Speed Test",
|
||||
"name": "Speed Test - Python",
|
||||
"description": "Runs a Speed Test using Python",
|
||||
"shell": "python",
|
||||
"category": "TRMM (Win):Network"
|
||||
"category": "TRMM (Win):Network",
|
||||
"default_timeout": "120"
|
||||
},
|
||||
{
|
||||
"guid": "9d34f482-1f0c-4b2f-b65f-a9cf3c13ef5f",
|
||||
"filename": "Win_Rename_Installed_App.ps1",
|
||||
"filename": "Win_TRMM_Rename_Installed_App.ps1",
|
||||
"submittedBy": "https://github.com/bradhawkins85",
|
||||
"name": "Rename Tactical RMM Agent",
|
||||
"name": "TacticalRMM Agent Rename",
|
||||
"description": "Updates the DisplayName registry entry for the Tactical RMM windows agent to your desired name. This script takes 1 required argument: the name you wish to set.",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):TacticalRMM Related"
|
||||
@@ -93,7 +101,7 @@
|
||||
"guid": "525ae965-1dcf-4c17-92b3-5da3cf6819f5",
|
||||
"filename": "Win_Bitlocker_Encrypted_Drive_c.ps1",
|
||||
"submittedBy": "https://github.com/ThatsNASt",
|
||||
"name": "Check C Drive for Bitlocker Status",
|
||||
"name": "Bitlocker - Check C Drive for Status",
|
||||
"description": "Runs a check on drive C for Bitlocker status.",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Storage"
|
||||
@@ -102,7 +110,7 @@
|
||||
"guid": "2ea35fa2-c227-4d17-a40e-4d39f252e27a",
|
||||
"filename": "Win_Bitlocker_Create_Status_Report.ps1",
|
||||
"submittedBy": "https://github.com/ThatsNASt",
|
||||
"name": "Create Bitlocker Status Report",
|
||||
"name": "Bitlocker - Create Status Report",
|
||||
"description": "Creates a Bitlocker status report.",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Storage"
|
||||
@@ -111,34 +119,82 @@
|
||||
"guid": "9e5769c1-3873-4941-bf70-e851e0afbd6d",
|
||||
"filename": "Win_Bitlocker_Retrieve_Status_Report.ps1",
|
||||
"submittedBy": "https://github.com/ThatsNASt",
|
||||
"name": "Retreive Bitlocker Status Report",
|
||||
"name": "Bitlocker - Retrieve Status Report",
|
||||
"description": "Retreives a Bitlocker status report.",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Storage"
|
||||
},
|
||||
{
|
||||
"guid": "72b93487-0266-43f0-97cc-03d4c7ee0b44",
|
||||
"filename": "Win_Bitlocker_Get_Recovery_Keys.ps1",
|
||||
"submittedBy": "https://github.com/silversword411",
|
||||
"name": "Bitlocker - Get Recovery Keys",
|
||||
"description": "Retreives a Bitlocker Recovery Keys",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Storage"
|
||||
},
|
||||
{
|
||||
"guid": "cfa14c28-4dfc-4d4e-95ee-a380652e058d",
|
||||
"filename": "Win_Bios_Check.ps1",
|
||||
"submittedBy": "https://github.com/ThatsNASt",
|
||||
"name": "Check BIOS Information",
|
||||
"name": "BIOS - Check Information",
|
||||
"description": "Retreives and reports on BIOS make, version, and date.",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Hardware"
|
||||
},
|
||||
{
|
||||
"guid": "e1c27982-b955-4766-85b6-d92527a177cf",
|
||||
"filename": "Win_Hardware_Monitor_Get_Info.ps1",
|
||||
"submittedBy": "https://github.com/MaxAnderson95/",
|
||||
"name": "Monitor - Get Info",
|
||||
"description": "Retreives and reports on Monitor info: Manufacturer, Model, Serial",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Hardware"
|
||||
},
|
||||
{
|
||||
"guid": "ae231ac4-b01f-4a39-a9d2-3d817af75260",
|
||||
"filename": "Win_Hardware_RAM_Status.ps1",
|
||||
"submittedBy": "https://github.com/silversword411",
|
||||
"name": "RAM - Check Information",
|
||||
"description": "Retreives and reports on RAM info: DIMM's, total memory, slots total and used",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Hardware"
|
||||
},
|
||||
{
|
||||
"guid": "72c56717-28ed-4cc6-b30f-b362d30fb4b6",
|
||||
"filename": "Win_Hardware_SN.ps1",
|
||||
"submittedBy": "https://github.com/subzdev",
|
||||
"name": "Hardware - Get Serial Number",
|
||||
"description": "Returns BIOS Serial Number - Use with Custom Fields for later use",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Collectors"
|
||||
},
|
||||
{
|
||||
"guid": "973c34d7-cab0-4fda-999c-b4933655f946",
|
||||
"filename": "Win_Screenconnect_GetGUID.ps1",
|
||||
"submittedBy": "https://github.com/silversword411",
|
||||
"name": "Screenconnect - Get GUID for client",
|
||||
"description": "Returns Screenconnect GUID for client - Use with Custom Fields for later use. ",
|
||||
"args": [
|
||||
"-serviceName {{client.ScreenConnectService}}"
|
||||
],
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Collectors"
|
||||
},
|
||||
{
|
||||
"guid": "95a2ee6f-b89b-4551-856e-3081b041caa7",
|
||||
"filename": "Win_Reset_High_Performance_Power_Profile_to_Defaults.ps1",
|
||||
"filename": "Win_Power_Profile_Reset_High_Performance_to_Defaults.ps1",
|
||||
"submittedBy": "https://github.com/azulskyknight",
|
||||
"name": "Reset High Perf Power Profile",
|
||||
"name": "Power Profile - Reset High Perf Power Profile to defaults",
|
||||
"description": "Resets monitor, disk, standby, and hibernate timers in the default High Performance power profile to their default values. It also re-indexes the AC and DC power profiles into their default order.",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Power"
|
||||
},
|
||||
{
|
||||
"guid": "2cbd30b0-84dd-4388-a36d-2e2e980f1a3e",
|
||||
"filename": "Win_Set_High_Performance_Power_Profile.ps1",
|
||||
"filename": "Win_Power_Profile_Set_High_Performance.ps1",
|
||||
"submittedBy": "https://github.com/azulskyknight",
|
||||
"name": "Set High Perf Power Profile",
|
||||
"name": "Power Profile - Set High Performance",
|
||||
"description": "Sets the High Performance Power profile to the active power profile. Use this to keep machines from falling asleep.",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Power"
|
||||
@@ -150,13 +206,14 @@
|
||||
"name": "Windows 10 Upgrade",
|
||||
"description": "Forces an upgrade to the latest release of Windows 10.",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Updates"
|
||||
"category": "TRMM (Win):Updates",
|
||||
"default_timeout": "25000"
|
||||
},
|
||||
{
|
||||
"guid": "375323e5-cac6-4f35-a304-bb7cef35902d",
|
||||
"filename": "Win_Disk_Status.ps1",
|
||||
"submittedBy": "https://github.com/dinger1986",
|
||||
"name": "Check Disk Hardware Health (using Event Viewer errors)",
|
||||
"name": "Disk Hardware Health Check (using Event Viewer errors)",
|
||||
"description": "Checks local disks for errors reported in event viewer within the last 24 hours",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Hardware"
|
||||
@@ -165,7 +222,7 @@
|
||||
"guid": "7c14beb4-d1c3-41aa-8e70-92a267d6e080",
|
||||
"filename": "Win_Duplicati_Status.ps1",
|
||||
"submittedBy": "https://github.com/dinger1986",
|
||||
"name": "Check Duplicati",
|
||||
"name": "Duplicati - Check Status",
|
||||
"description": "Checks Duplicati Backup is running properly over the last 24 hours",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):3rd Party Software"
|
||||
@@ -174,7 +231,7 @@
|
||||
"guid": "da51111c-aff6-4d87-9d76-0608e1f67fe5",
|
||||
"filename": "Win_Defender_Enable.ps1",
|
||||
"submittedBy": "https://github.com/dinger1986",
|
||||
"name": "Enable Windows Defender",
|
||||
"name": "Defender - Enable",
|
||||
"description": "Enables Windows Defender and sets preferences",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Security>Antivirus"
|
||||
@@ -183,16 +240,17 @@
|
||||
"guid": "a223d03a-e22e-40e0-94f2-92dd8c481d14",
|
||||
"filename": "Win_Open_SSH_Server_Install.ps1",
|
||||
"submittedBy": "https://github.com/dinger1986",
|
||||
"name": "Install SSH",
|
||||
"description": "Installs and enabled OpenSSH Server",
|
||||
"name": "SSH - Install Feature and Enable",
|
||||
"description": "Installs and enabled OpenSSH Server Feature in Win10",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Windows Features"
|
||||
"category": "TRMM (Win):Windows Features",
|
||||
"default_timeout": "300"
|
||||
},
|
||||
{
|
||||
"guid": "2435297a-6263-4e90-8688-1847400d0e22",
|
||||
"filename": "Win_RDP_enable.bat",
|
||||
"submittedBy": "https://github.com/dinger1986",
|
||||
"name": "Enable RDP",
|
||||
"name": "RDP - Enable",
|
||||
"description": "Enables RDP",
|
||||
"shell": "cmd",
|
||||
"category": "TRMM (Win):Windows Features"
|
||||
@@ -201,7 +259,7 @@
|
||||
"guid": "24f19ead-fdfe-46b4-9dcb-4cd0e12a3940",
|
||||
"filename": "Win_Speedtest.ps1",
|
||||
"submittedBy": "https://github.com/dinger1986",
|
||||
"name": "Speed Test Powershell",
|
||||
"name": "Speed Test - Powershell",
|
||||
"description": "Speed Test with Powershell(win 10 or server2016+)",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Network"
|
||||
@@ -210,26 +268,35 @@
|
||||
"guid": "a821975c-60df-4d58-8990-6cf8a55b4ee0",
|
||||
"filename": "Win_Sync_Time.bat",
|
||||
"submittedBy": "https://github.com/dinger1986",
|
||||
"name": "Sync DC Time",
|
||||
"name": "ADDC - Sync DC Time",
|
||||
"description": "Syncs time with domain controller",
|
||||
"shell": "cmd",
|
||||
"category": "TRMM (Win):Active Directory"
|
||||
},
|
||||
{
|
||||
"guid": "b6b9912f-4274-4162-99cc-9fd47fbcb292",
|
||||
"filename": "Win_ADDC_Sync_Start.bat",
|
||||
"submittedBy": "https://github.com/silversword411",
|
||||
"name": "ADDC - Sync AD",
|
||||
"description": "Trigger AD Sync on domain controller",
|
||||
"shell": "cmd",
|
||||
"category": "TRMM (Win):Active Directory"
|
||||
},
|
||||
{
|
||||
"guid": "b720e320-7755-4c89-9992-e1a6c43699ed",
|
||||
"filename": "Win_Defender_Clear_Logs.ps1",
|
||||
"submittedBy": "https://github.com/dinger1986",
|
||||
"name": "Clear Defender Logs",
|
||||
"name": "Defender - Clear Logs",
|
||||
"description": "Clears Windows Defender Logs",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Security>Antivirus"
|
||||
},
|
||||
{
|
||||
"guid": "d980fda3-a068-47eb-8495-1aab07a24e64",
|
||||
"filename": "Win_Defender_Status.ps1",
|
||||
"filename": "Win_Defender_Status_Report.ps1",
|
||||
"submittedBy": "https://github.com/dinger1986",
|
||||
"name": "Defender Status",
|
||||
"description": "This will check for Malware, Antispyware, that Windows Defender is Healthy, last scan etc within the last 24 hours",
|
||||
"name": "Defender - Status Report",
|
||||
"description": "This will check for Malware and Antispyware within the last 24 hours and display, otherwise will report as Healthy. Command Parameter: (number) if provided will check that number of days back in the log.",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Security>Antivirus"
|
||||
},
|
||||
@@ -237,14 +304,23 @@
|
||||
"guid": "9956e936-6fdb-4488-a9d8-8b274658037f",
|
||||
"filename": "Win_Disable_Fast_Startup.bat",
|
||||
"submittedBy": "https://github.com/dinger1986",
|
||||
"name": "Disable Fast Startup",
|
||||
"name": "Power - Fast Startup Disable",
|
||||
"description": "Disables Faststartup on Windows 10",
|
||||
"shell": "cmd",
|
||||
"category": "TRMM (Win):Power"
|
||||
},
|
||||
{
|
||||
"guid": "f628a02b-16c3-4ab5-b788-dec5bc2af1d9",
|
||||
"filename": "Win_Power_Disable_Hibernation.bat",
|
||||
"submittedBy": "https://github.com/silversword411",
|
||||
"name": "Power - Hibernate Disable",
|
||||
"description": "Disables Hibernation",
|
||||
"shell": "cmd",
|
||||
"category": "TRMM (Win):Power"
|
||||
},
|
||||
{
|
||||
"guid": "2472bbaf-1941-4722-8a58-d1dd0f528801",
|
||||
"filename": "Win_Update_Tactical_Exclusion.ps1",
|
||||
"filename": "Win_TRMM_AV_Update_Exclusion.ps1",
|
||||
"submittedBy": "https://github.com/dinger1986",
|
||||
"name": "TRMM Defender Exclusions",
|
||||
"description": "Windows Defender Exclusions for Tactical RMM",
|
||||
@@ -255,7 +331,7 @@
|
||||
"guid": "b253dc76-41a0-48ca-9cea-bee4277402c4",
|
||||
"filename": "Win_Display_Message_To_User.ps1",
|
||||
"submittedBy": "https://github.com/bradhawkins85",
|
||||
"name": "Display Message To User",
|
||||
"name": "Message Popup To User",
|
||||
"description": "Displays a popup message to the currently logged on user",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Other"
|
||||
@@ -264,7 +340,7 @@
|
||||
"guid": "19224d21-bd39-44bc-b9cf-8f1ba3ca9c11",
|
||||
"filename": "Win_Antivirus_Verify.ps1",
|
||||
"submittedBy": "https://github.com/beejayzed",
|
||||
"name": "Verify Antivirus Status",
|
||||
"name": "Antivirus - Verify Status",
|
||||
"description": "Verify and display status for all installed Antiviruses",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Security>Antivirus"
|
||||
@@ -273,7 +349,7 @@
|
||||
"guid": "f88c5c52-c6fe-44db-b727-b7912a4279ed",
|
||||
"filename": "Win_Create_All_User_Logon_Script.ps1",
|
||||
"submittedBy": "https://github.com/nr-plaxon",
|
||||
"name": "Create User Logon Script",
|
||||
"name": "Template Example - Create User Logon Script",
|
||||
"description": "Creates a powershell script that runs at logon of any user on the machine in the security context of the user.",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Other"
|
||||
@@ -282,29 +358,66 @@
|
||||
"guid": "5615aa90-0272-427b-8acf-0ca019612501",
|
||||
"filename": "Win_Chocolatey_Update_Installed.bat",
|
||||
"submittedBy": "https://github.com/silversword411",
|
||||
"name": "Chocolatey Update Installed Apps",
|
||||
"name": "Update Installed Apps",
|
||||
"description": "Update all apps that were installed using Chocolatey.",
|
||||
"shell": "cmd",
|
||||
"category": "TRMM (Win):3rd Party Software>Chocolatey"
|
||||
"category": "TRMM (Win):3rd Party Software>Chocolatey",
|
||||
"default_timeout": "3600"
|
||||
},
|
||||
{
|
||||
"guid": "fff8024d-d72e-4457-84fa-6c780f69a16f",
|
||||
"filename": "Win_AD_Check_And_Enable_AD_Recycle_Bin.ps1",
|
||||
"submittedBy": "https://github.com/silversword411",
|
||||
"name": "AD - Check and Enable AD Recycle Bin",
|
||||
"name": "ADDC - Check and Enable AD Recycle Bin",
|
||||
"description": "Only run on Domain Controllers, checks for Active Directory Recycle Bin and enables if not already enabled",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Active Directory"
|
||||
},
|
||||
{
|
||||
"guid": "3afd07c0-04fd-4b23-b5f2-88205c0744d4",
|
||||
"filename": "Win_User_Admins_Local_Disable.ps1",
|
||||
"submittedBy": "https://github.com/dinger1986",
|
||||
"name": "Local Administrators - Disables all local admins if joined to domain or AzureAD",
|
||||
"description": "Checks to see if computer is either joined to a AD domain or Azure AD. If it is, it disables all local admin accounts. If not joined to domain/AzureAD, leaves admin accounts in place",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):User Management"
|
||||
},
|
||||
{
|
||||
"guid": "71090fc4-faa6-460b-adb0-95d7863544e1",
|
||||
"filename": "Win_Check_Events_for_Bluescreens.ps1",
|
||||
"submittedBy": "https://github.com/dinger1986",
|
||||
"name": "Event Viewer - Check for Bluescreens",
|
||||
"description": "This will check for Bluescreen events on your system",
|
||||
"name": "Event Viewer - Bluescreen Notification",
|
||||
"description": "Event Viewer Monitor - Notify Bluescreen events on your system",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Monitoring"
|
||||
},
|
||||
{
|
||||
"guid": "8373846f-facc-49b9-9891-3a780a394c89",
|
||||
"filename": "Win_Local_User_Created_Monitor.ps1",
|
||||
"submittedBy": "https://github.com/dinger1986",
|
||||
"name": "Event Viewer - New User Notification",
|
||||
"description": "Event Viewer Monitor - Notify when new Local user is created",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Monitoring"
|
||||
},
|
||||
{
|
||||
"guid": "65e5cef1-8338-4180-a0bc-cd54e62de690",
|
||||
"filename": "Win_Task_Scheduler_New_Items_Monitor.ps1",
|
||||
"submittedBy": "https://github.com/dinger1986",
|
||||
"name": "Event Viewer - Task Scheduler New Item Notification",
|
||||
"description": "Event Viewer Monitor - Notify when new Task Scheduler item is created",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Monitoring"
|
||||
},
|
||||
{
|
||||
"guid": "08ca81f2-f044-4dfc-ad47-090b19b19d76",
|
||||
"filename": "Win_User_Logged_in_with_Temp_Profile.ps1",
|
||||
"submittedBy": "https://github.com/dinger1986",
|
||||
"name": "User Logged in with temp profile check",
|
||||
"description": "Check if users are logged in with a temp profile",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Other"
|
||||
},
|
||||
{
|
||||
"guid": "5d905886-9eb1-4129-8b81-a013f842eb24",
|
||||
"filename": "Win_Rename_Computer.ps1",
|
||||
@@ -317,35 +430,257 @@
|
||||
},
|
||||
{
|
||||
"guid": "f396dae2-c768-45c5-bd6c-176e56ed3614",
|
||||
"filename": "Win_Finish_updates_and_restart.ps1",
|
||||
"filename": "Win_Power_RestartorShutdown.ps1",
|
||||
"submittedBy": "https://github.com/tremor021",
|
||||
"name": "Finish updates and restart",
|
||||
"description": "Finish installing updates and restart PC",
|
||||
"name": "Power - Restart or Shutdown PC",
|
||||
"description": "Restart PC. Add parameter: shutdown if you want to shutdown computer",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Other"
|
||||
},
|
||||
{
|
||||
"guid": "63f89be0-a9c9-4c61-9b55-bce0b28b90b2",
|
||||
"filename": "Win_Finish_updates_and_shutdown.ps1",
|
||||
"submittedBy": "https://github.com/tremor021",
|
||||
"name": "Finish updates and shutdown",
|
||||
"description": "Finish installing updates and shutdown PC",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Other"
|
||||
"category": "TRMM (Win):Updates"
|
||||
},
|
||||
{
|
||||
"guid": "e09895d5-ca13-44a2-a38c-6e77c740f0e8",
|
||||
"filename": "ScreenConnectAIO.ps1",
|
||||
"filename": "Win_ScreenConnectAIO.ps1",
|
||||
"submittedBy": "https://github.com/bradhawkins85",
|
||||
"name": "ScreenConnect AIO",
|
||||
"description": "Install, Uninstall, Start and Stop ScreenConnect Access Agent",
|
||||
"args": [
|
||||
"-serviceName {{client.ScreenConnectService}}",
|
||||
"-url {{client.ScreenConnectInstaller}}",
|
||||
"-clientname {{client.name}}",
|
||||
"-sitename {{site.name}}",
|
||||
"-action install"
|
||||
],
|
||||
"default_timeout": "90",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):3rd Party Software"
|
||||
},
|
||||
{
|
||||
"guid": "3abbb62a-3757-492c-8979-b4fc6174845d",
|
||||
"filename": "Win_AutoRun_Disable.bat",
|
||||
"submittedBy": "https://github.com/silversword411",
|
||||
"name": "Autorun - Disable",
|
||||
"description": "Disable Autorun System Wide",
|
||||
"shell": "cmd",
|
||||
"category": "TRMM (Win):Other",
|
||||
"default_timeout": "30"
|
||||
},
|
||||
{
|
||||
"guid": "4a11877a-7555-494c-ac74-29d6df3c1989",
|
||||
"filename": "Win_Cortana_Disable.bat",
|
||||
"submittedBy": "https://github.com/silversword411",
|
||||
"name": "Cortana - Disable",
|
||||
"description": "Disable Cortana System Wide",
|
||||
"shell": "cmd",
|
||||
"category": "TRMM (Win):Other",
|
||||
"default_timeout": "30"
|
||||
},
|
||||
{
|
||||
"guid": "28ef1387-dd4f-4bab-b042-26250914e370",
|
||||
"filename": "Win_WOL_Enable_Status.ps1",
|
||||
"submittedBy": "https://github.com/silversword411",
|
||||
"name": "BROKEN Network WoL - Enable function",
|
||||
"description": "Wake on Lan enable on Dell, HP, Lenovo",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Network",
|
||||
"default_timeout": "90"
|
||||
},
|
||||
{
|
||||
"guid": "685d5432-0b84-46d5-98e8-3ec2054150fe",
|
||||
"filename": "Win_WOL_Test_State.ps1",
|
||||
"submittedBy": "https://github.com/silversword411",
|
||||
"name": "BROKEN Network WoL - Test State",
|
||||
"description": "Wake on Lan test status",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Network",
|
||||
"default_timeout": "90"
|
||||
},
|
||||
{
|
||||
"guid": "abe78170-7cf9-435b-9666-c5ef6c11a106",
|
||||
"filename": "Win_Network_IPv6_Disable.ps1",
|
||||
"submittedBy": "https://github.com/silversword411",
|
||||
"name": "Network IPv6 - Disable",
|
||||
"description": "Disable IPv6 on all adapters",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Network",
|
||||
"default_timeout": "90"
|
||||
},
|
||||
{
|
||||
"guid": "745bb7cd-b71a-4f2e-b6f2-c579b1828162",
|
||||
"filename": "Win_Network_DHCP_Set.bat",
|
||||
"submittedBy": "https://github.com/silversword411",
|
||||
"name": "Network - Set Primary NIC to DHCP",
|
||||
"description": "Enable DHCP on primary adapter",
|
||||
"shell": "cmd",
|
||||
"category": "TRMM (Win):Network",
|
||||
"default_timeout": "90"
|
||||
},
|
||||
{
|
||||
"guid": "83aa4d51-63ce-41e7-829f-3c16e6115bbf",
|
||||
"filename": "Win_Network_DNS_Set_to_1.1.1.2.ps1",
|
||||
"submittedBy": "https://github.com/silversword411",
|
||||
"name": "Network - Set all NICs to use DNS of 1.1.1.2",
|
||||
"description": "Domain computers skipped. Sets all NICs to have primary DNS server of 1.1.1.2, backup of 1.0.0.2 (Cloudflare malware blocking)",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Network",
|
||||
"default_timeout": "90"
|
||||
},
|
||||
{
|
||||
"guid": "0caa33bc-89ca-47e0-ad4a-04626ae6384d",
|
||||
"filename": "Win_Network_TCP_Reset_Stack.bat",
|
||||
"submittedBy": "https://github.com/silversword411",
|
||||
"name": "Network - Reset tcp using netsh",
|
||||
"description": "resets tcp stack using netsh",
|
||||
"shell": "cmd",
|
||||
"category": "TRMM (Win):Network",
|
||||
"default_timeout": "120"
|
||||
},
|
||||
{
|
||||
"guid": "6ce5682a-49db-4c0b-9417-609cf905ac43",
|
||||
"filename": "Win_Win10_Change_Key_and_Activate.ps1",
|
||||
"submittedBy": "https://github.com/silversword411",
|
||||
"name": "Product Key in Win10 Change and Activate",
|
||||
"description": "Insert new product key and Activate. Requires 1 parameter the product key you want to use",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Other",
|
||||
"default_timeout": "90"
|
||||
},
|
||||
{
|
||||
"guid": "83f6c6ea-6120-4fd3-bec8-d3abc505dcdf",
|
||||
"filename": "Win_TRMM_Start_Menu_Delete_Shortcut.ps1",
|
||||
"submittedBy": "https://github.com/silversword411",
|
||||
"name": "TacticalRMM Delete Start Menu Shortcut for App",
|
||||
"description": "Delete its application shortcut that's installed in the start menu by default",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):TacticalRMM Related",
|
||||
"default_timeout": "10"
|
||||
},
|
||||
{
|
||||
"guid": "60130fca-7636-446e-acd7-cc5d29d609c2",
|
||||
"filename": "Win_Firewall_Check_Status.ps1",
|
||||
"submittedBy": "https://github.com/dinger1986",
|
||||
"name": "Windows Firewall - Check Status",
|
||||
"description": "Windows Firewall - Check state, report status",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Network"
|
||||
},
|
||||
{
|
||||
"guid": "93379675-c01c-433f-87df-a11597c959f0",
|
||||
"filename": "Win_UAC_Check_Status.ps1",
|
||||
"submittedBy": "https://github.com/dinger1986",
|
||||
"name": "Windows UAC - Check Status",
|
||||
"description": "Windows UAC - Report status",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Security"
|
||||
},
|
||||
{
|
||||
"guid": "7ea6a11a-05c0-4151-b5c1-cb8af029299f",
|
||||
"filename": "Win_AzureAD_Check_Connection_Status.ps1",
|
||||
"submittedBy": "https://github.com/dinger1986",
|
||||
"name": "Azure AD - Check Status",
|
||||
"description": "Azure AD - Check if joined or not",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Azure>AD"
|
||||
},
|
||||
{
|
||||
"guid": "7d81859a-1ba3-42b0-8664-29844f0dd765",
|
||||
"filename": "Win_Azure_Mars_Cloud_Backup_Status.ps1",
|
||||
"submittedBy": "https://github.com/dinger1986",
|
||||
"name": "Azure - Mars Cloud backup Status",
|
||||
"description": "Azure - Mars Cloud backup Check Status",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Azure>Backup"
|
||||
},
|
||||
{
|
||||
"guid": "e18c64d0-b783-4b52-b44b-9bb7592b439b",
|
||||
"filename": "Win_FileSystem_Enable_Long_Paths.bat",
|
||||
"submittedBy": "https://github.com/silversword411",
|
||||
"name": "File System - Enable Long Paths",
|
||||
"description": "Enables NTFS Long paths greater than 260 characters",
|
||||
"shell": "cmd",
|
||||
"category": "TRMM (Win):Storage"
|
||||
},
|
||||
{
|
||||
"guid": "c6252ca8-5172-42ea-9114-e447f80868f5",
|
||||
"filename": "Win_USB_Disable_Access.bat",
|
||||
"submittedBy": "https://github.com/silversword411",
|
||||
"name": "USB - Disable Access",
|
||||
"description": "USB - Disable Plugged in USB devices",
|
||||
"shell": "cmd",
|
||||
"category": "TRMM (Win):Storage"
|
||||
},
|
||||
{
|
||||
"guid": "3785952f-69fb-4bda-b2fe-5e3e8642738a",
|
||||
"filename": "Win_USB_Enable_Access.bat",
|
||||
"submittedBy": "https://github.com/silversword411",
|
||||
"name": "USB - Enable Access",
|
||||
"description": "USB - Enable Plugged in USB devices",
|
||||
"shell": "cmd",
|
||||
"category": "TRMM (Win):Storage"
|
||||
},
|
||||
{
|
||||
"guid": "c6014da2-b188-4e1b-b96a-e3440ade3a6a",
|
||||
"filename": "Win_RecycleBin_Empty.ps1",
|
||||
"submittedBy": "https://github.com/silversword411",
|
||||
"name": "File System - Empty Recycle Bin",
|
||||
"description": "Empty the recycle bin",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Storage"
|
||||
},
|
||||
{
|
||||
"guid": "57997ec7-b293-4fd5-9f90-a25426d0eb90",
|
||||
"filename": "Win_Get_Computer_Users.ps1",
|
||||
"submittedBy": "https://github.com/tremor021",
|
||||
"name": "Get Computer Users",
|
||||
"description": "Get list of computer users and show which one is enabled",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Other"
|
||||
},
|
||||
{
|
||||
"guid": "77da9c87-5a7a-4ba1-bdde-3eeb3b01d62d",
|
||||
"filename": "Win_Network_Set_To_Private.ps1",
|
||||
"submittedBy": "https://github.com/tremor021",
|
||||
"name": "Network Category - Set Network To Private",
|
||||
"description": "Sets current network type to Private",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Network"
|
||||
},
|
||||
{
|
||||
"guid": "768f42d5-7b45-45ed-8233-254ae537aaa2",
|
||||
"filename": "Win_TaskScheduler_Add_Task.ps1",
|
||||
"submittedBy": "https://github.com/tremor021",
|
||||
"name": "Task Scheduler - Add a task",
|
||||
"description": "Add a task to Task Scheduler, needs editing",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Other"
|
||||
},
|
||||
{
|
||||
"guid": "e371f1c6-0dd9-44de-824c-a17e1ca4c4ab",
|
||||
"filename": "Win_Outlook_SentItems_To_Delegated_Folders.ps1",
|
||||
"submittedBy": "https://github.com/dinger1986",
|
||||
"name": "Outlook - Delegated folders set for all profiles",
|
||||
"description": "Uses RunAsUser to setup sent items for the currently logged on user on delegated folders to go into the delegated folders sent for all.",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Office",
|
||||
"default_timeout": "90"
|
||||
},
|
||||
{
|
||||
"guid": "17040742-184a-4251-8f7b-4a1b0a1f02d1",
|
||||
"filename": "Win_File_Copy_Misc.ps1",
|
||||
"submittedBy": "https://github.com/tremor021",
|
||||
"name": "EXAMPLE File Copying using powershell",
|
||||
"description": "Reference Script: Will need manual tweaking, for copying files/folders from paths/websites to local",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Misc>Reference",
|
||||
"default_timeout": "1"
|
||||
},
|
||||
{
|
||||
"guid": "168037d8-78e6-4a6a-a9a9-8ec2c1dbe949",
|
||||
"filename": "Win_MSI_Install.ps1",
|
||||
"submittedBy": "https://github.com/silversword411",
|
||||
"name": "EXAMPLE Function for running MSI install via powershell",
|
||||
"description": "Reference Script: Will need manual tweaking, for running MSI from powershell",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Misc>Reference",
|
||||
"default_timeout": "1"
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
18
api/tacticalrmm/scripts/migrations/0008_script_guid.py
Normal file
18
api/tacticalrmm/scripts/migrations/0008_script_guid.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.7 on 2021-04-15 02:10
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('scripts', '0007_script_args'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='script',
|
||||
name='guid',
|
||||
field=models.CharField(blank=True, max_length=64, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,12 +1,14 @@
|
||||
import base64
|
||||
import re
|
||||
from loguru import logger
|
||||
from typing import Any, List, Union
|
||||
from typing import List, Optional
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.db import models
|
||||
from loguru import logger
|
||||
|
||||
from logs.models import BaseAuditModel
|
||||
from tacticalrmm.utils import replace_db_values
|
||||
|
||||
SCRIPT_SHELLS = [
|
||||
("powershell", "Powershell"),
|
||||
@@ -23,6 +25,7 @@ logger.configure(**settings.LOG_CONFIG)
|
||||
|
||||
|
||||
class Script(BaseAuditModel):
|
||||
guid = name = models.CharField(max_length=64, null=True, blank=True)
|
||||
name = models.CharField(max_length=255)
|
||||
description = models.TextField(null=True, blank=True)
|
||||
filename = models.CharField(max_length=255) # deprecated
|
||||
@@ -78,9 +81,7 @@ class Script(BaseAuditModel):
|
||||
|
||||
for script in info:
|
||||
if os.path.exists(os.path.join(scripts_dir, script["filename"])):
|
||||
s = cls.objects.filter(script_type="builtin").filter(
|
||||
name=script["name"]
|
||||
)
|
||||
s = cls.objects.filter(script_type="builtin", guid=script["guid"])
|
||||
|
||||
category = (
|
||||
script["category"] if "category" in script.keys() else "Community"
|
||||
@@ -120,6 +121,46 @@ class Script(BaseAuditModel):
|
||||
"args",
|
||||
]
|
||||
)
|
||||
|
||||
# check if script was added without a guid
|
||||
elif cls.objects.filter(
|
||||
script_type="builtin", name=script["name"]
|
||||
).exists():
|
||||
s = cls.objects.get(script_type="builtin", name=script["name"])
|
||||
|
||||
if not s.guid:
|
||||
print(f"Updating GUID for: {script['name']}")
|
||||
s.guid = script["guid"]
|
||||
s.name = script["name"]
|
||||
s.description = script["description"]
|
||||
s.category = category
|
||||
s.shell = script["shell"]
|
||||
s.default_timeout = default_timeout
|
||||
s.args = args
|
||||
|
||||
with open(
|
||||
os.path.join(scripts_dir, script["filename"]), "rb"
|
||||
) as f:
|
||||
script_bytes = (
|
||||
f.read().decode("utf-8").encode("ascii", "ignore")
|
||||
)
|
||||
s.code_base64 = base64.b64encode(script_bytes).decode(
|
||||
"ascii"
|
||||
)
|
||||
|
||||
s.save(
|
||||
update_fields=[
|
||||
"guid",
|
||||
"name",
|
||||
"description",
|
||||
"category",
|
||||
"default_timeout",
|
||||
"code_base64",
|
||||
"shell",
|
||||
"args",
|
||||
]
|
||||
)
|
||||
|
||||
else:
|
||||
print(f"Adding new community script: {script['name']}")
|
||||
|
||||
@@ -131,6 +172,7 @@ class Script(BaseAuditModel):
|
||||
|
||||
cls(
|
||||
code_base64=code_base64,
|
||||
guid=script["guid"],
|
||||
name=script["name"],
|
||||
description=script["description"],
|
||||
filename=script["filename"],
|
||||
@@ -141,6 +183,9 @@ class Script(BaseAuditModel):
|
||||
args=args,
|
||||
).save()
|
||||
|
||||
# delete community scripts that had their name changed
|
||||
cls.objects.filter(script_type="builtin", guid=None).delete()
|
||||
|
||||
@staticmethod
|
||||
def serialize(script):
|
||||
# serializes the script and returns json
|
||||
@@ -149,12 +194,9 @@ class Script(BaseAuditModel):
|
||||
return ScriptSerializer(script).data
|
||||
|
||||
@classmethod
|
||||
def parse_script_args(
|
||||
cls, agent, shell: str, args: List[str] = list()
|
||||
) -> Union[List[str], None]:
|
||||
from core.models import CustomField
|
||||
def parse_script_args(cls, agent, shell: str, args: List[str] = list()) -> list:
|
||||
|
||||
if not list:
|
||||
if not args:
|
||||
return []
|
||||
|
||||
temp_args = list()
|
||||
@@ -167,88 +209,15 @@ class Script(BaseAuditModel):
|
||||
if match:
|
||||
# only get the match between the () in regex
|
||||
string = match.group(1)
|
||||
value = replace_db_values(string=string, agent=agent, shell=shell)
|
||||
|
||||
# split by period if exists. First should be model and second should be property
|
||||
temp = string.split(".")
|
||||
|
||||
# check for model and property
|
||||
if len(temp) != 2:
|
||||
# ignore arg since it is invalid
|
||||
continue
|
||||
|
||||
if temp[0] == "client":
|
||||
model = "client"
|
||||
obj = agent.client
|
||||
elif temp[0] == "site":
|
||||
model = "site"
|
||||
obj = agent.site
|
||||
elif temp[0] == "agent":
|
||||
model = "agent"
|
||||
obj = agent
|
||||
if value:
|
||||
temp_args.append(re.sub("\\{\\{.*\\}\\}", value, arg))
|
||||
else:
|
||||
# ignore arg since it is invalid
|
||||
continue
|
||||
|
||||
if hasattr(obj, temp[1]):
|
||||
value = getattr(obj, temp[1])
|
||||
|
||||
elif CustomField.objects.filter(model=model, name=temp[1]).exists():
|
||||
|
||||
field = CustomField.objects.get(model=model, name=temp[1])
|
||||
model_fields = getattr(field, f"{model}_fields")
|
||||
value = None
|
||||
if model_fields.filter(**{model: obj}).exists():
|
||||
value = model_fields.get(**{model: obj}).value
|
||||
|
||||
if not value and field.default_value:
|
||||
value = field.default_value
|
||||
|
||||
# check if value exists and if not use defa
|
||||
if value and field.type == "multiple":
|
||||
value = format_shell_array(shell, value)
|
||||
elif value and field.type == "checkbox":
|
||||
value = format_shell_bool(shell, value)
|
||||
|
||||
if not value:
|
||||
continue
|
||||
|
||||
else:
|
||||
# ignore arg since property is invalid
|
||||
continue
|
||||
|
||||
# replace the value in the arg and push to array
|
||||
# log any unhashable type errors
|
||||
try:
|
||||
temp_args.append(re.sub("\\{\\{.*\\}\\}", value, arg)) # type: ignore
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
continue
|
||||
# pass parameter unaltered
|
||||
temp_args.append(arg)
|
||||
|
||||
else:
|
||||
temp_args.append(arg)
|
||||
|
||||
return temp_args
|
||||
|
||||
|
||||
def format_shell_array(shell: str, value: Any) -> str:
|
||||
if shell == "cmd":
|
||||
return "array args are not supported with batch"
|
||||
elif shell == "powershell":
|
||||
temp_string = ""
|
||||
for item in value:
|
||||
temp_string += item + ","
|
||||
return temp_string.strip(",")
|
||||
else: # python
|
||||
temp_string = ""
|
||||
for item in value:
|
||||
temp_string += item + ","
|
||||
return temp_string.strip(",")
|
||||
|
||||
|
||||
def format_shell_bool(shell: str, value: Any) -> str:
|
||||
if shell == "cmd":
|
||||
return "1" if value else "0"
|
||||
elif shell == "powershell":
|
||||
return "$True" if value else "$False"
|
||||
else: # python
|
||||
return "True" if value else "False"
|
||||
|
||||
11
api/tacticalrmm/scripts/permissions.py
Normal file
11
api/tacticalrmm/scripts/permissions.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
from tacticalrmm.permissions import _has_perm
|
||||
|
||||
|
||||
class ManageScriptsPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
if r.method == "GET":
|
||||
return True
|
||||
|
||||
return _has_perm(r, "can_manage_scripts")
|
||||
@@ -1,4 +1,3 @@
|
||||
from email.policy import default
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
@@ -15,6 +14,7 @@ from .serializers import ScriptSerializer, ScriptTableSerializer
|
||||
|
||||
class TestScriptViews(TacticalTestCase):
|
||||
def setUp(self):
|
||||
self.setup_coresettings()
|
||||
self.authenticate()
|
||||
|
||||
def test_get_scripts(self):
|
||||
@@ -245,12 +245,39 @@ class TestScriptViews(TacticalTestCase):
|
||||
|
||||
Script.load_community_scripts()
|
||||
|
||||
community_scripts = Script.objects.filter(script_type="builtin").count()
|
||||
self.assertEqual(len(info), community_scripts)
|
||||
community_scripts_count = Script.objects.filter(script_type="builtin").count()
|
||||
if len(info) != community_scripts_count:
|
||||
raise Exception(
|
||||
f"There are {len(info)} scripts in json file but only {community_scripts_count} in database"
|
||||
)
|
||||
|
||||
# test updating already added community scripts
|
||||
Script.load_community_scripts()
|
||||
self.assertEqual(len(info), community_scripts)
|
||||
community_scripts_count2 = Script.objects.filter(script_type="builtin").count()
|
||||
self.assertEqual(len(info), community_scripts_count2)
|
||||
|
||||
def test_community_script_has_jsonfile_entry(self):
|
||||
with open(
|
||||
os.path.join(settings.BASE_DIR, "scripts/community_scripts.json")
|
||||
) as f:
|
||||
info = json.load(f)
|
||||
|
||||
filenames = [i["filename"] for i in info]
|
||||
|
||||
# normal
|
||||
if not settings.DOCKER_BUILD:
|
||||
scripts_dir = os.path.join(Path(settings.BASE_DIR).parents[1], "scripts")
|
||||
# docker
|
||||
else:
|
||||
scripts_dir = settings.SCRIPTS_DIR
|
||||
|
||||
with os.scandir(scripts_dir) as it:
|
||||
for f in it:
|
||||
if not f.name.startswith(".") and f.is_file():
|
||||
if f.name not in filenames:
|
||||
raise Exception(
|
||||
f"{f.name} is missing an entry in community_scripts.json"
|
||||
)
|
||||
|
||||
def test_script_filenames_do_not_contain_spaces(self):
|
||||
with open(
|
||||
@@ -259,4 +286,214 @@ class TestScriptViews(TacticalTestCase):
|
||||
info = json.load(f)
|
||||
for script in info:
|
||||
fn: str = script["filename"]
|
||||
self.assertTrue(" " not in fn)
|
||||
if " " in fn:
|
||||
raise Exception(f"{fn} must not contain spaces in filename")
|
||||
|
||||
def test_script_arg_variable_replacement(self):
|
||||
|
||||
agent = baker.make_recipe("agents.agent", public_ip="12.12.12.12")
|
||||
args = [
|
||||
"-Parameter",
|
||||
"-Another {{agent.public_ip}}",
|
||||
"-Client {{client.name}}",
|
||||
"-Site {{site.name}}",
|
||||
]
|
||||
|
||||
self.assertEqual(
|
||||
[
|
||||
"-Parameter",
|
||||
"-Another '12.12.12.12'",
|
||||
f"-Client '{agent.client.name}'",
|
||||
f"-Site '{agent.site.name}'",
|
||||
],
|
||||
Script.parse_script_args(agent=agent, shell="python", args=args),
|
||||
)
|
||||
|
||||
def test_script_arg_replacement_custom_field(self):
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
field = baker.make(
|
||||
"core.CustomField",
|
||||
name="Test Field",
|
||||
model="agent",
|
||||
type="text",
|
||||
default_value_string="DEFAULT",
|
||||
)
|
||||
|
||||
args = ["-Parameter", "-Another {{agent.Test Field}}"]
|
||||
|
||||
# test default value
|
||||
self.assertEqual(
|
||||
["-Parameter", "-Another 'DEFAULT'"],
|
||||
Script.parse_script_args(agent=agent, shell="python", args=args),
|
||||
)
|
||||
|
||||
# test with set value
|
||||
baker.make(
|
||||
"agents.AgentCustomField",
|
||||
field=field,
|
||||
agent=agent,
|
||||
string_value="CUSTOM VALUE",
|
||||
)
|
||||
self.assertEqual(
|
||||
["-Parameter", "-Another 'CUSTOM VALUE'"],
|
||||
Script.parse_script_args(agent=agent, shell="python", args=args),
|
||||
)
|
||||
|
||||
def test_script_arg_replacement_client_custom_fields(self):
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
field = baker.make(
|
||||
"core.CustomField",
|
||||
name="Test Field",
|
||||
model="client",
|
||||
type="text",
|
||||
default_value_string="DEFAULT",
|
||||
)
|
||||
|
||||
args = ["-Parameter", "-Another {{client.Test Field}}"]
|
||||
|
||||
# test default value
|
||||
self.assertEqual(
|
||||
["-Parameter", "-Another 'DEFAULT'"],
|
||||
Script.parse_script_args(agent=agent, shell="python", args=args),
|
||||
)
|
||||
|
||||
# test with set value
|
||||
baker.make(
|
||||
"clients.ClientCustomField",
|
||||
field=field,
|
||||
client=agent.client,
|
||||
string_value="CUSTOM VALUE",
|
||||
)
|
||||
self.assertEqual(
|
||||
["-Parameter", "-Another 'CUSTOM VALUE'"],
|
||||
Script.parse_script_args(agent=agent, shell="python", args=args),
|
||||
)
|
||||
|
||||
def test_script_arg_replacement_site_custom_fields(self):
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
field = baker.make(
|
||||
"core.CustomField",
|
||||
name="Test Field",
|
||||
model="site",
|
||||
type="text",
|
||||
default_value_string="DEFAULT",
|
||||
)
|
||||
|
||||
args = ["-Parameter", "-Another {{site.Test Field}}"]
|
||||
|
||||
# test default value
|
||||
self.assertEqual(
|
||||
["-Parameter", "-Another 'DEFAULT'"],
|
||||
Script.parse_script_args(agent=agent, shell="python", args=args),
|
||||
)
|
||||
|
||||
# test with set value
|
||||
value = baker.make(
|
||||
"clients.SiteCustomField",
|
||||
field=field,
|
||||
site=agent.site,
|
||||
string_value="CUSTOM VALUE",
|
||||
)
|
||||
self.assertEqual(
|
||||
["-Parameter", "-Another 'CUSTOM VALUE'"],
|
||||
Script.parse_script_args(agent=agent, shell="python", args=args),
|
||||
)
|
||||
|
||||
# test with set but empty field value
|
||||
value.string_value = "" # type: ignore
|
||||
value.save() # type: ignore
|
||||
|
||||
self.assertEqual(
|
||||
["-Parameter", "-Another 'DEFAULT'"],
|
||||
Script.parse_script_args(agent=agent, shell="python", args=args),
|
||||
)
|
||||
|
||||
# test blank default and value
|
||||
field.default_value_string = "" # type: ignore
|
||||
field.save() # type: ignore
|
||||
|
||||
self.assertEqual(
|
||||
["-Parameter", "-Another ''"],
|
||||
Script.parse_script_args(agent=agent, shell="python", args=args),
|
||||
)
|
||||
|
||||
def test_script_arg_replacement_array_fields(self):
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
field = baker.make(
|
||||
"core.CustomField",
|
||||
name="Test Field",
|
||||
model="agent",
|
||||
type="multiple",
|
||||
default_values_multiple=["this", "is", "an", "array"],
|
||||
)
|
||||
|
||||
args = ["-Parameter", "-Another {{agent.Test Field}}"]
|
||||
|
||||
# test default value
|
||||
self.assertEqual(
|
||||
["-Parameter", "-Another 'this,is,an,array'"],
|
||||
Script.parse_script_args(agent=agent, shell="python", args=args),
|
||||
)
|
||||
|
||||
# test with set value and python shell
|
||||
baker.make(
|
||||
"agents.AgentCustomField",
|
||||
field=field,
|
||||
agent=agent,
|
||||
multiple_value=["this", "is", "new"],
|
||||
)
|
||||
self.assertEqual(
|
||||
["-Parameter", "-Another 'this,is,new'"],
|
||||
Script.parse_script_args(agent=agent, shell="python", args=args),
|
||||
)
|
||||
|
||||
def test_script_arg_replacement_boolean_fields(self):
|
||||
agent = baker.make_recipe("agents.agent")
|
||||
field = baker.make(
|
||||
"core.CustomField",
|
||||
name="Test Field",
|
||||
model="agent",
|
||||
type="checkbox",
|
||||
default_value_bool=True,
|
||||
)
|
||||
|
||||
args = ["-Parameter", "-Another {{agent.Test Field}}"]
|
||||
|
||||
# test default value with python
|
||||
self.assertEqual(
|
||||
["-Parameter", "-Another 1"],
|
||||
Script.parse_script_args(agent=agent, shell="python", args=args),
|
||||
)
|
||||
|
||||
# test with set value and python shell
|
||||
custom = baker.make(
|
||||
"agents.AgentCustomField",
|
||||
field=field,
|
||||
agent=agent,
|
||||
bool_value=False,
|
||||
)
|
||||
self.assertEqual(
|
||||
["-Parameter", "-Another 0"],
|
||||
Script.parse_script_args(agent=agent, shell="python", args=args),
|
||||
)
|
||||
|
||||
# test with set value and cmd shell
|
||||
self.assertEqual(
|
||||
["-Parameter", "-Another 0"],
|
||||
Script.parse_script_args(agent=agent, shell="cmd", args=args),
|
||||
)
|
||||
|
||||
# test with set value and powershell
|
||||
self.assertEqual(
|
||||
["-Parameter", "-Another $False"],
|
||||
Script.parse_script_args(agent=agent, shell="powershell", args=args),
|
||||
)
|
||||
|
||||
# test with True value powershell
|
||||
custom.bool_value = True # type: ignore
|
||||
custom.save() # type: ignore
|
||||
|
||||
self.assertEqual(
|
||||
["-Parameter", "-Another $True"],
|
||||
Script.parse_script_args(agent=agent, shell="powershell", args=args),
|
||||
)
|
||||
|
||||
@@ -4,20 +4,23 @@ import json
|
||||
from django.conf import settings
|
||||
from django.shortcuts import get_object_or_404
|
||||
from loguru import logger
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.parsers import FileUploadParser
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from tacticalrmm.utils import notify_error
|
||||
|
||||
from .models import Script
|
||||
from .permissions import ManageScriptsPerms
|
||||
from .serializers import ScriptSerializer, ScriptTableSerializer
|
||||
|
||||
logger.configure(**settings.LOG_CONFIG)
|
||||
|
||||
|
||||
class GetAddScripts(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageScriptsPerms]
|
||||
parser_class = (FileUploadParser,)
|
||||
|
||||
def get(self, request):
|
||||
@@ -63,6 +66,8 @@ class GetAddScripts(APIView):
|
||||
|
||||
|
||||
class GetUpdateDeleteScript(APIView):
|
||||
permission_classes = [IsAuthenticated, ManageScriptsPerms]
|
||||
|
||||
def get(self, request, pk):
|
||||
script = get_object_or_404(Script, pk=pk)
|
||||
return Response(ScriptSerializer(script).data)
|
||||
@@ -103,6 +108,7 @@ class GetUpdateDeleteScript(APIView):
|
||||
|
||||
|
||||
@api_view()
|
||||
@permission_classes([IsAuthenticated, ManageScriptsPerms])
|
||||
def download(request, pk):
|
||||
script = get_object_or_404(Script, pk=pk)
|
||||
|
||||
|
||||
8
api/tacticalrmm/services/permissions.py
Normal file
8
api/tacticalrmm/services/permissions.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
from tacticalrmm.permissions import _has_perm
|
||||
|
||||
|
||||
class ManageWinSvcsPerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_manage_winsvcs")
|
||||
@@ -3,13 +3,15 @@ import asyncio
|
||||
from django.conf import settings
|
||||
from django.shortcuts import get_object_or_404
|
||||
from loguru import logger
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
||||
from agents.models import Agent
|
||||
from checks.models import Check
|
||||
from tacticalrmm.utils import notify_error
|
||||
|
||||
from .permissions import ManageWinSvcsPerms
|
||||
from .serializers import ServicesSerializer
|
||||
|
||||
logger.configure(**settings.LOG_CONFIG)
|
||||
@@ -34,6 +36,7 @@ def default_services(request):
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
@permission_classes([IsAuthenticated, ManageWinSvcsPerms])
|
||||
def service_action(request):
|
||||
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
||||
action = request.data["sv_action"]
|
||||
@@ -85,6 +88,7 @@ def service_detail(request, pk, svcname):
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
@permission_classes([IsAuthenticated, ManageWinSvcsPerms])
|
||||
def edit_service(request):
|
||||
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
||||
data = {
|
||||
|
||||
11
api/tacticalrmm/software/permissions.py
Normal file
11
api/tacticalrmm/software/permissions.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
from tacticalrmm.permissions import _has_perm
|
||||
|
||||
|
||||
class ManageSoftwarePerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
if r.method == "GET":
|
||||
return True
|
||||
|
||||
return _has_perm(r, "can_manage_software")
|
||||
@@ -3,7 +3,8 @@ from typing import Any
|
||||
|
||||
from django.shortcuts import get_object_or_404
|
||||
from packaging import version as pyver
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
||||
from agents.models import Agent
|
||||
@@ -11,6 +12,7 @@ from logs.models import PendingAction
|
||||
from tacticalrmm.utils import filter_software, notify_error
|
||||
|
||||
from .models import ChocoSoftware, InstalledSoftware
|
||||
from .permissions import ManageSoftwarePerms
|
||||
from .serializers import InstalledSoftwareSerializer
|
||||
|
||||
|
||||
@@ -20,6 +22,7 @@ def chocos(request):
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
@permission_classes([IsAuthenticated, ManageSoftwarePerms])
|
||||
def install(request):
|
||||
agent = get_object_or_404(Agent, pk=request.data["pk"])
|
||||
if pyver.parse(agent.version) < pyver.parse("1.4.8"):
|
||||
|
||||
@@ -41,7 +41,7 @@ app.conf.beat_schedule = {
|
||||
},
|
||||
"get-wmi": {
|
||||
"task": "agents.tasks.get_wmi_task",
|
||||
"schedule": crontab(minute="*/18"),
|
||||
"schedule": crontab(minute=18, hour="*/5"),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
7
api/tacticalrmm/tacticalrmm/permissions.py
Normal file
7
api/tacticalrmm/tacticalrmm/permissions.py
Normal file
@@ -0,0 +1,7 @@
|
||||
def _has_perm(request, perm):
|
||||
if request.user.is_superuser or (
|
||||
request.user.role and getattr(request.user.role, "is_superuser")
|
||||
):
|
||||
return True
|
||||
|
||||
return request.user.role and getattr(request.user.role, perm)
|
||||
@@ -15,33 +15,42 @@ EXE_DIR = os.path.join(BASE_DIR, "tacticalrmm/private/exe")
|
||||
AUTH_USER_MODEL = "accounts.User"
|
||||
|
||||
# latest release
|
||||
TRMM_VERSION = "0.5.1"
|
||||
TRMM_VERSION = "0.6.14"
|
||||
|
||||
# bump this version everytime vue code is changed
|
||||
# to alert user they need to manually refresh their browser
|
||||
APP_VER = "0.0.127"
|
||||
APP_VER = "0.0.138"
|
||||
|
||||
# https://github.com/wh1te909/rmmagent
|
||||
LATEST_AGENT_VER = "1.4.13"
|
||||
LATEST_AGENT_VER = "1.5.8"
|
||||
|
||||
MESH_VER = "0.8.5"
|
||||
MESH_VER = "0.8.60"
|
||||
|
||||
# for the update script, bump when need to recreate venv or npm install
|
||||
PIP_VER = "15"
|
||||
NPM_VER = "14"
|
||||
PIP_VER = "18"
|
||||
NPM_VER = "17"
|
||||
|
||||
SETUPTOOLS_VER = "57.0.0"
|
||||
WHEEL_VER = "0.36.2"
|
||||
|
||||
DL_64 = f"https://github.com/wh1te909/rmmagent/releases/download/v{LATEST_AGENT_VER}/winagent-v{LATEST_AGENT_VER}.exe"
|
||||
DL_32 = f"https://github.com/wh1te909/rmmagent/releases/download/v{LATEST_AGENT_VER}/winagent-v{LATEST_AGENT_VER}-x86.exe"
|
||||
|
||||
EXE_GEN_URLS = [
|
||||
"https://exe2.tacticalrmm.io/api/v1/exe",
|
||||
"https://exe.tacticalrmm.io/api/v1/exe",
|
||||
"https://exe2.tacticalrmm.io",
|
||||
"https://exe.tacticalrmm.io",
|
||||
]
|
||||
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
|
||||
|
||||
ASGI_APPLICATION = "tacticalrmm.asgi.application"
|
||||
|
||||
REST_KNOX = {
|
||||
"TOKEN_TTL": timedelta(hours=5),
|
||||
"AUTO_REFRESH": True,
|
||||
"MIN_REFRESH_INTERVAL": 600,
|
||||
}
|
||||
|
||||
try:
|
||||
from .local_settings import *
|
||||
except ImportError:
|
||||
@@ -77,6 +86,15 @@ if not "AZPIPELINE" in os.environ:
|
||||
if DEBUG: # type: ignore
|
||||
INSTALLED_APPS += ("django_extensions",)
|
||||
|
||||
CHANNEL_LAYERS = {
|
||||
"default": {
|
||||
"BACKEND": "channels_redis.core.RedisChannelLayer",
|
||||
"CONFIG": {
|
||||
"hosts": [(REDIS_HOST, 6379)], # type: ignore
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if "AZPIPELINE" in os.environ:
|
||||
ADMIN_ENABLED = False
|
||||
|
||||
@@ -101,11 +119,6 @@ MIDDLEWARE = [
|
||||
if ADMIN_ENABLED: # type: ignore
|
||||
MIDDLEWARE += ("django.contrib.messages.middleware.MessageMiddleware",)
|
||||
|
||||
REST_KNOX = {
|
||||
"TOKEN_TTL": timedelta(hours=5),
|
||||
"AUTO_REFRESH": True,
|
||||
"MIN_REFRESH_INTERVAL": 600,
|
||||
}
|
||||
|
||||
ROOT_URLCONF = "tacticalrmm.urls"
|
||||
|
||||
|
||||
@@ -10,9 +10,11 @@ from core.models import CoreSettings
|
||||
class TacticalTestCase(TestCase):
|
||||
def authenticate(self):
|
||||
self.john = User(username="john")
|
||||
self.john.is_superuser = True
|
||||
self.john.set_password("hunter2")
|
||||
self.john.save()
|
||||
self.alice = User(username="alice")
|
||||
self.alice.is_superuser = True
|
||||
self.alice.set_password("hunter2")
|
||||
self.alice.save()
|
||||
self.client_setup()
|
||||
@@ -20,7 +22,8 @@ class TacticalTestCase(TestCase):
|
||||
|
||||
def setup_agent_auth(self, agent):
|
||||
agent_user = User.objects.create_user(
|
||||
username=agent.agent_id, password=User.objects.make_random_password(60)
|
||||
username=agent.agent_id,
|
||||
password=User.objects.make_random_password(60),
|
||||
)
|
||||
Token.objects.create(user=agent_user)
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ from .utils import (
|
||||
generate_winagent_exe,
|
||||
get_bit_days,
|
||||
reload_nats,
|
||||
run_nats_api_cmd,
|
||||
)
|
||||
|
||||
|
||||
@@ -65,8 +66,7 @@ class TestUtils(TestCase):
|
||||
mock_subprocess.assert_not_called()
|
||||
|
||||
@override_settings(
|
||||
ALLOWED_HOSTS=["api.example.com"],
|
||||
SECRET_KEY="sekret",
|
||||
ALLOWED_HOSTS=["api.example.com"], SECRET_KEY="sekret", DOCKER_BUILD=False
|
||||
)
|
||||
@patch("subprocess.run")
|
||||
def test_reload_nats(self, mock_subprocess):
|
||||
@@ -74,6 +74,12 @@ class TestUtils(TestCase):
|
||||
|
||||
mock_subprocess.assert_called_once()
|
||||
|
||||
@patch("subprocess.run")
|
||||
def test_run_nats_api_cmd(self, mock_subprocess):
|
||||
ids = ["a", "b", "c"]
|
||||
_ = run_nats_api_cmd("monitor", ids)
|
||||
mock_subprocess.assert_called_once()
|
||||
|
||||
def test_bitdays_to_string(self):
|
||||
a = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
|
||||
all_days = [
|
||||
|
||||
@@ -3,7 +3,7 @@ from django.urls import include, path
|
||||
from knox import views as knox_views
|
||||
|
||||
from accounts.views import CheckCreds, LoginView
|
||||
from core import consumers
|
||||
from core.consumers import DashInfo
|
||||
|
||||
urlpatterns = [
|
||||
path("checkcreds/", CheckCreds.as_view()),
|
||||
@@ -32,5 +32,5 @@ if hasattr(settings, "ADMIN_ENABLED") and settings.ADMIN_ENABLED:
|
||||
urlpatterns += (path(settings.ADMIN_URL, admin.site.urls),)
|
||||
|
||||
ws_urlpatterns = [
|
||||
path("ws/dashinfo/", consumers.DashInfo.as_asgi()), # type: ignore
|
||||
path("ws/dashinfo/", DashInfo.as_asgi()), # type: ignore
|
||||
]
|
||||
|
||||
@@ -4,7 +4,8 @@ import string
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
from typing import Union
|
||||
import urllib.parse
|
||||
from typing import Optional, Union
|
||||
|
||||
import pytz
|
||||
import requests
|
||||
@@ -19,6 +20,7 @@ from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
|
||||
from agents.models import Agent
|
||||
from core.models import CodeSignToken
|
||||
|
||||
logger.configure(**settings.LOG_CONFIG)
|
||||
|
||||
@@ -50,12 +52,27 @@ def generate_winagent_exe(
|
||||
file_name: str,
|
||||
) -> Union[Response, FileResponse]:
|
||||
|
||||
from agents.utils import get_exegen_url
|
||||
|
||||
inno = (
|
||||
f"winagent-v{settings.LATEST_AGENT_VER}.exe"
|
||||
if arch == "64"
|
||||
else f"winagent-v{settings.LATEST_AGENT_VER}-x86.exe"
|
||||
)
|
||||
|
||||
try:
|
||||
codetoken = CodeSignToken.objects.first().token
|
||||
base_url = get_exegen_url() + "/api/v1/winagents/?"
|
||||
params = {
|
||||
"version": settings.LATEST_AGENT_VER,
|
||||
"arch": arch,
|
||||
"token": codetoken,
|
||||
}
|
||||
dl_url = base_url + urllib.parse.urlencode(params)
|
||||
except:
|
||||
codetoken = ""
|
||||
dl_url = settings.DL_64 if arch == "64" else settings.DL_32
|
||||
|
||||
data = {
|
||||
"client": client,
|
||||
"site": site,
|
||||
@@ -66,8 +83,9 @@ def generate_winagent_exe(
|
||||
"goarch": "amd64" if arch == "64" else "386",
|
||||
"token": token,
|
||||
"inno": inno,
|
||||
"url": settings.DL_64 if arch == "64" else settings.DL_32,
|
||||
"url": dl_url,
|
||||
"api": api,
|
||||
"codesigntoken": codetoken,
|
||||
}
|
||||
headers = {"Content-type": "application/json"}
|
||||
|
||||
@@ -76,7 +94,7 @@ def generate_winagent_exe(
|
||||
for url in settings.EXE_GEN_URLS:
|
||||
try:
|
||||
r = requests.post(
|
||||
url,
|
||||
f"{url}/api/v1/exe",
|
||||
json=data,
|
||||
headers=headers,
|
||||
stream=True,
|
||||
@@ -228,3 +246,143 @@ class KnoxAuthMiddlewareInstance:
|
||||
KnoxAuthMiddlewareStack = lambda inner: KnoxAuthMiddlewareInstance(
|
||||
AuthMiddlewareStack(inner)
|
||||
)
|
||||
|
||||
|
||||
def run_nats_api_cmd(mode: str, ids: list[str], timeout: int = 30) -> None:
|
||||
config = {
|
||||
"key": settings.SECRET_KEY,
|
||||
"natsurl": f"tls://{settings.ALLOWED_HOSTS[0]}:4222",
|
||||
"agents": ids,
|
||||
}
|
||||
with tempfile.NamedTemporaryFile() as fp:
|
||||
with open(fp.name, "w") as f:
|
||||
json.dump(config, f)
|
||||
|
||||
cmd = ["/usr/local/bin/nats-api", "-c", fp.name, "-m", mode]
|
||||
try:
|
||||
subprocess.run(cmd, capture_output=True, timeout=timeout)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
|
||||
def get_latest_trmm_ver() -> str:
|
||||
url = "https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/api/tacticalrmm/tacticalrmm/settings.py"
|
||||
try:
|
||||
r = requests.get(url, timeout=5)
|
||||
except:
|
||||
return "error"
|
||||
|
||||
try:
|
||||
for line in r.text.splitlines():
|
||||
if "TRMM_VERSION" in line:
|
||||
return line.split(" ")[2].strip('"')
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
return "error"
|
||||
|
||||
|
||||
def replace_db_values(
|
||||
string: str, agent: Agent = None, shell: str = None, quotes=True
|
||||
) -> Union[str, None]:
|
||||
from core.models import CustomField, GlobalKVStore
|
||||
|
||||
# split by period if exists. First should be model and second should be property i.e {{client.name}}
|
||||
temp = string.split(".")
|
||||
|
||||
# check for model and property
|
||||
if len(temp) < 2:
|
||||
# ignore arg since it is invalid
|
||||
return None
|
||||
|
||||
# value is in the global keystore and replace value
|
||||
if temp[0] == "global":
|
||||
if GlobalKVStore.objects.filter(name=temp[1]).exists():
|
||||
value = GlobalKVStore.objects.get(name=temp[1]).value
|
||||
|
||||
return f"'{value}'" if quotes else value
|
||||
else:
|
||||
logger.error(
|
||||
f"Couldn't lookup value for: {string}. Make sure it exists in CoreSettings > Key Store"
|
||||
)
|
||||
return None
|
||||
|
||||
if not agent:
|
||||
# agent must be set if not global property
|
||||
return f"There was an error finding the agent: {agent}"
|
||||
|
||||
if temp[0] == "client":
|
||||
model = "client"
|
||||
obj = agent.client
|
||||
elif temp[0] == "site":
|
||||
model = "site"
|
||||
obj = agent.site
|
||||
elif temp[0] == "agent":
|
||||
model = "agent"
|
||||
obj = agent
|
||||
else:
|
||||
# ignore arg since it is invalid
|
||||
logger.error(
|
||||
f"Not enough information to find value for: {string}. Only agent, site, client, and global are supported."
|
||||
)
|
||||
return None
|
||||
|
||||
if hasattr(obj, temp[1]):
|
||||
value = f"'{getattr(obj, temp[1])}'" if quotes else getattr(obj, temp[1])
|
||||
|
||||
elif CustomField.objects.filter(model=model, name=temp[1]).exists():
|
||||
|
||||
field = CustomField.objects.get(model=model, name=temp[1])
|
||||
model_fields = getattr(field, f"{model}_fields")
|
||||
value = None
|
||||
if model_fields.filter(**{model: obj}).exists():
|
||||
if field.type != "checkbox" and model_fields.get(**{model: obj}).value:
|
||||
value = model_fields.get(**{model: obj}).value
|
||||
elif field.type == "checkbox":
|
||||
value = model_fields.get(**{model: obj}).value
|
||||
|
||||
# need explicit None check since a false boolean value will pass default value
|
||||
if value == None and field.default_value != None:
|
||||
value = field.default_value
|
||||
|
||||
# check if value exists and if not use default
|
||||
if value and field.type == "multiple":
|
||||
value = (
|
||||
f"'{format_shell_array(value)}'"
|
||||
if quotes
|
||||
else format_shell_array(value)
|
||||
)
|
||||
elif value != None and field.type == "checkbox":
|
||||
value = format_shell_bool(value, shell)
|
||||
else:
|
||||
value = f"'{value}'" if quotes else value
|
||||
|
||||
else:
|
||||
# ignore arg since property is invalid
|
||||
logger.error(
|
||||
f"Couldn't find property on supplied variable: {string}. Make sure it exists as a custom field or a valid agent property"
|
||||
)
|
||||
return None
|
||||
|
||||
# log any unhashable type errors
|
||||
if value != None:
|
||||
return value # type: ignore
|
||||
else:
|
||||
logger.error(
|
||||
f"Couldn't lookup value for: {string}. Make sure it exists as a custom field or a valid agent property"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def format_shell_array(value: list) -> str:
|
||||
temp_string = ""
|
||||
for item in value:
|
||||
temp_string += item + ","
|
||||
return f"{temp_string.strip(',')}"
|
||||
|
||||
|
||||
def format_shell_bool(value: bool, shell: Optional[str]) -> str:
|
||||
if shell == "powershell":
|
||||
return "$True" if value else "$False"
|
||||
else:
|
||||
return "1" if value else "0"
|
||||
|
||||
8
api/tacticalrmm/winupdate/permissions.py
Normal file
8
api/tacticalrmm/winupdate/permissions.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
from tacticalrmm.permissions import _has_perm
|
||||
|
||||
|
||||
class ManageWinUpdatePerms(permissions.BasePermission):
|
||||
def has_permission(self, r, view):
|
||||
return _has_perm(r, "can_manage_winupdates")
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user