Compare commits
283 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 | ||
|
|
bf776eeb2b |
@@ -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,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"
|
||||
)
|
||||
@@ -47,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,6 +11,7 @@ class UserUISerializer(ModelSerializer):
|
||||
"dark_mode",
|
||||
"show_community_scripts",
|
||||
"agent_dblclick_action",
|
||||
"url_action",
|
||||
"default_agent_tbl_tab",
|
||||
"client_tree_sort",
|
||||
"client_tree_splitter",
|
||||
@@ -21,7 +22,7 @@ class UserUISerializer(ModelSerializer):
|
||||
class UserSerializer(ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = (
|
||||
fields = [
|
||||
"id",
|
||||
"username",
|
||||
"first_name",
|
||||
@@ -29,7 +30,8 @@ class UserSerializer(ModelSerializer):
|
||||
"email",
|
||||
"is_active",
|
||||
"last_login",
|
||||
)
|
||||
"role",
|
||||
]
|
||||
|
||||
|
||||
class TOTPSetupSerializer(ModelSerializer):
|
||||
@@ -48,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__"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -255,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
|
||||
@@ -414,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
|
||||
@@ -439,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:
|
||||
@@ -520,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 (
|
||||
@@ -527,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)
|
||||
|
||||
@@ -540,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 (
|
||||
@@ -547,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
|
||||
@@ -564,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 (
|
||||
@@ -571,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)
|
||||
|
||||
@@ -724,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
|
||||
|
||||
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")
|
||||
@@ -116,6 +116,7 @@ class AgentTableSerializer(serializers.ModelSerializer):
|
||||
"logged_username",
|
||||
"italic",
|
||||
"policy",
|
||||
"block_policy_inheritance",
|
||||
]
|
||||
depth = 2
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import asyncio
|
||||
import datetime as dt
|
||||
import random
|
||||
import requests
|
||||
import urllib.parse
|
||||
from time import sleep
|
||||
from typing import Union
|
||||
@@ -12,7 +11,7 @@ from loguru import logger
|
||||
from packaging import version as pyver
|
||||
|
||||
from agents.models import Agent
|
||||
from core.models import CoreSettings, CodeSignToken
|
||||
from core.models import CodeSignToken, CoreSettings
|
||||
from logs.models import PendingAction
|
||||
from scripts.models import Script
|
||||
from tacticalrmm.celery import app
|
||||
@@ -21,21 +20,9 @@ from tacticalrmm.utils import run_nats_api_cmd
|
||||
logger.configure(**settings.LOG_CONFIG)
|
||||
|
||||
|
||||
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
|
||||
def agent_update(pk: int, codesigntoken: str = None, force: bool = False) -> str:
|
||||
from agents.utils import get_exegen_url
|
||||
|
||||
if r.status_code == 200:
|
||||
return url
|
||||
|
||||
return random.choice(urls)
|
||||
|
||||
|
||||
def agent_update(pk: int, codesigntoken: str = None) -> str:
|
||||
agent = Agent.objects.get(pk=pk)
|
||||
|
||||
if pyver.parse(agent.version) <= pyver.parse("1.3.0"):
|
||||
@@ -52,28 +39,29 @@ def agent_update(pk: int, codesigntoken: str = None) -> str:
|
||||
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/?"
|
||||
base_url = get_exegen_url() + "/api/v1/winagents/?"
|
||||
params = {"version": version, "arch": agent.arch, "token": codesigntoken}
|
||||
url = base_url + urllib.parse.urlencode(params)
|
||||
else:
|
||||
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",
|
||||
@@ -87,6 +75,21 @@ def agent_update(pk: int, codesigntoken: str = None) -> 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:
|
||||
@@ -208,6 +211,7 @@ def agent_outages_task() -> None:
|
||||
|
||||
agents = Agent.objects.only(
|
||||
"pk",
|
||||
"agent_id",
|
||||
"last_seen",
|
||||
"offline_time",
|
||||
"overdue_time",
|
||||
@@ -275,6 +279,34 @@ def run_script_email_results_task(
|
||||
logger.error(e)
|
||||
|
||||
|
||||
@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
|
||||
def monitor_agents_task() -> None:
|
||||
agents = Agent.objects.only(
|
||||
|
||||
@@ -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,7 +915,7 @@ class TestAgentTasks(TacticalTestCase):
|
||||
self.authenticate()
|
||||
self.setup_coresettings()
|
||||
|
||||
@patch("agents.tasks._get_exegen_url")
|
||||
@patch("agents.utils.get_exegen_url")
|
||||
@patch("agents.models.Agent.nats_cmd")
|
||||
def test_agent_update(self, nats_cmd, get_exe):
|
||||
from agents.tasks import agent_update
|
||||
|
||||
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,
|
||||
@@ -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
|
||||
@@ -761,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,6 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import CoreSettings, CustomField, CodeSignToken
|
||||
from .models import CodeSignToken, CoreSettings, CustomField
|
||||
|
||||
admin.site.register(CoreSettings)
|
||||
admin.site.register(CustomField)
|
||||
|
||||
@@ -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()
|
||||
|
||||
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"),)
|
||||
@@ -279,3 +277,62 @@ class CodeSignToken(models.Model):
|
||||
|
||||
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, CodeSignToken
|
||||
from .models import CodeSignToken, CoreSettings, CustomField, GlobalKVStore, URLAction
|
||||
|
||||
|
||||
class CoreSettingsSerializer(serializers.ModelSerializer):
|
||||
@@ -33,3 +33,15 @@ 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,6 +1,6 @@
|
||||
import requests
|
||||
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
|
||||
@@ -8,8 +8,8 @@ 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
|
||||
|
||||
|
||||
@@ -88,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
|
||||
@@ -106,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 = {
|
||||
@@ -120,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 = {
|
||||
@@ -132,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)
|
||||
|
||||
@@ -273,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)
|
||||
|
||||
@@ -13,4 +13,9 @@ urlpatterns = [
|
||||
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,25 +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, CodeSignToken
|
||||
from .models import CodeSignToken, CoreSettings, CustomField, GlobalKVStore, URLAction
|
||||
from .permissions import CodeSignPerms, EditCoreSettingsPerms, ServerMaintPerms
|
||||
from .serializers import (
|
||||
CodeSignTokenSerializer,
|
||||
CoreSettingsSerializer,
|
||||
CustomFieldSerializer,
|
||||
CodeSignTokenSerializer,
|
||||
KeyStoreSerializer,
|
||||
URLActionSerializer,
|
||||
)
|
||||
|
||||
|
||||
class UploadMeshAgent(APIView):
|
||||
permission_classes = [IsAuthenticated, MeshPerms]
|
||||
parser_class = (FileUploadParser,)
|
||||
|
||||
def put(self, request, format=None):
|
||||
@@ -45,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)
|
||||
@@ -61,16 +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,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -89,6 +104,7 @@ def email_test(request):
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
@permission_classes([IsAuthenticated, ServerMaintPerms])
|
||||
def server_maintenance(request):
|
||||
from tacticalrmm.utils import reload_nats
|
||||
|
||||
@@ -143,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)
|
||||
@@ -163,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)
|
||||
|
||||
@@ -186,6 +206,8 @@ class GetUpdateDeleteCustomFields(APIView):
|
||||
|
||||
|
||||
class CodeSign(APIView):
|
||||
permission_classes = [IsAuthenticated, CodeSignPerms]
|
||||
|
||||
def get(self, request):
|
||||
token = CodeSignToken.objects.first()
|
||||
return Response(CodeSignTokenSerializer(token).data)
|
||||
@@ -228,3 +250,106 @@ class CodeSign(APIView):
|
||||
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,21 +1,24 @@
|
||||
[
|
||||
{
|
||||
"guid": "6820cb5e-5a7f-4d9b-8c22-d54677e3cc04",
|
||||
"filename": "Win_Clear_Firefox_Cache.ps1",
|
||||
"filename": "Win_Firefox_Clear_Cache.ps1",
|
||||
"submittedBy": "https://github.com/Omnicef",
|
||||
"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": "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",
|
||||
@@ -24,7 +27,8 @@
|
||||
"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",
|
||||
@@ -33,7 +37,8 @@
|
||||
"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",
|
||||
@@ -42,7 +47,8 @@
|
||||
"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",
|
||||
@@ -51,7 +57,8 @@
|
||||
"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",
|
||||
@@ -75,14 +82,15 @@
|
||||
"guid": "3c46290b-85db-4cd2-93a2-943c8c93b3b1",
|
||||
"filename": "Speedtest.py",
|
||||
"submittedBy": "https://github.com/wh1te909",
|
||||
"name": "Network - 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": "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.",
|
||||
@@ -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"
|
||||
@@ -152,9 +160,30 @@
|
||||
"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": "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.",
|
||||
@@ -163,7 +192,7 @@
|
||||
},
|
||||
{
|
||||
"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": "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.",
|
||||
@@ -177,7 +206,8 @@
|
||||
"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",
|
||||
@@ -192,7 +222,7 @@
|
||||
"guid": "7c14beb4-d1c3-41aa-8e70-92a267d6e080",
|
||||
"filename": "Win_Duplicati_Status.ps1",
|
||||
"submittedBy": "https://github.com/dinger1986",
|
||||
"name": "Duplicati - Check",
|
||||
"name": "Duplicati - Check Status",
|
||||
"description": "Checks Duplicati Backup is running properly over the last 24 hours",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):3rd Party Software"
|
||||
@@ -213,7 +243,8 @@
|
||||
"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",
|
||||
@@ -228,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"
|
||||
@@ -262,10 +293,10 @@
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
@@ -289,7 +320,7 @@
|
||||
},
|
||||
{
|
||||
"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",
|
||||
@@ -327,10 +358,11 @@
|
||||
"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",
|
||||
@@ -341,6 +373,15 @@
|
||||
"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",
|
||||
@@ -389,21 +430,12 @@
|
||||
},
|
||||
{
|
||||
"guid": "f396dae2-c768-45c5-bd6c-176e56ed3614",
|
||||
"filename": "Win_Finish_updates_and_restart.ps1",
|
||||
"filename": "Win_Power_RestartorShutdown.ps1",
|
||||
"submittedBy": "https://github.com/tremor021",
|
||||
"name": "Updates - Finish and restart",
|
||||
"description": "Finish installing Windows 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": "Updates - Finish and Shutdown",
|
||||
"description": "Finish installing Windows updates and shutdown PC",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Other"
|
||||
"category": "TRMM (Win):Updates"
|
||||
},
|
||||
{
|
||||
"guid": "e09895d5-ca13-44a2-a38c-6e77c740f0e8",
|
||||
@@ -414,6 +446,8 @@
|
||||
"args": [
|
||||
"-serviceName {{client.ScreenConnectService}}",
|
||||
"-url {{client.ScreenConnectInstaller}}",
|
||||
"-clientname {{client.name}}",
|
||||
"-sitename {{site.name}}",
|
||||
"-action install"
|
||||
],
|
||||
"default_timeout": "90",
|
||||
@@ -422,7 +456,7 @@
|
||||
},
|
||||
{
|
||||
"guid": "3abbb62a-3757-492c-8979-b4fc6174845d",
|
||||
"filename": "Win_Disable_AutoRun.bat",
|
||||
"filename": "Win_AutoRun_Disable.bat",
|
||||
"submittedBy": "https://github.com/silversword411",
|
||||
"name": "Autorun - Disable",
|
||||
"description": "Disable Autorun System Wide",
|
||||
@@ -432,7 +466,7 @@
|
||||
},
|
||||
{
|
||||
"guid": "4a11877a-7555-494c-ac74-29d6df3c1989",
|
||||
"filename": "Win_Disable_Cortana.bat",
|
||||
"filename": "Win_Cortana_Disable.bat",
|
||||
"submittedBy": "https://github.com/silversword411",
|
||||
"name": "Cortana - Disable",
|
||||
"description": "Disable Cortana System Wide",
|
||||
@@ -444,7 +478,7 @@
|
||||
"guid": "28ef1387-dd4f-4bab-b042-26250914e370",
|
||||
"filename": "Win_WOL_Enable_Status.ps1",
|
||||
"submittedBy": "https://github.com/silversword411",
|
||||
"name": "Network WoL - Enable function",
|
||||
"name": "BROKEN Network WoL - Enable function",
|
||||
"description": "Wake on Lan enable on Dell, HP, Lenovo",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Network",
|
||||
@@ -454,12 +488,52 @@
|
||||
"guid": "685d5432-0b84-46d5-98e8-3ec2054150fe",
|
||||
"filename": "Win_WOL_Test_State.ps1",
|
||||
"submittedBy": "https://github.com/silversword411",
|
||||
"name": "Network WoL - Test State",
|
||||
"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",
|
||||
@@ -474,8 +548,8 @@
|
||||
"guid": "83f6c6ea-6120-4fd3-bec8-d3abc505dcdf",
|
||||
"filename": "Win_TRMM_Start_Menu_Delete_Shortcut.ps1",
|
||||
"submittedBy": "https://github.com/silversword411",
|
||||
"name": "TRMM Delete Start Menu Shortcut for App",
|
||||
"description": "Tactical RMM delete its application shortcut that's installed in the start menu",
|
||||
"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"
|
||||
@@ -563,9 +637,9 @@
|
||||
},
|
||||
{
|
||||
"guid": "77da9c87-5a7a-4ba1-bdde-3eeb3b01d62d",
|
||||
"filename": "Win_Set_Network_To_Private.ps1",
|
||||
"filename": "Win_Network_Set_To_Private.ps1",
|
||||
"submittedBy": "https://github.com/tremor021",
|
||||
"name": "Set Network To Private",
|
||||
"name": "Network Category - Set Network To Private",
|
||||
"description": "Sets current network type to Private",
|
||||
"shell": "powershell",
|
||||
"category": "TRMM (Win):Network"
|
||||
@@ -574,9 +648,39 @@
|
||||
"guid": "768f42d5-7b45-45ed-8233-254ae537aaa2",
|
||||
"filename": "Win_TaskScheduler_Add_Task.ps1",
|
||||
"submittedBy": "https://github.com/tremor021",
|
||||
"name": "Add task to TaskScheduler",
|
||||
"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"
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
@@ -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"),
|
||||
@@ -192,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()
|
||||
@@ -210,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):
|
||||
@@ -288,3 +288,212 @@ class TestScriptViews(TacticalTestCase):
|
||||
fn: str = script["filename"]
|
||||
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,20 +15,23 @@ EXE_DIR = os.path.join(BASE_DIR, "tacticalrmm/private/exe")
|
||||
AUTH_USER_MODEL = "accounts.User"
|
||||
|
||||
# latest release
|
||||
TRMM_VERSION = "0.6.0"
|
||||
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.129"
|
||||
APP_VER = "0.0.138"
|
||||
|
||||
# https://github.com/wh1te909/rmmagent
|
||||
LATEST_AGENT_VER = "1.5.0"
|
||||
LATEST_AGENT_VER = "1.5.8"
|
||||
|
||||
MESH_VER = "0.7.93"
|
||||
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"
|
||||
@@ -42,6 +45,12 @@ 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)
|
||||
|
||||
|
||||
@@ -66,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):
|
||||
|
||||
@@ -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
|
||||
]
|
||||
|
||||
@@ -5,7 +5,7 @@ import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
import urllib.parse
|
||||
from typing import Union
|
||||
from typing import Optional, Union
|
||||
|
||||
import pytz
|
||||
import requests
|
||||
@@ -52,7 +52,7 @@ def generate_winagent_exe(
|
||||
file_name: str,
|
||||
) -> Union[Response, FileResponse]:
|
||||
|
||||
from agents.tasks import _get_exegen_url
|
||||
from agents.utils import get_exegen_url
|
||||
|
||||
inno = (
|
||||
f"winagent-v{settings.LATEST_AGENT_VER}.exe"
|
||||
@@ -62,7 +62,7 @@ def generate_winagent_exe(
|
||||
|
||||
try:
|
||||
codetoken = CodeSignToken.objects.first().token
|
||||
base_url = _get_exegen_url() + "/api/v1/winagents/?"
|
||||
base_url = get_exegen_url() + "/api/v1/winagents/?"
|
||||
params = {
|
||||
"version": settings.LATEST_AGENT_VER,
|
||||
"arch": arch,
|
||||
@@ -263,3 +263,126 @@ def run_nats_api_cmd(mode: str, ids: list[str], timeout: int = 30) -> None:
|
||||
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")
|
||||
@@ -1,13 +1,15 @@
|
||||
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 agents.models import Agent
|
||||
from tacticalrmm.utils import get_default_timezone
|
||||
|
||||
from .models import WinUpdate
|
||||
from .permissions import ManageWinUpdatePerms
|
||||
from .serializers import UpdateSerializer
|
||||
|
||||
|
||||
@@ -20,6 +22,7 @@ def get_win_updates(request, pk):
|
||||
|
||||
|
||||
@api_view()
|
||||
@permission_classes([IsAuthenticated, ManageWinUpdatePerms])
|
||||
def run_update_scan(request, pk):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
agent.delete_superseded_updates()
|
||||
@@ -28,6 +31,7 @@ def run_update_scan(request, pk):
|
||||
|
||||
|
||||
@api_view()
|
||||
@permission_classes([IsAuthenticated, ManageWinUpdatePerms])
|
||||
def install_updates(request, pk):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
agent.delete_superseded_updates()
|
||||
@@ -41,6 +45,7 @@ def install_updates(request, pk):
|
||||
|
||||
|
||||
@api_view(["PATCH"])
|
||||
@permission_classes([IsAuthenticated, ManageWinUpdatePerms])
|
||||
def edit_policy(request):
|
||||
patch = get_object_or_404(WinUpdate, pk=request.data["pk"])
|
||||
patch.action = request.data["policy"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
SCRIPT_VERSION="12"
|
||||
SCRIPT_VERSION="13"
|
||||
SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/backup.sh'
|
||||
|
||||
GREEN='\033[0;32m'
|
||||
@@ -59,6 +59,7 @@ mkdir ${tmp_dir}/nginx
|
||||
mkdir ${tmp_dir}/systemd
|
||||
mkdir ${tmp_dir}/rmm
|
||||
mkdir ${tmp_dir}/confd
|
||||
mkdir ${tmp_dir}/redis
|
||||
|
||||
|
||||
pg_dump --dbname=postgresql://"${POSTGRES_USER}":"${POSTGRES_PW}"@127.0.0.1:5432/tacticalrmm | gzip -9 > ${tmp_dir}/postgres/db-${dt_now}.psql.gz
|
||||
@@ -72,6 +73,8 @@ sudo tar -czvf ${tmp_dir}/nginx/etc-nginx.tar.gz -C /etc/nginx .
|
||||
|
||||
sudo tar -czvf ${tmp_dir}/confd/etc-confd.tar.gz -C /etc/conf.d .
|
||||
|
||||
sudo tar -czvf ${tmp_dir}/redis/etc-redis.tar.gz -C /var/lib/redis/appendonly.aof
|
||||
|
||||
sudo cp ${sysd}/rmm.service ${sysd}/celery.service ${sysd}/celerybeat.service ${sysd}/meshcentral.service ${sysd}/nats.service ${tmp_dir}/systemd/
|
||||
if [ -f "${sysd}/daphne.service" ]; then
|
||||
sudo cp ${sysd}/daphne.service ${tmp_dir}/systemd/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM nats:2.2-alpine
|
||||
FROM nats:2.2.6-alpine
|
||||
|
||||
ENV TACTICAL_DIR /opt/tactical
|
||||
ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready
|
||||
|
||||
@@ -18,6 +18,7 @@ volumes:
|
||||
postgres_data:
|
||||
mongo_data:
|
||||
mesh_data:
|
||||
redis_data:
|
||||
|
||||
services:
|
||||
# postgres database for api service
|
||||
@@ -38,7 +39,10 @@ services:
|
||||
tactical-redis:
|
||||
container_name: trmm-redis
|
||||
image: redis:6.0-alpine
|
||||
command: redis-server --appendonly yes
|
||||
restart: always
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
networks:
|
||||
- redis
|
||||
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
# Alerting Overview
|
||||
|
||||
## Notification Types
|
||||
|
||||
* *Email Alerts* - Sends email
|
||||
* *SMS Alerts* - Sends text message
|
||||
* *Dashboard Alerts* - Adds a notification in the dashboard alert icon
|
||||
|
||||
|
||||
## Alert Severities
|
||||
|
||||
* Informational
|
||||
* Warning
|
||||
* Error
|
||||
|
||||
#### Agents
|
||||
Agent offline alerts always have an error severity.
|
||||
|
||||
#### Checks
|
||||
Checks can be configured to create alerts with different severities
|
||||
|
||||
* Memory and Cpuload checks can be configured with a warning and error threshold. To disable one of them put in a 0.
|
||||
* Script checks allow for information and warning return codes. Everything else, besides a 0 will result in an error severity.
|
||||
* Event Log, service, and ping checks require you to set the severity to information, warning, or error.
|
||||
|
||||
#### Automated Tasks
|
||||
For automated tasks, you set the what the alert severity should be directly on the task.
|
||||
|
||||
|
||||
## Configure Alert Templates
|
||||
Alert template allow you to setup alerting and notifications on many agents at once. Alert templates can be applied to Sites, Client, Automation Policies, and in the Global Settings.
|
||||
|
||||
To create an alert template, go to Settings > Alerts Manager. Then click New
|
||||
|
||||
In the form, give the alert template a name and make sure it is enabled.
|
||||
|
||||
Optionally setup any of the below settings:
|
||||
* *Failure Action* - Runs the selected script once on any agent. This is useful for running one-time tasks like sending an http request to an external system to create a ticket.
|
||||
* *Failure action args* - Optionally pass in arguments to the failure script.
|
||||
* *Failure action timeout* - Sets the timeout for the script.
|
||||
* *Resolved action* - Runs the selected script once on any agent if the alert is resolved. This is useful for running onetime tasks like sending an http request to an external system to close the ticket that was created.
|
||||
* *Resolved action args* - Optionally pass in arguments to the resolved script.
|
||||
* *Resolved action timeout* - Sets the timeout for the script.
|
||||
* *Email Recipients* - Overrides the default email recipients in Global Settings.
|
||||
* *From Email* - Overrides the From email address in Global Settings.
|
||||
* *SMS Recipients* - Overrides the SMS recipients in Global Settings.
|
||||
* *Include desktops* - Will apply to desktops
|
||||
#### agent/check/task settings
|
||||
* *Email on resolved* - Sends a email when the alert clears
|
||||
* *Text on resolved* - Sends a text when the alert clears
|
||||
* *Always email* - This enables the email notification setting on the agent/check/task
|
||||
* *Always sms* - This enables the text notification setting on the agent/check/task
|
||||
* *Always dashboard alert* - This enables the dashboard alert notification setting on the agent/check/task
|
||||
* *Periodic notification* - This sets up a periodic notification on for the agent/check/task alert
|
||||
* *Alert on severity* - When configured, will only send a notification through the corresponding channel if the alert is of the specified severity
|
||||
|
||||
## Applying Alert Templates
|
||||
|
||||
Alerts are applied in the following over. The agent picks the closest matching alert template.
|
||||
|
||||
* Right-click on any Client or Site and go to Assign Alert Template
|
||||
* In Automation Manager, click on Assign Alert Template for the policy you want to apply it to
|
||||
* In Global Settings, select the default alert template
|
||||
|
||||
1. Policy w/ Alert Template applied to Site
|
||||
2. Site
|
||||
3. Policy w/ Alert Template applied to Client
|
||||
4. Client
|
||||
5. Default Alert Template
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user