Compare commits

...

245 Commits

Author SHA1 Message Date
wh1te909
7e48015a54 Release 0.4.8 2021-02-16 18:57:37 +00:00
wh1te909
9ed3abf932 fix tests 2021-02-16 18:55:55 +00:00
wh1te909
61762828a3 fix typo 2021-02-16 18:50:42 +00:00
wh1te909
59beabe5ac bump versions 2021-02-16 18:47:51 +00:00
wh1te909
0b30faa28c decrease pause timeout for installer 2021-02-16 18:45:59 +00:00
wh1te909
d12d49b93f update quasar [skip ci] 2021-02-16 17:24:48 +00:00
wh1te909
f1d64d275a update go [skip ci] 2021-02-16 17:15:13 +00:00
wh1te909
d094eeeb03 update natsapi [skip ci] 2021-02-16 17:09:05 +00:00
wh1te909
be25af658e partially implement #222 2021-02-16 09:22:28 +00:00
wh1te909
794f52c229 delete remove salt task 2021-02-16 08:46:42 +00:00
wh1te909
5d4dc4ed4c change monitoragents func to run async 2021-02-16 08:33:54 +00:00
wh1te909
e49d97b898 disable loading spinner during alert poll 2021-02-16 01:06:42 +00:00
wh1te909
b6b4f1ba62 fix query 2021-02-16 01:06:12 +00:00
wh1te909
653d476716 back to http requests wh1te909/rmmagent@278b3a8a55 2021-02-14 03:12:22 +00:00
sadnub
48b855258c improve test coverage for automation 2021-02-13 16:14:15 -05:00
wh1te909
c7efdaf5f9 change run_script to take the script PK instead of entire script model 2021-02-13 19:41:38 +00:00
wh1te909
22523ed3d3 Merge branch 'develop' of https://github.com/wh1te909/tacticalrmm into develop 2021-02-13 19:40:41 +00:00
wh1te909
33c602dd61 update reqs 2021-02-13 19:40:18 +00:00
sadnub
e2a5509b76 add missing task tests in automation and alerts 2021-02-13 14:38:03 -05:00
wh1te909
61a0fa1a89 fix runscript email 2021-02-12 22:50:21 +00:00
wh1te909
a35bd8292b catch service error 2021-02-12 19:24:06 +00:00
Tragic Bronson
06c8ae60e3 Merge pull request #269 from sadnub/feature-alerts
WIP - Feature alerts
2021-02-12 10:48:27 -08:00
sadnub
deeab1f845 fix/add tests for check thresholds 2021-02-12 13:39:46 -05:00
sadnub
da81c4c987 fix failure and resolved action timeouts 2021-02-12 12:49:16 -05:00
sadnub
d180f1b2d5 fix check threshold modals and add client/serverside validation. Allow viewing alert script results in alerts overview. Fix diskspace check history computation. other fixes and improvements 2021-02-12 12:37:53 -05:00
sadnub
526135629c fix some typos and implement runscript and runscriptfull on agent function 2021-02-11 20:11:03 -05:00
sadnub
6b9493e057 reworked alerts a bit to not need AgentOutage table. Implemented resolve/failure script running on alert. also added script arg support for alert actions. Allow scripts to be run on any running agent 2021-02-11 20:11:03 -05:00
sadnub
9bb33d2afc fix tests 2021-02-11 20:11:03 -05:00
sadnub
7421138533 finish alerts views testing. Minor bug fixes 2021-02-11 20:11:03 -05:00
sadnub
d0800c52bb black 2021-02-11 20:11:03 -05:00
sadnub
913fcd4df2 fix tests and added soem minor fixes 2021-02-11 20:11:03 -05:00
sadnub
83322cc725 fix automation tests. minor fixes 2021-02-11 20:11:03 -05:00
sadnub
5944501feb fix migrations for real this time 2021-02-11 20:11:03 -05:00
sadnub
17e3603d3d implement overriding email/sms settings with alert templates 2021-02-11 20:11:03 -05:00
sadnub
95be43ae47 fix alerts icon and fix policycheck/task status. added resolved alerts actions 2021-02-11 20:11:03 -05:00
sadnub
feb91cbbaa fix migration issue and consolidate migrations a bit 2021-02-11 20:11:03 -05:00
sadnub
79409af168 implement alert periodic notifications for agent, task, and check. implement sms/email functionality for autotasks 2021-02-11 20:11:03 -05:00
sadnub
5dbfb64822 add handle alerts functions to agents, checks, and tasks. Minor fixes 2021-02-11 20:11:03 -05:00
sadnub
5e7ebf5e69 added relation view and a number of bug fixes 2021-02-11 20:11:03 -05:00
sadnub
e73215ca74 implement alert template exclusions 2021-02-11 20:11:03 -05:00
sadnub
a5f123b9ce bug fixes with automated manager deleting policies and adding 2021-02-11 20:11:03 -05:00
sadnub
ac058e9675 fixed alerts manager table, added celery task to unsnooze alerts, added bulk actions to alerts overview 2021-02-11 20:11:02 -05:00
sadnub
371b764d1d added new alert option for dashboard alerts, added actions to be run if alert triggered on agent, random fixes 2021-02-11 20:11:02 -05:00
sadnub
66d7172e09 reworked policy add for client, site, and agent. removed vue unit tests, added alertign to auto tasks, added edit autotask capabilities for certain fields, moved policy generation logic to save method on Client, Site, Agent, Policy models 2021-02-11 20:11:02 -05:00
sadnub
99d3a8a749 more alerts work 2021-02-11 20:11:02 -05:00
sadnub
db5ff372a4 alerts overview work 2021-02-11 20:11:02 -05:00
sadnub
3fe83f81be migrations fix and finishing up automation manager rework 2021-02-11 20:11:02 -05:00
sadnub
669e638fd6 automation manager rework start 2021-02-11 20:11:02 -05:00
sadnub
f1f999f3b6 more alerts work 2021-02-11 20:11:02 -05:00
sadnub
6f3b6fa9ce alerts wip 2021-02-11 20:11:02 -05:00
wh1te909
938f945301 drop min ram req 2021-02-12 00:23:22 +00:00
Tragic Bronson
e3efb2aad6 Merge pull request #273 from wh1te909/dependabot/pip/dot-devcontainer/cryptography-3.3.2
Bump cryptography from 3.2.1 to 3.3.2 in /.devcontainer
2021-02-11 13:47:26 -08:00
Tragic Bronson
1e678c0d78 Merge pull request #272 from wh1te909/dependabot/pip/api/tacticalrmm/cryptography-3.3.2
Bump cryptography from 3.3.1 to 3.3.2 in /api/tacticalrmm
2021-02-11 13:47:14 -08:00
wh1te909
a59c111140 add community script 2021-02-11 17:58:59 +00:00
Tragic Bronson
a8b2a31bed Merge pull request #275 from bradhawkins85/patch-3
Create Display Message To User.ps1
2021-02-11 09:45:55 -08:00
bradhawkins85
37402f9ee8 Create Display Message To User.ps1 2021-02-11 11:16:28 +10:00
dependabot[bot]
e7b5ecb40f Bump cryptography from 3.2.1 to 3.3.2 in /.devcontainer
Bumps [cryptography](https://github.com/pyca/cryptography) from 3.2.1 to 3.3.2.
- [Release notes](https://github.com/pyca/cryptography/releases)
- [Changelog](https://github.com/pyca/cryptography/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/3.2.1...3.3.2)

Signed-off-by: dependabot[bot] <support@github.com>
2021-02-10 02:34:29 +00:00
dependabot[bot]
c817ef04b9 Bump cryptography from 3.3.1 to 3.3.2 in /api/tacticalrmm
Bumps [cryptography](https://github.com/pyca/cryptography) from 3.3.1 to 3.3.2.
- [Release notes](https://github.com/pyca/cryptography/releases)
- [Changelog](https://github.com/pyca/cryptography/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/3.3.1...3.3.2)

Signed-off-by: dependabot[bot] <support@github.com>
2021-02-10 01:51:52 +00:00
wh1te909
f52b18439c update ssh script 2021-02-09 19:08:24 +00:00
wh1te909
1e03c628d5 Release 0.4.7 2021-02-06 01:04:02 +00:00
wh1te909
71fb39db1f bump versions 2021-02-06 00:59:49 +00:00
wh1te909
bcfb3726b0 update restore script to work on debian 10 2021-02-06 00:40:25 +00:00
wh1te909
c6e9e29671 increase uwsgi buffer size 2021-02-06 00:39:22 +00:00
wh1te909
1bfefcce39 fix backup 2021-02-06 00:38:29 +00:00
wh1te909
22488e93e1 approve updates when triggered manually 2021-02-03 23:23:34 +00:00
wh1te909
244b89f035 exclude migrations from black 2021-02-03 20:11:49 +00:00
wh1te909
1f9a241b94 Release 0.4.6 2021-02-02 19:33:30 +00:00
wh1te909
03641aae42 bump versions 2021-02-02 19:20:24 +00:00
wh1te909
a2bdd113cc update natsapi [skip ci] 2021-02-02 19:19:29 +00:00
wh1te909
a92e2f3c7b more winupdate fixes 2021-02-02 09:42:12 +00:00
wh1te909
97766b3a57 more superseded updates cleanup 2021-02-02 01:12:20 +00:00
wh1te909
9ef4c3bb06 more pending actions fix 2021-02-01 21:23:37 +00:00
wh1te909
d82f0cd757 Release 0.4.5 2021-02-01 20:57:53 +00:00
wh1te909
5f529e2af4 bump versions 2021-02-01 20:57:35 +00:00
wh1te909
beadd9e02b fix duplicate pending actions being created 2021-02-01 20:56:05 +00:00
wh1te909
72543789cb Release 0.4.4 2021-02-01 19:24:51 +00:00
wh1te909
5789439fa9 bump versions 2021-02-01 19:23:03 +00:00
wh1te909
f549126bcf update natsapi 2021-02-01 19:20:32 +00:00
wh1te909
7197548bad new pipelines vm 2021-01-31 02:42:10 +00:00
wh1te909
241fde783c add back pending actions for agent updates 2021-01-31 02:06:55 +00:00
wh1te909
2b872cd1f4 remove old views 2021-01-31 00:19:10 +00:00
wh1te909
a606fb4d1d add some deps to install for stripped down vps [skip ci] 2021-01-30 21:31:01 +00:00
wh1te909
9f9c6be38e update natsapi [skip ci] github.com/wh1te909/rmmagent@47b25c29362f0639ec606571f679df1f523e69a9 2021-01-30 06:42:20 +00:00
wh1te909
01ee524049 Release 0.4.3 2021-01-30 04:45:10 +00:00
wh1te909
af9cb65338 bump version 2021-01-30 04:44:41 +00:00
wh1te909
8aa11c580b move agents monitor task to go 2021-01-30 04:39:15 +00:00
wh1te909
ada627f444 forgot to enable natsapi during install 2021-01-30 04:28:27 +00:00
wh1te909
a7b6d338c3 update reqs 2021-01-30 02:06:56 +00:00
wh1te909
9f00538b97 fix tests 2021-01-29 23:38:59 +00:00
wh1te909
a085015282 increase timeout for security eventlogs 2021-01-29 23:34:16 +00:00
wh1te909
0b9c220fbb remove old task 2021-01-29 20:36:28 +00:00
wh1te909
0e3d04873d move wmi celery task to golang 2021-01-29 20:10:52 +00:00
wh1te909
b7578d939f add test for community script shell type 2021-01-29 09:37:34 +00:00
wh1te909
b5c28de03f Release 0.4.2 2021-01-29 08:23:06 +00:00
wh1te909
e17d25c156 bump versions 2021-01-29 08:12:03 +00:00
wh1te909
c25dc1b99c also override shell during load community scripts 2021-01-29 07:39:08 +00:00
Tragic Bronson
a493a574bd Merge pull request #265 from saulens22/patch-1
Fix "TRMM Defender Exclusions" script shell type
2021-01-28 23:36:03 -08:00
Saulius Kazokas
4284493dce Fix "TRMM Defender Exclusions" script shell type 2021-01-29 07:10:10 +02:00
wh1te909
25059de8e1 fix superseded windows defender updates 2021-01-29 02:37:51 +00:00
wh1te909
1731b05ad0 remove old serializers 2021-01-29 02:25:31 +00:00
wh1te909
e80dc663ac remove unused func 2021-01-29 02:22:06 +00:00
wh1te909
39988a4c2f cleanup an old view 2021-01-29 02:15:27 +00:00
wh1te909
415bff303a add some debug for unsupported agents 2021-01-29 01:22:35 +00:00
wh1te909
a65eb62a54 checkrunner changes wh1te909/rmmagent@10a0935f1b 2021-01-29 00:34:18 +00:00
wh1te909
03b2982128 update build flags 2021-01-28 23:11:32 +00:00
wh1te909
bff0527857 Release 0.4.1 2021-01-27 07:48:14 +00:00
wh1te909
f3b7634254 fix tests 2021-01-27 07:45:00 +00:00
wh1te909
6a9593c0b9 bump versions 2021-01-27 07:35:11 +00:00
wh1te909
edb785b8e5 prepare for agent 1.4.0 2021-01-27 07:11:49 +00:00
wh1te909
26d757b50a checkrunner interval changes wh1te909/rmmagent@7f131d54cf 2021-01-27 06:38:42 +00:00
wh1te909
535079ee87 update natsapi 2021-01-26 20:54:30 +00:00
wh1te909
ac380c29c1 fix last response sorting closes #258 2021-01-26 19:58:08 +00:00
wh1te909
3fd212f26c more optimizations 2021-01-25 21:05:59 +00:00
wh1te909
04a3abc651 fix tests 2021-01-25 20:46:22 +00:00
wh1te909
6caf85ddd1 optimize some queries 2021-01-25 20:27:20 +00:00
wh1te909
16e4071508 use error msg from backend 2021-01-25 19:57:50 +00:00
wh1te909
69e7c4324b start mkdocs 2021-01-25 19:55:48 +00:00
wh1te909
a1c4a8cbe5 fix tab refresh 2021-01-23 06:27:33 +00:00
wh1te909
e37f6cfda7 Release 0.4.0 2021-01-23 03:46:22 +00:00
wh1te909
989c804409 bump version 2021-01-23 03:45:49 +00:00
sadnub
7345bc3c82 fix image build script 2021-01-22 20:04:30 -05:00
sadnub
69bee35700 remove winupdate container from dev 2021-01-22 20:03:30 -05:00
sadnub
598e24df7c remove salt and celery-winupdate containers 2021-01-22 19:57:58 -05:00
sadnub
0ae669201e Merge branch 'develop' of https://github.com/wh1te909/tacticalrmm into develop 2021-01-22 19:26:03 -05:00
wh1te909
f52a8a4642 black 2021-01-23 00:02:26 +00:00
wh1te909
9c40b61ef2 fix test 2021-01-22 23:41:10 +00:00
wh1te909
72dabcda83 fix a test 2021-01-22 23:29:18 +00:00
wh1te909
161a06dbcc don't change tab when using site refresh button 2021-01-22 23:27:28 +00:00
wh1te909
8ed3d4e70c update quasar 2021-01-22 23:26:44 +00:00
wh1te909
a4223ccc8a bump agent and mesh vers 2021-01-22 22:56:33 +00:00
wh1te909
ca85923855 add purge 2021-01-22 09:34:08 +00:00
wh1te909
52bfe7c493 update natsapi 2021-01-22 00:41:27 +00:00
wh1te909
4786bd0cbe create meshusername during install 2021-01-22 00:40:09 +00:00
wh1te909
cadab160ff add check to remove salt 2021-01-21 23:58:31 +00:00
wh1te909
6a7f17b2b0 more salt cleanup 2021-01-21 00:00:34 +00:00
wh1te909
4986a4d775 more salt cleanup 2021-01-20 23:22:02 +00:00
wh1te909
903af0c2cf goodbye salt, you've served us well 2021-01-20 22:11:54 +00:00
wh1te909
3282fa803c move to go for chocolatey wh1te909/rmmagent@cebde22fa0 2021-01-19 23:43:37 +00:00
wh1te909
67cc47608d add hosts check to migration doc 2021-01-19 23:25:35 +00:00
wh1te909
0411704b8b update rmmagent and resty 2021-01-19 23:10:50 +00:00
wh1te909
1de85b2c69 more winupdate rework wh1te909/rmmagent@08ec2f9191 2021-01-19 03:14:54 +00:00
wh1te909
33b012f29d typo 2021-01-19 03:11:07 +00:00
wh1te909
1357584df3 start winupdate rework 2021-01-19 00:59:38 +00:00
sadnub
e15809e271 Merge branch 'develop' of https://github.com/sadnub/tacticalrmm into develop 2021-01-18 09:17:17 -05:00
wh1te909
0da1950427 Release 0.3.3 2021-01-18 11:01:25 +00:00
wh1te909
e590b921be fix #252 2021-01-18 11:00:50 +00:00
wh1te909
09462692f5 Release 0.3.2 2021-01-18 10:00:45 +00:00
wh1te909
c1d1b5f762 bump version 2021-01-18 10:00:26 +00:00
wh1te909
6b9c87b858 feat: set agent table tab default #249 2021-01-18 09:57:50 +00:00
wh1te909
485b6eb904 Merge branch 'develop' of https://github.com/wh1te909/tacticalrmm into develop 2021-01-18 09:32:00 +00:00
wh1te909
057630bdb5 fix agent table sort #250 2021-01-18 09:31:28 +00:00
wh1te909
6b02873b30 fix agent table sort #250 2021-01-18 09:12:01 +00:00
wh1te909
0fa0fc6d6b add json linter to migration docs 2021-01-17 18:09:47 +00:00
wh1te909
339ec07465 Release 0.3.1 2021-01-17 05:48:27 +00:00
wh1te909
cd2e798fea bump versions 2021-01-17 05:43:34 +00:00
wh1te909
d5cadbeae2 split agent update into chunks 2021-01-17 05:42:38 +00:00
wh1te909
8046a3ccae Release 0.3.0 2021-01-17 02:16:06 +00:00
wh1te909
bf91d60b31 natsapi bin 1.0.0 2021-01-17 02:07:53 +00:00
wh1te909
539c047ec8 update go 2021-01-17 01:53:45 +00:00
wh1te909
290c18fa87 bump versions 2021-01-17 01:22:08 +00:00
wh1te909
98c46f5e57 fix domain 2021-01-17 01:21:21 +00:00
wh1te909
f8bd5b5b4e update configs/scripts and add migration docs for 0.3.0 2021-01-17 01:16:28 +00:00
wh1te909
816d32edad black 2021-01-16 23:34:55 +00:00
wh1te909
8453835c05 Merge branch 'develop' of https://github.com/wh1te909/tacticalrmm into develop 2021-01-16 23:32:54 +00:00
wh1te909
9328c356c8 possible fix for mesh scaling 2021-01-16 23:32:46 +00:00
sadnub
89e3c1fc94 remove my print statements 2021-01-16 17:46:56 -05:00
sadnub
67e54cd15d Remove pending action duplicates and make policy check/task propogation more efficient 2021-01-16 17:46:56 -05:00
sadnub
278ea24786 improve dev env 2021-01-16 17:46:56 -05:00
sadnub
aba1662631 remove my print statements 2021-01-16 17:46:30 -05:00
sadnub
61eeb60c19 Remove pending action duplicates and make policy check/task propogation more efficient 2021-01-16 17:44:27 -05:00
wh1te909
5e9a8f4806 new natsapi binary 2021-01-16 21:55:06 +00:00
wh1te909
4cb274e9bc update to celery 5 2021-01-16 21:52:30 +00:00
wh1te909
8b9b1a6a35 update mesh docker conf 2021-01-16 21:50:29 +00:00
sadnub
2655964113 improve dev env 2021-01-16 11:20:24 -05:00
wh1te909
188bad061b add wmi task 2021-01-16 10:31:00 +00:00
wh1te909
3af4c329aa update reqs 2021-01-16 09:42:03 +00:00
wh1te909
6c13395f7d add debug 2021-01-16 09:41:27 +00:00
wh1te909
77b32ba360 remove import 2021-01-16 09:39:15 +00:00
sadnub
91dba291ac nats-api fixes 2021-01-15 23:41:21 -05:00
sadnub
a6bc293640 Finish up check charts 2021-01-15 22:11:40 -05:00
sadnub
53882d6e5f fix dev port 2021-01-15 21:25:32 -05:00
sadnub
d68adfbf10 docker nats-api rework 2021-01-15 21:11:27 -05:00
sadnub
498a392d7f check graphs wip 2021-01-15 21:10:25 -05:00
sadnub
740f6c05db docker cli additions 2021-01-15 21:10:25 -05:00
wh1te909
d810ce301f update natsapi flags 2021-01-16 00:01:31 +00:00
wh1te909
5ef6a14d24 add nats-api binary 2021-01-15 18:21:25 +00:00
wh1te909
a13f6f1e68 move recovery to natsapi 2021-01-15 10:19:01 +00:00
wh1te909
d2d0f1aaee fix tests 2021-01-15 09:57:46 +00:00
wh1te909
e64c72cc89 #234 sort proc mem using bytes wh1te909/rmmagent@04470dd4ce 2021-01-15 09:44:18 +00:00
wh1te909
9ab915a08b Release 0.2.23 2021-01-14 02:43:56 +00:00
wh1te909
e26fbf0328 bump versions 2021-01-14 02:29:14 +00:00
wh1te909
d9a52c4a2a update reqs 2021-01-14 02:27:40 +00:00
wh1te909
7b2ec90de9 feat: double-click agent action #232 2021-01-14 02:21:08 +00:00
wh1te909
d310bf8bbf add community scripts from dinger #242 2021-01-14 01:17:58 +00:00
wh1te909
2abc6cc939 partially fix sort 2021-01-14 00:01:08 +00:00
sadnub
56d4e694a2 fix annotations and error for the check chart 2021-01-13 18:43:09 -05:00
wh1te909
5f002c9cdc bump mesh 2021-01-13 23:35:14 +00:00
wh1te909
759daf4b4a add wording 2021-01-13 23:35:01 +00:00
wh1te909
3a8d9568e3 split some tasks into chunks to reduce load 2021-01-13 22:26:54 +00:00
wh1te909
ff22a9d94a fix deployments in docker 2021-01-13 22:19:09 +00:00
sadnub
a6e42d5374 fix removing pendingactions that are outstanding 2021-01-13 13:21:09 -05:00
wh1te909
a2f74e0488 add natsapi flags 2021-01-12 21:14:43 +00:00
wh1te909
ee44240569 black 2021-01-12 21:06:44 +00:00
wh1te909
d0828744a2 update nginx conf
(cherry picked from commit bf61e27f8a)
2021-01-12 06:38:52 +00:00
wh1te909
6e2e576b29 start natsapi 2021-01-12 06:32:00 +00:00
wh1te909
bf61e27f8a update nginx conf 2021-01-12 03:02:03 +00:00
Tragic Bronson
c441c30b46 Merge pull request #243 from sadnub/develop
Move Check Runs from Audit to its own table
2021-01-11 00:29:59 -08:00
Tragic Bronson
0e741230ea Merge pull request #242 from dinger1986/develop
Added some scripts checks etc
2021-01-11 00:29:47 -08:00
sadnub
1bfe9ac2db complete other pending actions with same task if task is deleted 2021-01-10 20:19:38 -05:00
sadnub
6812e72348 fix process sorting 2021-01-10 19:35:39 -05:00
sadnub
b6449d2f5b black 2021-01-10 16:33:10 -05:00
sadnub
7e3ea20dce add some tests and bug fixes 2021-01-10 16:27:48 -05:00
sadnub
c9d6fe9dcd allow returning all check data 2021-01-10 15:14:02 -05:00
sadnub
4a649a6b8b black 2021-01-10 14:47:34 -05:00
sadnub
8fef184963 add check history graph for cpu, memory, and diskspace 2021-01-10 14:15:05 -05:00
sadnub
69583ca3c0 docker dev fixes 2021-01-10 13:17:49 -05:00
dinger1986
6038a68e91 Win Defender exclusions for Tactical 2021-01-10 17:56:12 +00:00
dinger1986
fa8bd8db87 Manually reinstall Mesh just incase 2021-01-10 17:54:41 +00:00
dinger1986
18b4f0ed0f Runs DNS check on host as defined 2021-01-10 17:53:53 +00:00
dinger1986
461f9d66c9 Disable Faststartup on Windows 10 2021-01-10 17:51:33 +00:00
dinger1986
2155103c7a Check Win Defender for detections etc 2021-01-10 17:51:06 +00:00
dinger1986
c9a6839c45 Clears Win Defender log files 2021-01-10 17:50:13 +00:00
dinger1986
9fbe331a80 Allows the following Apps access by Win Defender 2021-01-10 17:49:36 +00:00
dinger1986
a56389c4ce Sync time with DC 2021-01-10 17:46:47 +00:00
dinger1986
64656784cb Powershell Speedtest 2021-01-10 17:46:00 +00:00
dinger1986
6eff2c181e Install RDP and change power config 2021-01-10 17:44:23 +00:00
dinger1986
1aa48c6d62 Install OpenSSH on PCs 2021-01-10 17:42:11 +00:00
dinger1986
c7ca1a346d Enable Windows Defender and set preferences 2021-01-10 17:40:06 +00:00
dinger1986
fa0ec7b502 check Duplicati Backup is running properly 2021-01-10 17:38:06 +00:00
dinger1986
768438c136 Checks disks for errors reported in event viewer 2021-01-10 17:36:42 +00:00
dinger1986
9badea0b3c Update DiskStatus.ps1
Checks local disks for errors reported in event viewer within the last 24 hours
2021-01-10 17:35:50 +00:00
dinger1986
43263a1650 Add files via upload 2021-01-10 17:33:48 +00:00
wh1te909
821e02dc75 update mesh docker conf 2021-01-10 00:20:44 +00:00
wh1te909
ed011ecf28 remove old mesh overrides #217 2021-01-10 00:15:11 +00:00
wh1te909
d861de4c2f update community scripts 2021-01-09 22:26:02 +00:00
Tragic Bronson
3a3b2449dc Merge pull request #241 from RVL-Solutions/develop
Create Windows10Upgrade.ps1
2021-01-09 14:12:05 -08:00
Ruben van Leusden
d2614406ca Create Windows10Upgrade.ps1
Shared by Kyt through Discord
2021-01-08 22:20:33 +01:00
Tragic Bronson
0798d098ae Merge pull request #238 from wh1te909/revert-235-master
Revert "Create Windows10Upgrade.ps1"
2021-01-08 10:38:33 -08:00
Tragic Bronson
dab7ddc2bb Revert "Create Windows10Upgrade.ps1" 2021-01-08 10:36:42 -08:00
Tragic Bronson
081a96e281 Merge pull request #235 from RVL-Solutions/master
Create Windows10Upgrade.ps1
2021-01-08 10:36:19 -08:00
wh1te909
a7dd881d79 Release 0.2.22 2021-01-08 18:16:17 +00:00
wh1te909
8134d5e24d remove threading 2021-01-08 18:15:55 +00:00
Ruben van Leusden
ba6756cd45 Create Windows10Upgrade.ps1 2021-01-06 23:19:14 +01:00
Tragic Bronson
5d8fce21ac Merge pull request #230 from wh1te909/dependabot/npm_and_yarn/web/axios-0.21.1
Bump axios from 0.21.0 to 0.21.1 in /web
2021-01-05 13:51:18 -08:00
dependabot[bot]
e7e4a5bcd4 Bump axios from 0.21.0 to 0.21.1 in /web
Bumps [axios](https://github.com/axios/axios) from 0.21.0 to 0.21.1.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v0.21.1/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.21.0...v0.21.1)

Signed-off-by: dependabot[bot] <support@github.com>
2021-01-05 15:54:54 +00:00
252 changed files with 57684 additions and 49246 deletions

View File

@@ -23,5 +23,6 @@ POSTGRES_USER=postgres
POSTGRES_PASS=postgrespass POSTGRES_PASS=postgrespass
# DEV SETTINGS # DEV SETTINGS
APP_PORT=8080 APP_PORT=80
API_PORT=8000 API_PORT=80
HTTP_PROTOCOL=https

View File

@@ -3,12 +3,15 @@ version: '3.4'
services: services:
api-dev: api-dev:
image: api-dev image: api-dev
restart: always
build: build:
context: . context: .
dockerfile: ./api.dockerfile dockerfile: ./api.dockerfile
command: ["tactical-api"] command: ["tactical-api"]
environment:
API_PORT: ${API_PORT}
ports: ports:
- 8000:8000 - "8000:${API_PORT}"
volumes: volumes:
- tactical-data-dev:/opt/tactical - tactical-data-dev:/opt/tactical
- ..:/workspace:cached - ..:/workspace:cached
@@ -19,40 +22,31 @@ services:
app-dev: app-dev:
image: node:12-alpine image: node:12-alpine
ports: restart: always
- 8080:8080 command: /bin/sh -c "npm install && npm run serve -- --host 0.0.0.0 --port ${APP_PORT}"
command: /bin/sh -c "npm install && npm run serve -- --host 0.0.0.0 --port 8080"
working_dir: /workspace/web working_dir: /workspace/web
volumes: volumes:
- ..:/workspace:cached - ..:/workspace:cached
ports:
- "8080:${APP_PORT}"
networks: networks:
dev: dev:
aliases: aliases:
- tactical-frontend - tactical-frontend
# salt master and api
salt-dev:
image: ${IMAGE_REPO}tactical-salt:${VERSION}
restart: always
volumes:
- tactical-data-dev:/opt/tactical
- salt-data-dev:/etc/salt
ports:
- "4505:4505"
- "4506:4506"
networks:
dev:
aliases:
- tactical-salt
# nats # nats
nats-dev: nats-dev:
image: ${IMAGE_REPO}tactical-nats:${VERSION} image: ${IMAGE_REPO}tactical-nats:${VERSION}
restart: always restart: always
environment:
API_HOST: ${API_HOST}
API_PORT: ${API_PORT}
DEV: 1
ports: ports:
- "4222:4222" - "4222:4222"
volumes: volumes:
- tactical-data-dev:/opt/tactical - tactical-data-dev:/opt/tactical
- ..:/workspace:cached
networks: networks:
dev: dev:
aliases: aliases:
@@ -136,6 +130,8 @@ services:
MESH_USER: ${MESH_USER} MESH_USER: ${MESH_USER}
TRMM_USER: ${TRMM_USER} TRMM_USER: ${TRMM_USER}
TRMM_PASS: ${TRMM_PASS} TRMM_PASS: ${TRMM_PASS}
HTTP_PROTOCOL: ${HTTP_PROTOCOL}
APP_PORT: ${APP_PORT}
depends_on: depends_on:
- postgres-dev - postgres-dev
- meshcentral-dev - meshcentral-dev
@@ -179,23 +175,6 @@ services:
- postgres-dev - postgres-dev
- redis-dev - redis-dev
# container for celery winupdate tasks
celerywinupdate-dev:
image: api-dev
build:
context: .
dockerfile: ./api.dockerfile
command: ["tactical-celerywinupdate-dev"]
restart: always
networks:
- dev
volumes:
- tactical-data-dev:/opt/tactical
- ..:/workspace:cached
depends_on:
- postgres-dev
- redis-dev
nginx-dev: nginx-dev:
# container for tactical reverse proxy # container for tactical reverse proxy
image: ${IMAGE_REPO}tactical-nginx:${VERSION} image: ${IMAGE_REPO}tactical-nginx:${VERSION}
@@ -206,8 +185,8 @@ services:
MESH_HOST: ${MESH_HOST} MESH_HOST: ${MESH_HOST}
CERT_PUB_KEY: ${CERT_PUB_KEY} CERT_PUB_KEY: ${CERT_PUB_KEY}
CERT_PRIV_KEY: ${CERT_PRIV_KEY} CERT_PRIV_KEY: ${CERT_PRIV_KEY}
APP_PORT: 8080 APP_PORT: ${APP_PORT}
API_PORT: 8000 API_PORT: ${API_PORT}
networks: networks:
dev: dev:
ipv4_address: 172.21.0.20 ipv4_address: 172.21.0.20
@@ -222,7 +201,6 @@ volumes:
postgres-data-dev: postgres-data-dev:
mongo-dev-data: mongo-dev-data:
mesh-data-dev: mesh-data-dev:
salt-data-dev:
networks: networks:
dev: dev:

View File

@@ -9,8 +9,6 @@ set -e
: "${POSTGRES_USER:=tactical}" : "${POSTGRES_USER:=tactical}"
: "${POSTGRES_PASS:=tactical}" : "${POSTGRES_PASS:=tactical}"
: "${POSTGRES_DB:=tacticalrmm}" : "${POSTGRES_DB:=tacticalrmm}"
: "${SALT_HOST:=tactical-salt}"
: "${SALT_USER:=saltapi}"
: "${MESH_CONTAINER:=tactical-meshcentral}" : "${MESH_CONTAINER:=tactical-meshcentral}"
: "${MESH_USER:=meshcentral}" : "${MESH_USER:=meshcentral}"
: "${MESH_PASS:=meshcentralpass}" : "${MESH_PASS:=meshcentralpass}"
@@ -18,6 +16,9 @@ set -e
: "${API_HOST:=tactical-backend}" : "${API_HOST:=tactical-backend}"
: "${APP_HOST:=tactical-frontend}" : "${APP_HOST:=tactical-frontend}"
: "${REDIS_HOST:=tactical-redis}" : "${REDIS_HOST:=tactical-redis}"
: "${HTTP_PROTOCOL:=http}"
: "${APP_PORT:=8080}"
: "${API_PORT:=8000}"
# Add python venv to path # Add python venv to path
export PATH="${VIRTUAL_ENV}/bin:$PATH" export PATH="${VIRTUAL_ENV}/bin:$PATH"
@@ -44,18 +45,10 @@ function django_setup {
echo "setting up django environment" echo "setting up django environment"
# configure django settings # configure django settings
MESH_TOKEN=$(cat ${TACTICAL_DIR}/tmp/mesh_token) MESH_TOKEN="$(cat ${TACTICAL_DIR}/tmp/mesh_token)"
DJANGO_SEKRET=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 80 | head -n 1) DJANGO_SEKRET=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 80 | head -n 1)
# write salt pass to tmp dir
if [ ! -f "${TACTICAL__DIR}/tmp/salt_pass" ]; then
SALT_PASS=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 20 | head -n 1)
echo "${SALT_PASS}" > ${TACTICAL_DIR}/tmp/salt_pass
else
SALT_PASS=$(cat ${TACTICAL_DIR}/tmp/salt_pass)
fi
localvars="$(cat << EOF localvars="$(cat << EOF
SECRET_KEY = '${DJANGO_SEKRET}' SECRET_KEY = '${DJANGO_SEKRET}'
@@ -68,7 +61,7 @@ KEY_FILE = '/opt/tactical/certs/privkey.pem'
SCRIPTS_DIR = '${WORKSPACE_DIR}/scripts' SCRIPTS_DIR = '${WORKSPACE_DIR}/scripts'
ALLOWED_HOSTS = ['${API_HOST}'] ALLOWED_HOSTS = ['${API_HOST}', '*']
ADMIN_URL = 'admin/' ADMIN_URL = 'admin/'
@@ -103,9 +96,6 @@ if not DEBUG:
) )
}) })
SALT_USERNAME = '${SALT_USER}'
SALT_PASSWORD = '${SALT_PASS}'
SALT_HOST = '${SALT_HOST}'
MESH_USERNAME = '${MESH_USER}' MESH_USERNAME = '${MESH_USER}'
MESH_SITE = 'https://${MESH_HOST}' MESH_SITE = 'https://${MESH_HOST}'
MESH_TOKEN_KEY = '${MESH_TOKEN}' MESH_TOKEN_KEY = '${MESH_TOKEN}'
@@ -116,38 +106,36 @@ EOF
echo "${localvars}" > ${WORKSPACE_DIR}/api/tacticalrmm/tacticalrmm/local_settings.py echo "${localvars}" > ${WORKSPACE_DIR}/api/tacticalrmm/tacticalrmm/local_settings.py
# run migrations and init scripts # run migrations and init scripts
python manage.py migrate --no-input "${VIRTUAL_ENV}"/bin/python manage.py migrate --no-input
python manage.py collectstatic --no-input "${VIRTUAL_ENV}"/bin/python manage.py collectstatic --no-input
python manage.py initial_db_setup "${VIRTUAL_ENV}"/bin/python manage.py initial_db_setup
python manage.py initial_mesh_setup "${VIRTUAL_ENV}"/bin/python manage.py initial_mesh_setup
python manage.py load_chocos "${VIRTUAL_ENV}"/bin/python manage.py load_chocos
python manage.py load_community_scripts "${VIRTUAL_ENV}"/bin/python manage.py load_community_scripts
python manage.py reload_nats "${VIRTUAL_ENV}"/bin/python manage.py reload_nats
# create super user # create super user
echo "from accounts.models import User; User.objects.create_superuser('${TRMM_USER}', 'admin@example.com', '${TRMM_PASS}') if not User.objects.filter(username='${TRMM_USER}').exists() else 0;" | python manage.py shell echo "from accounts.models import User; User.objects.create_superuser('${TRMM_USER}', 'admin@example.com', '${TRMM_PASS}') if not User.objects.filter(username='${TRMM_USER}').exists() else 0;" | python manage.py shell
} }
if [ "$1" = 'tactical-init-dev' ]; then if [ "$1" = 'tactical-init-dev' ]; then
# make directories if they don't exist # make directories if they don't exist
mkdir -p ${TACTICAL_DIR}/tmp mkdir -p "${TACTICAL_DIR}/tmp"
test -f "${TACTICAL_READY_FILE}" && rm "${TACTICAL_READY_FILE}" test -f "${TACTICAL_READY_FILE}" && rm "${TACTICAL_READY_FILE}"
# setup Python virtual env and install dependencies # setup Python virtual env and install dependencies
python -m venv --copies ${VIRTUAL_ENV} ! test -e "${VIRTUAL_ENV}" && python -m venv --copies ${VIRTUAL_ENV}
pip install --no-cache-dir -r /requirements.txt "${VIRTUAL_ENV}"/bin/pip install --no-cache-dir -r /requirements.txt
django_setup django_setup
# create .env file for frontend # create .env file for frontend
webenv="$(cat << EOF webenv="$(cat << EOF
PROD_URL = "http://${API_HOST}:8000" PROD_URL = "${HTTP_PROTOCOL}://${API_HOST}"
DEV_URL = "http://${API_HOST}:8000" DEV_URL = "${HTTP_PROTOCOL}://${API_HOST}"
DEV_HOST = 0.0.0.0 APP_URL = https://${APP_HOST}
DEV_PORT = 8080
EOF EOF
)" )"
echo "${webenv}" | tee ${WORKSPACE_DIR}/web/.env > /dev/null echo "${webenv}" | tee ${WORKSPACE_DIR}/web/.env > /dev/null
@@ -161,22 +149,20 @@ EOF
fi fi
if [ "$1" = 'tactical-api' ]; then if [ "$1" = 'tactical-api' ]; then
cp "${WORKSPACE_DIR}"/api/tacticalrmm/core/goinstaller/bin/goversioninfo /usr/local/bin/goversioninfo
chmod +x /usr/local/bin/goversioninfo
check_tactical_ready check_tactical_ready
python manage.py runserver 0.0.0.0:8000 "${VIRTUAL_ENV}"/bin/python manage.py runserver 0.0.0.0:"${API_PORT}"
fi fi
if [ "$1" = 'tactical-celery-dev' ]; then if [ "$1" = 'tactical-celery-dev' ]; then
check_tactical_ready check_tactical_ready
celery -A tacticalrmm worker -l debug "${VIRTUAL_ENV}"/bin/celery -A tacticalrmm worker -l debug
fi fi
if [ "$1" = 'tactical-celerybeat-dev' ]; then if [ "$1" = 'tactical-celerybeat-dev' ]; then
check_tactical_ready check_tactical_ready
test -f "${WORKSPACE_DIR}/api/tacticalrmm/celerybeat.pid" && rm "${WORKSPACE_DIR}/api/tacticalrmm/celerybeat.pid" test -f "${WORKSPACE_DIR}/api/tacticalrmm/celerybeat.pid" && rm "${WORKSPACE_DIR}/api/tacticalrmm/celerybeat.pid"
celery -A tacticalrmm beat -l debug "${VIRTUAL_ENV}"/bin/celery -A tacticalrmm beat -l debug
fi
if [ "$1" = 'tactical-celerywinupdate-dev' ]; then
check_tactical_ready
celery -A tacticalrmm worker -Q wupdate -l debug
fi fi

View File

@@ -1,39 +1,38 @@
# To ensure app dependencies are ported from your virtual environment/host machine into your container, run 'pip freeze > requirements.txt' in the terminal to overwrite this file # To ensure app dependencies are ported from your virtual environment/host machine into your container, run 'pip freeze > requirements.txt' in the terminal to overwrite this file
amqp==2.6.1 amqp==5.0.5
asgiref==3.3.1 asgiref==3.3.1
asyncio-nats-client==0.11.4 asyncio-nats-client==0.11.4
billiard==3.6.3.0 billiard==3.6.3.0
celery==4.4.6 celery==5.0.5
certifi==2020.12.5 certifi==2020.12.5
cffi==1.14.3 cffi==1.14.5
chardet==3.0.4 chardet==4.0.0
cryptography==3.2.1 cryptography==3.4.4
decorator==4.4.2 decorator==4.4.2
Django==3.1.4 Django==3.1.6
django-cors-headers==3.5.0 django-cors-headers==3.7.0
django-rest-knox==4.1.0 django-rest-knox==4.1.0
djangorestframework==3.12.2 djangorestframework==3.12.2
future==0.18.2 future==0.18.2
idna==2.10 kombu==5.0.2
kombu==4.6.11
loguru==0.5.3 loguru==0.5.3
msgpack==1.0.0 msgpack==1.0.2
packaging==20.4 packaging==20.8
psycopg2-binary==2.8.6 psycopg2-binary==2.8.6
pycparser==2.20 pycparser==2.20
pycryptodome==3.9.9 pycryptodome==3.10.1
pyotp==2.4.1 pyotp==2.6.0
pyparsing==2.4.7 pyparsing==2.4.7
pytz==2020.4 pytz==2021.1
qrcode==6.1 qrcode==6.1
redis==3.5.3 redis==3.5.3
requests==2.25.0 requests==2.25.1
six==1.15.0 six==1.15.0
sqlparse==0.4.1 sqlparse==0.4.1
twilio==6.49.0 twilio==6.52.0
urllib3==1.26.2 urllib3==1.26.3
validators==0.18.1 validators==0.18.2
vine==1.3.0 vine==5.0.0
websockets==8.1 websockets==8.1
zipp==3.4.0 zipp==3.4.0
black black
@@ -42,3 +41,6 @@ django-extensions
coverage coverage
coveralls coveralls
model_bakery model_bakery
mkdocs
mkdocs-material
pymdown-extensions

View File

@@ -57,16 +57,6 @@ jobs:
platforms: linux/amd64 platforms: linux/amd64
tags: tacticalrmm/tactical-nats:${{ steps.prep.outputs.version }},tacticalrmm/tactical-nats:latest tags: tacticalrmm/tactical-nats:${{ steps.prep.outputs.version }},tacticalrmm/tactical-nats:latest
- name: Build and Push Tactical Salt Image
uses: docker/build-push-action@v2
with:
context: .
push: true
pull: true
file: ./docker/containers/tactical-salt/dockerfile
platforms: linux/amd64
tags: tacticalrmm/tactical-salt:${{ steps.prep.outputs.version }},tacticalrmm/tactical-salt:latest
- name: Build and Push Tactical Frontend Image - name: Build and Push Tactical Frontend Image
uses: docker/build-push-action@v2 uses: docker/build-push-action@v2
with: with:

19
.vscode/settings.json vendored
View File

@@ -41,4 +41,23 @@
"**/*.zip": true "**/*.zip": true
}, },
}, },
"go.useLanguageServer": true,
"[go]": {
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": false,
},
"editor.snippetSuggestions": "none",
},
"[go.mod]": {
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": true,
},
},
"gopls": {
"usePlaceholders": true,
"completeUnimported": true,
"staticcheck": true,
}
} }

View File

@@ -6,7 +6,7 @@
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/python/black) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/python/black)
Tactical RMM is a remote monitoring & management tool for Windows computers, built with Django and Vue.\ Tactical RMM is a remote monitoring & management tool for Windows computers, built with Django and Vue.\
It uses an [agent](https://github.com/wh1te909/rmmagent) written in golang, as well as the [SaltStack](https://github.com/saltstack/salt) api and [MeshCentral](https://github.com/Ylianst/MeshCentral) It uses an [agent](https://github.com/wh1te909/rmmagent) written in golang and integrates with [MeshCentral](https://github.com/Ylianst/MeshCentral)
# [LIVE DEMO](https://rmm.xlawgaming.com/) # [LIVE DEMO](https://rmm.xlawgaming.com/)
Demo database resets every hour. Alot of features are disabled for obvious reasons due to the nature of this app. Demo database resets every hour. Alot of features are disabled for obvious reasons due to the nature of this app.
@@ -36,7 +36,7 @@ Demo database resets every hour. Alot of features are disabled for obvious reaso
## Installation ## Installation
### Requirements ### Requirements
- VPS with 4GB ram (an install script is provided for Ubuntu Server 20.04 / Debian 10) - VPS with 2GB ram (an install script is provided for Ubuntu Server 20.04 / Debian 10)
- A domain you own with at least 3 subdomains - A domain you own with at least 3 subdomains
- Google Authenticator app (2 factor is NOT optional) - Google Authenticator app (2 factor is NOT optional)
@@ -62,7 +62,6 @@ sudo ufw default allow outgoing
sudo ufw allow ssh sudo ufw allow ssh
sudo ufw allow http sudo ufw allow http
sudo ufw allow https sudo ufw allow https
sudo ufw allow proto tcp from any to any port 4505,4506
sudo ufw allow proto tcp from any to any port 4222 sudo ufw allow proto tcp from any to any port 4222
sudo ufw enable && sudo ufw reload sudo ufw enable && sudo ufw reload
``` ```

View File

@@ -1,457 +0,0 @@
from __future__ import absolute_import
import psutil
import os
import datetime
import zlib
import json
import base64
import wmi
import win32evtlog
import win32con
import win32evtlogutil
import winerror
from time import sleep
import requests
import subprocess
import random
import platform
ARCH = "64" if platform.machine().endswith("64") else "32"
PROGRAM_DIR = os.path.join(os.environ["ProgramFiles"], "TacticalAgent")
TAC_RMM = os.path.join(PROGRAM_DIR, "tacticalrmm.exe")
NSSM = os.path.join(PROGRAM_DIR, "nssm.exe" if ARCH == "64" else "nssm-x86.exe")
TEMP_DIR = os.path.join(os.environ["WINDIR"], "Temp")
SYS_DRIVE = os.environ["SystemDrive"]
PY_BIN = os.path.join(SYS_DRIVE, "\\salt", "bin", "python.exe")
SALT_CALL = os.path.join(SYS_DRIVE, "\\salt", "salt-call.bat")
def get_services():
# see https://github.com/wh1te909/tacticalrmm/issues/38
# for why I am manually implementing the svc.as_dict() method of psutil
ret = []
for svc in psutil.win_service_iter():
i = {}
try:
i["display_name"] = svc.display_name()
i["binpath"] = svc.binpath()
i["username"] = svc.username()
i["start_type"] = svc.start_type()
i["status"] = svc.status()
i["pid"] = svc.pid()
i["name"] = svc.name()
i["description"] = svc.description()
except Exception:
continue
else:
ret.append(i)
return ret
def run_python_script(filename, timeout, script_type="userdefined"):
# no longer used in agent version 0.11.0
file_path = os.path.join(TEMP_DIR, filename)
if os.path.exists(file_path):
try:
os.remove(file_path)
except:
pass
if script_type == "userdefined":
__salt__["cp.get_file"](f"salt://scripts/userdefined/{filename}", file_path)
else:
__salt__["cp.get_file"](f"salt://scripts/{filename}", file_path)
return __salt__["cmd.run_all"](f"{PY_BIN} {file_path}", timeout=timeout)
def run_script(filepath, filename, shell, timeout, args=[], bg=False):
if shell == "powershell" or shell == "cmd":
if args:
return __salt__["cmd.script"](
source=filepath,
args=" ".join(map(lambda x: f'"{x}"', args)),
shell=shell,
timeout=timeout,
bg=bg,
)
else:
return __salt__["cmd.script"](
source=filepath, shell=shell, timeout=timeout, bg=bg
)
elif shell == "python":
file_path = os.path.join(TEMP_DIR, filename)
if os.path.exists(file_path):
try:
os.remove(file_path)
except:
pass
__salt__["cp.get_file"](filepath, file_path)
salt_cmd = "cmd.run_bg" if bg else "cmd.run_all"
if args:
a = " ".join(map(lambda x: f'"{x}"', args))
cmd = f"{PY_BIN} {file_path} {a}"
return __salt__[salt_cmd](cmd, timeout=timeout)
else:
return __salt__[salt_cmd](f"{PY_BIN} {file_path}", timeout=timeout)
def uninstall_agent():
remove_exe = os.path.join(PROGRAM_DIR, "unins000.exe")
__salt__["cmd.run_bg"]([remove_exe, "/VERYSILENT", "/SUPPRESSMSGBOXES"])
return "ok"
def update_salt():
for p in psutil.process_iter():
with p.oneshot():
if p.name() == "tacticalrmm.exe" and "updatesalt" in p.cmdline():
return "running"
from subprocess import Popen, PIPE
CREATE_NEW_PROCESS_GROUP = 0x00000200
DETACHED_PROCESS = 0x00000008
cmd = [TAC_RMM, "-m", "updatesalt"]
p = Popen(
cmd,
stdin=PIPE,
stdout=PIPE,
stderr=PIPE,
close_fds=True,
creationflags=DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP,
)
return p.pid
def run_manual_checks():
__salt__["cmd.run_bg"]([TAC_RMM, "-m", "runchecks"])
return "ok"
def install_updates():
for p in psutil.process_iter():
with p.oneshot():
if p.name() == "tacticalrmm.exe" and "winupdater" in p.cmdline():
return "running"
return __salt__["cmd.run_bg"]([TAC_RMM, "-m", "winupdater"])
def _wait_for_service(svc, status, retries=10):
attempts = 0
while 1:
try:
service = psutil.win_service_get(svc)
except psutil.NoSuchProcess:
stat = "fail"
attempts += 1
sleep(5)
else:
stat = service.status()
if stat != status:
attempts += 1
sleep(5)
else:
attempts = 0
if attempts == 0 or attempts > retries:
break
return stat
def agent_update_v2(inno, url):
# make sure another instance of the update is not running
# this function spawns 2 instances of itself (because we call it twice with salt run_bg)
# so if more than 2 running, don't continue as an update is already running
count = 0
for p in psutil.process_iter():
try:
with p.oneshot():
if "win_agent.agent_update_v2" in p.cmdline():
count += 1
except Exception:
continue
if count > 2:
return "already running"
sleep(random.randint(1, 20)) # don't flood the rmm
exe = os.path.join(TEMP_DIR, inno)
if os.path.exists(exe):
try:
os.remove(exe)
except:
pass
try:
r = requests.get(url, stream=True, timeout=600)
except Exception:
return "failed"
if r.status_code != 200:
return "failed"
with open(exe, "wb") as f:
for chunk in r.iter_content(chunk_size=1024):
if chunk:
f.write(chunk)
del r
ret = subprocess.run([exe, "/VERYSILENT", "/SUPPRESSMSGBOXES"], timeout=120)
tac = _wait_for_service(svc="tacticalagent", status="running")
if tac != "running":
subprocess.run([NSSM, "start", "tacticalagent"], timeout=30)
chk = _wait_for_service(svc="checkrunner", status="running")
if chk != "running":
subprocess.run([NSSM, "start", "checkrunner"], timeout=30)
return "ok"
def do_agent_update_v2(inno, url):
return __salt__["cmd.run_bg"](
[
SALT_CALL,
"win_agent.agent_update_v2",
f"inno={inno}",
f"url={url}",
"--local",
]
)
def agent_update(version, url):
# make sure another instance of the update is not running
# this function spawns 2 instances of itself so if more than 2 running,
# don't continue as an update is already running
count = 0
for p in psutil.process_iter():
try:
with p.oneshot():
if "win_agent.agent_update" in p.cmdline():
count += 1
except Exception:
continue
if count > 2:
return "already running"
sleep(random.randint(1, 60)) # don't flood the rmm
try:
r = requests.get(url, stream=True, timeout=600)
except Exception:
return "failed"
if r.status_code != 200:
return "failed"
exe = os.path.join(TEMP_DIR, f"winagent-v{version}.exe")
with open(exe, "wb") as f:
for chunk in r.iter_content(chunk_size=1024):
if chunk:
f.write(chunk)
del r
services = ("tacticalagent", "checkrunner")
for svc in services:
subprocess.run([NSSM, "stop", svc], timeout=120)
sleep(10)
r = subprocess.run([exe, "/VERYSILENT", "/SUPPRESSMSGBOXES"], timeout=300)
sleep(30)
for svc in services:
subprocess.run([NSSM, "start", svc], timeout=120)
return "ok"
def do_agent_update(version, url):
return __salt__["cmd.run_bg"](
[
SALT_CALL,
"win_agent.agent_update",
f"version={version}",
f"url={url}",
"--local",
]
)
class SystemDetail:
def __init__(self):
self.c = wmi.WMI()
self.comp_sys_prod = self.c.Win32_ComputerSystemProduct()
self.comp_sys = self.c.Win32_ComputerSystem()
self.memory = self.c.Win32_PhysicalMemory()
self.os = self.c.Win32_OperatingSystem()
self.base_board = self.c.Win32_BaseBoard()
self.bios = self.c.Win32_BIOS()
self.disk = self.c.Win32_DiskDrive()
self.network_adapter = self.c.Win32_NetworkAdapter()
self.network_config = self.c.Win32_NetworkAdapterConfiguration()
self.desktop_monitor = self.c.Win32_DesktopMonitor()
self.cpu = self.c.Win32_Processor()
self.usb = self.c.Win32_USBController()
def get_all(self, obj):
ret = []
for i in obj:
tmp = [
{j: getattr(i, j)}
for j in list(i.properties)
if getattr(i, j) is not None
]
ret.append(tmp)
return ret
def system_info():
info = SystemDetail()
return {
"comp_sys_prod": info.get_all(info.comp_sys_prod),
"comp_sys": info.get_all(info.comp_sys),
"mem": info.get_all(info.memory),
"os": info.get_all(info.os),
"base_board": info.get_all(info.base_board),
"bios": info.get_all(info.bios),
"disk": info.get_all(info.disk),
"network_adapter": info.get_all(info.network_adapter),
"network_config": info.get_all(info.network_config),
"desktop_monitor": info.get_all(info.desktop_monitor),
"cpu": info.get_all(info.cpu),
"usb": info.get_all(info.usb),
}
def local_sys_info():
return __salt__["cmd.run_bg"]([TAC_RMM, "-m", "sysinfo"])
def get_procs():
ret = []
# setup
for proc in psutil.process_iter():
with proc.oneshot():
proc.cpu_percent(interval=None)
# need time for psutil to record cpu percent
sleep(1)
for c, proc in enumerate(psutil.process_iter(), 1):
x = {}
with proc.oneshot():
if proc.pid == 0 or not proc.name():
continue
x["name"] = proc.name()
x["cpu_percent"] = proc.cpu_percent(interval=None) / psutil.cpu_count()
x["memory_percent"] = proc.memory_percent()
x["pid"] = proc.pid
x["ppid"] = proc.ppid()
x["status"] = proc.status()
x["username"] = proc.username()
x["id"] = c
ret.append(x)
return ret
def _compress_json(j):
return {
"wineventlog": base64.b64encode(
zlib.compress(json.dumps(j).encode("utf-8", errors="ignore"))
).decode("ascii", errors="ignore")
}
def get_eventlog(logtype, last_n_days):
start_time = datetime.datetime.now() - datetime.timedelta(days=last_n_days)
flags = win32evtlog.EVENTLOG_BACKWARDS_READ | win32evtlog.EVENTLOG_SEQUENTIAL_READ
status_dict = {
win32con.EVENTLOG_AUDIT_FAILURE: "AUDIT_FAILURE",
win32con.EVENTLOG_AUDIT_SUCCESS: "AUDIT_SUCCESS",
win32con.EVENTLOG_INFORMATION_TYPE: "INFO",
win32con.EVENTLOG_WARNING_TYPE: "WARNING",
win32con.EVENTLOG_ERROR_TYPE: "ERROR",
0: "INFO",
}
computer = "localhost"
hand = win32evtlog.OpenEventLog(computer, logtype)
total = win32evtlog.GetNumberOfEventLogRecords(hand)
log = []
uid = 0
done = False
try:
while 1:
events = win32evtlog.ReadEventLog(hand, flags, 0)
for ev_obj in events:
uid += 1
# return once total number of events reach or we'll be stuck in an infinite loop
if uid >= total:
done = True
break
the_time = ev_obj.TimeGenerated.Format()
time_obj = datetime.datetime.strptime(the_time, "%c")
if time_obj < start_time:
done = True
break
computer = str(ev_obj.ComputerName)
src = str(ev_obj.SourceName)
evt_type = str(status_dict[ev_obj.EventType])
evt_id = str(winerror.HRESULT_CODE(ev_obj.EventID))
evt_category = str(ev_obj.EventCategory)
record = str(ev_obj.RecordNumber)
msg = (
str(win32evtlogutil.SafeFormatMessage(ev_obj, logtype))
.replace("<", "")
.replace(">", "")
)
event_dict = {
"computer": computer,
"source": src,
"eventType": evt_type,
"eventID": evt_id,
"eventCategory": evt_category,
"message": msg,
"time": the_time,
"record": record,
"uid": uid,
}
log.append(event_dict)
if done:
break
except Exception:
pass
win32evtlog.CloseEventLog(hand)
return _compress_json(log)

View File

@@ -0,0 +1,26 @@
# Generated by Django 3.1.4 on 2021-01-14 01:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("accounts", "0009_user_show_community_scripts"),
]
operations = [
migrations.AddField(
model_name="user",
name="agent_dblclick_action",
field=models.CharField(
choices=[
("editagent", "Edit Agent"),
("takecontrol", "Take Control"),
("remotebg", "Remote Background"),
],
default="editagent",
max_length=50,
),
),
]

View File

@@ -0,0 +1,26 @@
# Generated by Django 3.1.5 on 2021-01-18 09:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("accounts", "0010_user_agent_dblclick_action"),
]
operations = [
migrations.AddField(
model_name="user",
name="default_agent_tbl_tab",
field=models.CharField(
choices=[
("server", "Servers"),
("workstation", "Workstations"),
("mixed", "Mixed"),
],
default="server",
max_length=50,
),
),
]

View File

@@ -3,12 +3,30 @@ from django.contrib.auth.models import AbstractUser
from logs.models import BaseAuditModel from logs.models import BaseAuditModel
AGENT_DBLCLICK_CHOICES = [
("editagent", "Edit Agent"),
("takecontrol", "Take Control"),
("remotebg", "Remote Background"),
]
AGENT_TBL_TAB_CHOICES = [
("server", "Servers"),
("workstation", "Workstations"),
("mixed", "Mixed"),
]
class User(AbstractUser, BaseAuditModel): class User(AbstractUser, BaseAuditModel):
is_active = models.BooleanField(default=True) is_active = models.BooleanField(default=True)
totp_key = models.CharField(max_length=50, null=True, blank=True) totp_key = models.CharField(max_length=50, null=True, blank=True)
dark_mode = models.BooleanField(default=True) dark_mode = models.BooleanField(default=True)
show_community_scripts = models.BooleanField(default=True) show_community_scripts = models.BooleanField(default=True)
agent_dblclick_action = models.CharField(
max_length=50, choices=AGENT_DBLCLICK_CHOICES, default="editagent"
)
default_agent_tbl_tab = models.CharField(
max_length=50, choices=AGENT_TBL_TAB_CHOICES, default="server"
)
agent = models.OneToOneField( agent = models.OneToOneField(
"agents.Agent", "agents.Agent",

View File

@@ -278,6 +278,14 @@ class TestUserAction(TacticalTestCase):
r = self.client.patch(url, data, format="json") r = self.client.patch(url, data, format="json")
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
data = {
"userui": True,
"agent_dblclick_action": "editagent",
"default_agent_tbl_tab": "mixed",
}
r = self.client.patch(url, data, format="json")
self.assertEqual(r.status_code, 200)
self.check_not_authenticated("patch", url) self.check_not_authenticated("patch", url)

View File

@@ -189,12 +189,17 @@ class UserUI(APIView):
def patch(self, request): def patch(self, request):
user = request.user user = request.user
if "dark_mode" in request.data: if "dark_mode" in request.data.keys():
user.dark_mode = request.data["dark_mode"] user.dark_mode = request.data["dark_mode"]
user.save(update_fields=["dark_mode"]) user.save(update_fields=["dark_mode"])
if "show_community_scripts" in request.data: if "show_community_scripts" in request.data.keys():
user.show_community_scripts = request.data["show_community_scripts"] user.show_community_scripts = request.data["show_community_scripts"]
user.save(update_fields=["show_community_scripts"]) user.save(update_fields=["show_community_scripts"])
if "userui" in request.data.keys():
user.agent_dblclick_action = request.data["agent_dblclick_action"]
user.default_agent_tbl_tab = request.data["default_agent_tbl_tab"]
user.save(update_fields=["agent_dblclick_action", "default_agent_tbl_tab"])
return Response("ok") return Response("ok")

View File

@@ -1,8 +1,7 @@
from django.contrib import admin from django.contrib import admin
from .models import Agent, AgentOutage, RecoveryAction, Note from .models import Agent, RecoveryAction, Note
admin.site.register(Agent) admin.site.register(Agent)
admin.site.register(AgentOutage)
admin.site.register(RecoveryAction) admin.site.register(RecoveryAction)
admin.site.register(Note) admin.site.register(Note)

View File

@@ -3,19 +3,20 @@ import string
import os import os
import json import json
from model_bakery.recipe import Recipe, seq from model_bakery.recipe import Recipe, foreign_key
from itertools import cycle from itertools import cycle
from django.utils import timezone as djangotime from django.utils import timezone as djangotime
from django.conf import settings from django.conf import settings
from .models import Agent
def generate_agent_id(hostname): def generate_agent_id(hostname):
rand = "".join(random.choice(string.ascii_letters) for _ in range(35)) rand = "".join(random.choice(string.ascii_letters) for _ in range(35))
return f"{rand}-{hostname}" return f"{rand}-{hostname}"
site = Recipe("clients.Site")
def get_wmi_data(): def get_wmi_data():
with open( with open(
os.path.join(settings.BASE_DIR, "tacticalrmm/test_data/wmi_python_agent.json") os.path.join(settings.BASE_DIR, "tacticalrmm/test_data/wmi_python_agent.json")
@@ -24,9 +25,10 @@ def get_wmi_data():
agent = Recipe( agent = Recipe(
Agent, "agents.Agent",
site=foreign_key(site),
hostname="DESKTOP-TEST123", hostname="DESKTOP-TEST123",
version="1.1.1", version="1.3.0",
monitoring_type=cycle(["workstation", "server"]), monitoring_type=cycle(["workstation", "server"]),
salt_id=generate_agent_id("DESKTOP-TEST123"), salt_id=generate_agent_id("DESKTOP-TEST123"),
agent_id="71AHC-AA813-HH1BC-AAHH5-00013|DESKTOP-TEST123", agent_id="71AHC-AA813-HH1BC-AAHH5-00013|DESKTOP-TEST123",

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.4 on 2021-01-29 21:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agents', '0026_auto_20201125_2334'),
]
operations = [
migrations.AddField(
model_name='agent',
name='overdue_dashboard_alert',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.1.4 on 2021-02-06 15:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agents', '0027_agent_overdue_dashboard_alert'),
]
operations = [
migrations.AddField(
model_name='agentoutage',
name='outage_email_sent_time',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='agentoutage',
name='outage_sms_sent_time',
field=models.DateTimeField(blank=True, null=True),
),
]

View File

@@ -0,0 +1,16 @@
# Generated by Django 3.1.4 on 2021-02-10 21:56
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('agents', '0028_auto_20210206_1534'),
]
operations = [
migrations.DeleteModel(
name='AgentOutage',
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.6 on 2021-02-16 08:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agents', '0029_delete_agentoutage'),
]
operations = [
migrations.AddField(
model_name='agent',
name='offline_time',
field=models.PositiveIntegerField(default=4),
),
]

View File

@@ -1,4 +1,3 @@
import requests
import time import time
import base64 import base64
from Crypto.Cipher import AES from Crypto.Cipher import AES
@@ -9,7 +8,10 @@ import validators
import msgpack import msgpack
import re import re
from collections import Counter from collections import Counter
from typing import List, Union, Any
from loguru import logger from loguru import logger
import asyncio
from packaging import version as pyver from packaging import version as pyver
from distutils.version import LooseVersion from distutils.version import LooseVersion
from nats.aio.client import Client as NATS from nats.aio.client import Client as NATS
@@ -18,6 +20,7 @@ from nats.aio.errors import ErrTimeout
from django.db import models from django.db import models
from django.conf import settings from django.conf import settings
from django.utils import timezone as djangotime from django.utils import timezone as djangotime
from alerts.models import AlertTemplate
from core.models import CoreSettings, TZ_CHOICES from core.models import CoreSettings, TZ_CHOICES
from logs.models import BaseAuditModel from logs.models import BaseAuditModel
@@ -50,6 +53,8 @@ class Agent(BaseAuditModel):
mesh_node_id = models.CharField(null=True, blank=True, max_length=255) mesh_node_id = models.CharField(null=True, blank=True, max_length=255)
overdue_email_alert = models.BooleanField(default=False) overdue_email_alert = models.BooleanField(default=False)
overdue_text_alert = models.BooleanField(default=False) overdue_text_alert = models.BooleanField(default=False)
overdue_dashboard_alert = models.BooleanField(default=False)
offline_time = models.PositiveIntegerField(default=4)
overdue_time = models.PositiveIntegerField(default=30) overdue_time = models.PositiveIntegerField(default=30)
check_interval = models.PositiveIntegerField(default=120) check_interval = models.PositiveIntegerField(default=120)
needs_reboot = models.BooleanField(default=False) needs_reboot = models.BooleanField(default=False)
@@ -75,6 +80,24 @@ class Agent(BaseAuditModel):
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
) )
def save(self, *args, **kwargs):
# get old agent if exists
old_agent = type(self).objects.get(pk=self.pk) if self.pk else None
super(BaseAuditModel, self).save(*args, **kwargs)
# check if new agent has been create
# or check if policy have changed on agent
# 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
):
self.generate_checks_from_policies()
self.generate_tasks_from_policies()
def __str__(self): def __str__(self):
return self.hostname return self.hostname
@@ -117,14 +140,6 @@ class Agent(BaseAuditModel):
return settings.DL_32 return settings.DL_32
return None return None
@property
def winsalt_dl(self):
if self.arch == "64":
return settings.SALT_64
elif self.arch == "32":
return settings.SALT_32
return None
@property @property
def win_inno_exe(self): def win_inno_exe(self):
if self.arch == "64": if self.arch == "64":
@@ -135,7 +150,7 @@ class Agent(BaseAuditModel):
@property @property
def status(self): def status(self):
offline = djangotime.now() - djangotime.timedelta(minutes=6) offline = djangotime.now() - djangotime.timedelta(minutes=self.offline_time)
overdue = djangotime.now() - djangotime.timedelta(minutes=self.overdue_time) overdue = djangotime.now() - djangotime.timedelta(minutes=self.overdue_time)
if self.last_seen is not None: if self.last_seen is not None:
@@ -256,6 +271,63 @@ class Agent(BaseAuditModel):
except: except:
return ["unknown disk"] return ["unknown disk"]
def run_script(
self,
scriptpk: int,
args: List[str] = [],
timeout: int = 120,
full: bool = False,
wait: bool = False,
run_on_any: bool = False,
) -> Any:
from scripts.models import Script
script = Script.objects.get(pk=scriptpk)
data = {
"func": "runscriptfull" if full else "runscript",
"timeout": timeout,
"script_args": args,
"payload": {
"code": script.code,
"shell": script.shell,
},
}
running_agent = self
if run_on_any:
nats_ping = {"func": "ping", "timeout": 1}
# try on self first
r = asyncio.run(self.nats_cmd(nats_ping))
if r == "pong":
running_agent = self
else:
online = [
agent
for agent in Agent.objects.only(
"pk", "last_seen", "overdue_time", "offline_time"
)
if agent.status == "online"
]
for agent in online:
r = asyncio.run(agent.nats_cmd(nats_ping))
if r == "pong":
running_agent = agent
break
if running_agent.pk == self.pk:
return "Unable to find an online agent"
if wait:
return asyncio.run(running_agent.nats_cmd(data, timeout=timeout, wait=True))
else:
asyncio.run(running_agent.nats_cmd(data, wait=False))
return "ok"
# auto approves updates # auto approves updates
def approve_updates(self): def approve_updates(self):
patch_policy = self.get_patch_policy() patch_policy = self.get_patch_policy()
@@ -382,14 +454,114 @@ class Agent(BaseAuditModel):
return patch_policy return patch_policy
# clear is used to delete managed policy checks from agent def get_approved_update_guids(self) -> List[str]:
# parent_checks specifies a list of checks to delete from agent with matching parent_check field return list(
def generate_checks_from_policies(self, clear=False): self.winupdates.filter(action="approve", installed=False).values_list(
from automation.models import Policy "guid", flat=True
)
)
# Clear agent checks managed by policy # returns alert template assigned in the following order: policy, site, client, global
if clear: # will return None if nothing is found
self.agentchecks.filter(managed_by_policy=True).delete() def get_alert_template(self) -> Union[AlertTemplate, None]:
site = self.site
client = self.client
core = CoreSettings.objects.first()
templates = list()
# check if alert template is on a policy assigned to agent
if (
self.policy
and self.policy.alert_template
and self.policy.alert_template.is_active
):
templates.append(self.policy.alert_template)
# check if policy with alert template is assigned to the site
elif (
self.monitoring_type == "server"
and site.server_policy
and site.server_policy.alert_template
and site.server_policy.alert_template.is_active
):
templates.append(site.server_policy.alert_template)
elif (
self.monitoring_type == "workstation"
and site.workstation_policy
and site.workstation_policy.alert_template
and site.workstation_policy.alert_template.is_active
):
templates.append(site.workstation_policy.alert_template)
# check if alert template is assigned to site
elif site.alert_template and site.alert_template.is_active:
templates.append(site.alert_template)
# check if policy with alert template is assigned to the client
elif (
self.monitoring_type == "server"
and client.server_policy
and client.server_policy.alert_template
and client.server_policy.alert_template.is_active
):
templates.append(client.server_policy.alert_template)
elif (
self.monitoring_type == "workstation"
and client.workstation_policy
and client.workstation_policy.alert_template
and client.workstation_policy.alert_template.is_active
):
templates.append(client.workstation_policy.alert_template)
# check if alert template is on client and return
elif client.alert_template and client.alert_template.is_active:
templates.append(client.alert_template)
# check if alert template is applied globally and return
elif core.alert_template and core.alert_template.is_active:
templates.append(core.alert_template)
# if agent is a workstation, check if policy with alert template is assigned to the site, client, or core
elif (
self.monitoring_type == "server"
and core.server_policy
and core.server_policy.alert_template
and core.server_policy.alert_template.is_active
):
templates.append(core.server_policy.alert_template)
elif (
self.monitoring_type == "workstation"
and core.workstation_policy
and core.workstation_policy.alert_template
and core.workstation_policy.alert_template.is_active
):
templates.append(core.workstation_policy.alert_template)
# go through the templates and return the first one that isn't excluded
for template in templates:
# check if client, site, or agent has been excluded from template
if (
client.pk in template.excluded_clients.all()
or site.pk in template.excluded_sites.all()
or self.pk in template.excluded_agents.all()
):
continue
# see if template is excluding desktops
if (
self.monitoring_type == "workstation"
and not template.agent_include_desktops
):
continue
else:
return template
# no alert templates found or agent has been excluded
return None
def generate_checks_from_policies(self):
from automation.models import Policy
# Clear agent checks that have overriden_by_policy set # Clear agent checks that have overriden_by_policy set
self.agentchecks.update(overriden_by_policy=False) self.agentchecks.update(overriden_by_policy=False)
@@ -397,17 +569,9 @@ class Agent(BaseAuditModel):
# Generate checks based on policies # Generate checks based on policies
Policy.generate_policy_checks(self) Policy.generate_policy_checks(self)
# clear is used to delete managed policy tasks from agent def generate_tasks_from_policies(self):
# parent_tasks specifies a list of tasks to delete from agent with matching parent_task field
def generate_tasks_from_policies(self, clear=False):
from autotasks.tasks import delete_win_task_schedule
from automation.models import Policy from automation.models import Policy
# Clear agent tasks managed by policy
if clear:
for task in self.autotasks.filter(managed_by_policy=True):
delete_win_task_schedule.delay(task.pk)
# Generate tasks based on policies # Generate tasks based on policies
Policy.generate_policy_tasks(self) Policy.generate_policy_tasks(self)
@@ -466,77 +630,6 @@ class Agent(BaseAuditModel):
await nc.flush() await nc.flush()
await nc.close() await nc.close()
def salt_api_cmd(self, **kwargs):
# salt should always timeout first before the requests' timeout
try:
timeout = kwargs["timeout"]
except KeyError:
# default timeout
timeout = 15
salt_timeout = 12
else:
if timeout < 8:
timeout = 8
salt_timeout = 5
else:
salt_timeout = timeout - 3
json = {
"client": "local",
"tgt": self.salt_id,
"fun": kwargs["func"],
"timeout": salt_timeout,
"username": settings.SALT_USERNAME,
"password": settings.SALT_PASSWORD,
"eauth": "pam",
}
if "arg" in kwargs:
json.update({"arg": kwargs["arg"]})
if "kwargs" in kwargs:
json.update({"kwarg": kwargs["kwargs"]})
try:
resp = requests.post(
f"http://{settings.SALT_HOST}:8123/run",
json=[json],
timeout=timeout,
)
except Exception:
return "timeout"
try:
ret = resp.json()["return"][0][self.salt_id]
except Exception as e:
logger.error(f"{self.salt_id}: {e}")
return "error"
else:
return ret
def salt_api_async(self, **kwargs):
json = {
"client": "local_async",
"tgt": self.salt_id,
"fun": kwargs["func"],
"username": settings.SALT_USERNAME,
"password": settings.SALT_PASSWORD,
"eauth": "pam",
}
if "arg" in kwargs:
json.update({"arg": kwargs["arg"]})
if "kwargs" in kwargs:
json.update({"kwarg": kwargs["kwargs"]})
try:
resp = requests.post(f"http://{settings.SALT_HOST}:8123/run", json=[json])
except Exception:
return "timeout"
return resp
@staticmethod @staticmethod
def serialize(agent): def serialize(agent):
# serializes the agent and returns json # serializes the agent and returns json
@@ -547,32 +640,6 @@ class Agent(BaseAuditModel):
del ret["client"] del ret["client"]
return ret return ret
@staticmethod
def salt_batch_async(**kwargs):
assert isinstance(kwargs["minions"], list)
json = {
"client": "local_async",
"tgt_type": "list",
"tgt": kwargs["minions"],
"fun": kwargs["func"],
"username": settings.SALT_USERNAME,
"password": settings.SALT_PASSWORD,
"eauth": "pam",
}
if "arg" in kwargs:
json.update({"arg": kwargs["arg"]})
if "kwargs" in kwargs:
json.update({"kwarg": kwargs["kwargs"]})
try:
resp = requests.post(f"http://{settings.SALT_HOST}:8123/run", json=[json])
except Exception:
return "timeout"
return resp
def delete_superseded_updates(self): def delete_superseded_updates(self):
try: try:
pks = [] # list of pks to delete pks = [] # list of pks to delete
@@ -625,73 +692,210 @@ class Agent(BaseAuditModel):
elif action.details["action"] == "taskdelete": elif action.details["action"] == "taskdelete":
delete_win_task_schedule.delay(task_id, pending_action=action.id) 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.exclude(status="completed"):
if action.details["task_id"] == task_id:
action.delete()
class AgentOutage(models.Model): def handle_alert(self, checkin: bool = False) -> None:
agent = models.ForeignKey( from alerts.models import Alert
Agent, from agents.tasks import (
related_name="agentoutages", agent_recovery_email_task,
null=True, agent_recovery_sms_task,
blank=True, agent_outage_email_task,
on_delete=models.CASCADE, agent_outage_sms_task,
) )
outage_time = models.DateTimeField(auto_now_add=True)
recovery_time = models.DateTimeField(null=True, blank=True)
outage_email_sent = models.BooleanField(default=False)
outage_sms_sent = models.BooleanField(default=False)
recovery_email_sent = models.BooleanField(default=False)
recovery_sms_sent = models.BooleanField(default=False)
@property # return if agent is in maintenace mode
def is_active(self): if self.maintenance_mode:
return False if self.recovery_time else True return
alert_template = self.get_alert_template()
# called when agent is back online
if checkin:
if Alert.objects.filter(agent=self, resolved=False).exists():
# resolve alert if exists
alert = Alert.objects.get(agent=self, resolved=False)
alert.resolve()
# check if a resolved notification should be emailed
if (
not alert.resolved_email_sent
and alert_template
and alert_template.agent_email_on_resolved
or self.overdue_email_alert
):
agent_recovery_email_task.delay(pk=alert.pk)
# check if a resolved notification should be texted
if (
not alert.resolved_sms_sent
and alert_template
and alert_template.agent_text_on_resolved
or self.overdue_text_alert
):
agent_recovery_sms_task.delay(pk=alert.pk)
# check if any scripts should be run
if (
not alert.resolved_action_run
and alert_template
and alert_template.resolved_action
):
r = self.run_script(
scriptpk=alert_template.resolved_action.pk,
args=alert_template.resolved_action_args,
timeout=alert_template.resolved_action_timeout,
wait=True,
full=True,
run_on_any=True,
)
# command was successful
if type(r) == dict:
alert.resolved_action_retcode = r["retcode"]
alert.resolved_action_stdout = r["stdout"]
alert.resolved_action_stderr = r["stderr"]
alert.resolved_action_execution_time = "{:.4f}".format(
r["execution_time"]
)
alert.resolved_action_run = djangotime.now()
alert.save()
else:
logger.error(
f"Resolved action: {alert_template.resolved_action} failed to run on any agent for {self.hostname} resolved outage"
)
# called when agent is offline
else:
# check if alert hasn't been created yet so create it
if not Alert.objects.filter(agent=self, resolved=False).exists():
alert = Alert.create_availability_alert(self)
# add a null check history to allow gaps in graph
for check in self.agentchecks.all():
check.add_check_history(None)
else:
alert = Alert.objects.get(agent=self, resolved=False)
# create dashboard alert if enabled
if (
alert_template
and alert_template.agent_always_alert
or self.overdue_dashboard_alert
):
alert.hidden = False
alert.save()
# send email alert if enabled
if (
not alert.email_sent
and alert_template
and alert_template.agent_always_email
or self.overdue_email_alert
):
agent_outage_email_task.delay(
pk=alert.pk,
alert_interval=alert_template.check_periodic_alert_days
if alert_template
else None,
)
# send text message if enabled
if (
not alert.sms_sent
and alert_template
and alert_template.agent_always_text
or self.overdue_text_alert
):
agent_outage_sms_task.delay(
pk=alert.pk,
alert_interval=alert_template.check_periodic_alert_days
if alert_template
else None,
)
# check if any scripts should be run
if not alert.action_run and alert_template and alert_template.action:
r = self.run_script(
scriptpk=alert_template.action.pk,
args=alert_template.action_args,
timeout=alert_template.action_timeout,
wait=True,
full=True,
run_on_any=True,
)
# command was successful
if type(r) == dict:
alert.action_retcode = r["retcode"]
alert.action_stdout = r["stdout"]
alert.action_stderr = r["stderr"]
alert.action_execution_time = "{:.4f}".format(r["execution_time"])
alert.action_run = djangotime.now()
alert.save()
else:
logger.error(
f"Failure action: {alert_template.action.name} failed to run on any agent for {self.hostname} outage"
)
def send_outage_email(self): def send_outage_email(self):
from core.models import CoreSettings from core.models import CoreSettings
CORE = CoreSettings.objects.first() CORE = CoreSettings.objects.first()
alert_template = self.get_alert_template()
CORE.send_mail( CORE.send_mail(
f"{self.agent.client.name}, {self.agent.site.name}, {self.agent.hostname} - data overdue", f"{self.client.name}, {self.site.name}, {self.hostname} - data overdue",
( (
f"Data has not been received from client {self.agent.client.name}, " f"Data has not been received from client {self.client.name}, "
f"site {self.agent.site.name}, " f"site {self.site.name}, "
f"agent {self.agent.hostname} " f"agent {self.hostname} "
"within the expected time." "within the expected time."
), ),
alert_template=alert_template,
) )
def send_recovery_email(self): def send_recovery_email(self):
from core.models import CoreSettings from core.models import CoreSettings
CORE = CoreSettings.objects.first() CORE = CoreSettings.objects.first()
alert_template = self.get_alert_template()
CORE.send_mail( CORE.send_mail(
f"{self.agent.client.name}, {self.agent.site.name}, {self.agent.hostname} - data received", f"{self.client.name}, {self.site.name}, {self.hostname} - data received",
( (
f"Data has been received from client {self.agent.client.name}, " f"Data has been received from client {self.client.name}, "
f"site {self.agent.site.name}, " f"site {self.site.name}, "
f"agent {self.agent.hostname} " f"agent {self.hostname} "
"after an interruption in data transmission." "after an interruption in data transmission."
), ),
alert_template=alert_template,
) )
def send_outage_sms(self): def send_outage_sms(self):
from core.models import CoreSettings from core.models import CoreSettings
alert_template = self.get_alert_template()
CORE = CoreSettings.objects.first() CORE = CoreSettings.objects.first()
CORE.send_sms( CORE.send_sms(
f"{self.agent.client.name}, {self.agent.site.name}, {self.agent.hostname} - data overdue" f"{self.client.name}, {self.site.name}, {self.hostname} - data overdue",
alert_template=alert_template,
) )
def send_recovery_sms(self): def send_recovery_sms(self):
from core.models import CoreSettings from core.models import CoreSettings
CORE = CoreSettings.objects.first() CORE = CoreSettings.objects.first()
alert_template = self.get_alert_template()
CORE.send_sms( CORE.send_sms(
f"{self.agent.client.name}, {self.agent.site.name}, {self.agent.hostname} - data received" f"{self.client.name}, {self.site.name}, {self.hostname} - data received",
alert_template=alert_template,
) )
def __str__(self):
return self.agent.hostname
RECOVERY_CHOICES = [ RECOVERY_CHOICES = [
("salt", "Salt"), ("salt", "Salt"),

View File

@@ -34,6 +34,17 @@ class AgentSerializer(serializers.ModelSerializer):
] ]
class AgentOverdueActionSerializer(serializers.ModelSerializer):
class Meta:
model = Agent
fields = [
"pk",
"overdue_email_alert",
"overdue_text_alert",
"overdue_dashboard_alert",
]
class AgentTableSerializer(serializers.ModelSerializer): class AgentTableSerializer(serializers.ModelSerializer):
patches_pending = serializers.ReadOnlyField(source="has_patches_pending") patches_pending = serializers.ReadOnlyField(source="has_patches_pending")
pending_actions = serializers.SerializerMethodField() pending_actions = serializers.SerializerMethodField()
@@ -42,17 +53,31 @@ class AgentTableSerializer(serializers.ModelSerializer):
last_seen = serializers.SerializerMethodField() last_seen = serializers.SerializerMethodField()
client_name = serializers.ReadOnlyField(source="client.name") client_name = serializers.ReadOnlyField(source="client.name")
site_name = serializers.ReadOnlyField(source="site.name") site_name = serializers.ReadOnlyField(source="site.name")
logged_username = serializers.SerializerMethodField()
italic = serializers.SerializerMethodField()
policy = serializers.ReadOnlyField(source="policy.id")
def get_pending_actions(self, obj): def get_pending_actions(self, obj):
return obj.pendingactions.filter(status="pending").count() return obj.pendingactions.filter(status="pending").count()
def get_last_seen(self, obj): def get_last_seen(self, obj) -> str:
if obj.time_zone is not None: if obj.time_zone is not None:
agent_tz = pytz.timezone(obj.time_zone) agent_tz = pytz.timezone(obj.time_zone)
else: else:
agent_tz = self.context["default_tz"] agent_tz = self.context["default_tz"]
return obj.last_seen.astimezone(agent_tz).strftime("%m %d %Y %H:%M:%S") return obj.last_seen.astimezone(agent_tz).timestamp()
def get_logged_username(self, obj) -> str:
if obj.logged_in_username == "None" and obj.status == "online":
return obj.last_logged_in_user
elif obj.logged_in_username != "None":
return obj.logged_in_username
else:
return "-"
def get_italic(self, obj) -> bool:
return obj.logged_in_username == "None" and obj.status == "online"
class Meta: class Meta:
model = Agent model = Agent
@@ -70,12 +95,14 @@ class AgentTableSerializer(serializers.ModelSerializer):
"status", "status",
"overdue_text_alert", "overdue_text_alert",
"overdue_email_alert", "overdue_email_alert",
"overdue_dashboard_alert",
"last_seen", "last_seen",
"boot_time", "boot_time",
"checks", "checks",
"logged_in_username",
"last_logged_in_user",
"maintenance_mode", "maintenance_mode",
"logged_username",
"italic",
"policy",
] ]
depth = 2 depth = 2
@@ -101,10 +128,12 @@ class AgentEditSerializer(serializers.ModelSerializer):
"timezone", "timezone",
"check_interval", "check_interval",
"overdue_time", "overdue_time",
"offline_time",
"overdue_text_alert", "overdue_text_alert",
"overdue_email_alert", "overdue_email_alert",
"all_timezones", "all_timezones",
"winupdatepolicy", "winupdatepolicy",
"policy",
] ]

View File

@@ -2,79 +2,57 @@ import asyncio
from loguru import logger from loguru import logger
from time import sleep from time import sleep
import random import random
import requests
from concurrent.futures import ThreadPoolExecutor
from packaging import version as pyver from packaging import version as pyver
from typing import List from typing import List, Union
import datetime as dt
from django.utils import timezone as djangotime
from django.conf import settings from django.conf import settings
from scripts.models import Script from scripts.models import Script
from tacticalrmm.celery import app from tacticalrmm.celery import app
from agents.models import Agent, AgentOutage from agents.models import Agent
from core.models import CoreSettings from core.models import CoreSettings
from logs.models import PendingAction from logs.models import PendingAction
logger.configure(**settings.LOG_CONFIG) logger.configure(**settings.LOG_CONFIG)
def _check_agent_service(pk: int) -> None:
agent = Agent.objects.get(pk=pk)
r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=2))
if r == "pong":
logger.info(
f"Detected crashed tacticalagent service on {agent.hostname}, attempting recovery"
)
data = {"func": "recover", "payload": {"mode": "tacagent"}}
asyncio.run(agent.nats_cmd(data, wait=False))
def _check_in_full(pk: int) -> None:
agent = Agent.objects.get(pk=pk)
asyncio.run(agent.nats_cmd({"func": "checkinfull"}, wait=False))
@app.task
def check_in_task() -> None:
q = Agent.objects.only("pk", "version")
agents: List[int] = [
i.pk for i in q if pyver.parse(i.version) >= pyver.parse("1.1.12")
]
with ThreadPoolExecutor() as executor:
executor.map(_check_in_full, agents)
@app.task
def monitor_agents_task() -> None:
q = Agent.objects.all()
agents: List[int] = [i.pk for i in q if i.has_nats and i.status != "online"]
with ThreadPoolExecutor() as executor:
executor.map(_check_agent_service, agents)
def agent_update(pk: int) -> str: def agent_update(pk: int) -> str:
agent = Agent.objects.get(pk=pk) agent = Agent.objects.get(pk=pk)
if pyver.parse(agent.version) <= pyver.parse("1.1.11"):
logger.warning(
f"{agent.hostname} v{agent.version} is running an unsupported version. Refusing to auto update."
)
return "not supported"
# skip if we can't determine the arch # skip if we can't determine the arch
if agent.arch is None: if agent.arch is None:
logger.warning(f"Unable to determine arch on {agent.hostname}. Skipping.") logger.warning(
f"Unable to determine arch on {agent.hostname}. Skipping agent update."
)
return "noarch" return "noarch"
# removed sqlite in 1.4.0 to get rid of cgo dependency
# 1.3.0 has migration func to move from sqlite to win registry, so force an upgrade to 1.3.0 if old agent
if pyver.parse(agent.version) >= pyver.parse("1.3.0"):
version = settings.LATEST_AGENT_VER version = settings.LATEST_AGENT_VER
url = agent.winagent_dl url = agent.winagent_dl
inno = agent.win_inno_exe inno = agent.win_inno_exe
else:
version = "1.3.0"
inno = (
"winagent-v1.3.0.exe" if agent.arch == "64" else "winagent-v1.3.0-x86.exe"
)
url = f"https://github.com/wh1te909/rmmagent/releases/download/v1.3.0/{inno}"
if agent.has_nats:
if pyver.parse(agent.version) <= pyver.parse("1.1.11"):
if agent.pendingactions.filter( if agent.pendingactions.filter(
action_type="agentupdate", status="pending" action_type="agentupdate", status="pending"
).exists(): ).exists():
action = agent.pendingactions.filter( agent.pendingactions.filter(
action_type="agentupdate", status="pending" action_type="agentupdate", status="pending"
).last() ).delete()
if pyver.parse(action.details["version"]) < pyver.parse(version):
action.delete()
else:
return "pending"
PendingAction.objects.create( PendingAction.objects.create(
agent=agent, agent=agent,
@@ -85,7 +63,7 @@ def agent_update(pk: int) -> str:
"inno": inno, "inno": inno,
}, },
) )
else:
nats_data = { nats_data = {
"func": "agentupdate", "func": "agentupdate",
"payload": { "payload": {
@@ -95,28 +73,23 @@ def agent_update(pk: int) -> str:
}, },
} }
asyncio.run(agent.nats_cmd(nats_data, wait=False)) asyncio.run(agent.nats_cmd(nats_data, wait=False))
return "created" return "created"
return "not supported"
@app.task @app.task
def send_agent_update_task(pks: List[int], version: str) -> None: def send_agent_update_task(pks: List[int]) -> None:
q = Agent.objects.filter(pk__in=pks) chunks = (pks[i : i + 30] for i in range(0, len(pks), 30))
agents: List[int] = [ for chunk in chunks:
i.pk for i in q if pyver.parse(i.version) < pyver.parse(version) for pk in chunk:
]
for pk in agents:
agent_update(pk) agent_update(pk)
sleep(0.05)
sleep(4)
@app.task @app.task
def auto_self_agent_update_task() -> None: def auto_self_agent_update_task() -> None:
core = CoreSettings.objects.first() core = CoreSettings.objects.first()
if not core.agent_auto_update: if not core.agent_auto_update:
logger.info("Agent auto update is disabled. Skipping.")
return return
q = Agent.objects.only("pk", "version") q = Agent.objects.only("pk", "version")
@@ -126,184 +99,126 @@ def auto_self_agent_update_task() -> None:
if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER) if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
] ]
for pk in pks: chunks = (pks[i : i + 30] for i in range(0, len(pks), 30))
agent_update(pk)
@app.task
def sync_sysinfo_task():
agents = Agent.objects.all()
online = [
i
for i in agents
if pyver.parse(i.version) >= pyver.parse("1.1.3") and i.status == "online"
]
for agent in online:
asyncio.run(agent.nats_cmd({"func": "sync"}, wait=False))
@app.task
def sync_salt_modules_task(pk):
agent = Agent.objects.get(pk=pk)
r = agent.salt_api_cmd(timeout=35, func="saltutil.sync_modules")
# successful sync if new/charnged files: {'return': [{'MINION-15': ['modules.get_eventlog', 'modules.win_agent', 'etc...']}]}
# successful sync with no new/changed files: {'return': [{'MINION-15': []}]}
if r == "timeout" or r == "error":
return f"Unable to sync modules {agent.salt_id}"
return f"Successfully synced salt modules on {agent.hostname}"
@app.task
def batch_sync_modules_task():
# sync modules, split into chunks of 50 agents to not overload salt
agents = Agent.objects.all()
online = [i.salt_id for i in agents]
chunks = (online[i : i + 50] for i in range(0, len(online), 50))
for chunk in chunks: for chunk in chunks:
Agent.salt_batch_async(minions=chunk, func="saltutil.sync_modules") for pk in chunk:
sleep(10) agent_update(pk)
sleep(0.05)
sleep(4)
@app.task @app.task
def uninstall_agent_task(salt_id, has_nats): def agent_outage_email_task(pk: int, alert_interval: Union[float, None] = None) -> str:
attempts = 0 from alerts.models import Alert
error = False
if not has_nats: alert = Alert.objects.get(pk=pk)
while 1:
try:
r = requests.post( if not alert.email_sent:
f"http://{settings.SALT_HOST}:8123/run", sleep(random.randint(1, 15))
json=[ alert.agent.send_outage_email()
{ alert.email_sent = djangotime.now()
"client": "local", alert.save(update_fields=["email_sent"])
"tgt": salt_id,
"fun": "win_agent.uninstall_agent",
"timeout": 8,
"username": settings.SALT_USERNAME,
"password": settings.SALT_PASSWORD,
"eauth": "pam",
}
],
timeout=10,
)
ret = r.json()["return"][0][salt_id]
except Exception:
attempts += 1
else: else:
if ret != "ok": if alert_interval:
attempts += 1 # send an email only if the last email sent is older than alert interval
else: delta = djangotime.now() - dt.timedelta(days=alert_interval)
attempts = 0 if alert.email_sent < delta:
sleep(random.randint(1, 10))
if attempts >= 10: alert.agent.send_outage_email()
error = True alert.email_sent = djangotime.now()
break alert.save(update_fields=["email_sent"])
elif attempts == 0:
break
if error:
logger.error(f"{salt_id} uninstall failed")
else:
logger.info(f"{salt_id} was successfully uninstalled")
try:
r = requests.post(
f"http://{settings.SALT_HOST}:8123/run",
json=[
{
"client": "wheel",
"fun": "key.delete",
"match": salt_id,
"username": settings.SALT_USERNAME,
"password": settings.SALT_PASSWORD,
"eauth": "pam",
}
],
timeout=30,
)
except Exception:
logger.error(f"{salt_id} unable to remove salt-key")
return "ok" return "ok"
@app.task @app.task
def agent_outage_email_task(pk): def agent_recovery_email_task(pk: int) -> str:
from alerts.models import Alert
sleep(random.randint(1, 15)) sleep(random.randint(1, 15))
outage = AgentOutage.objects.get(pk=pk) alert = Alert.objects.get(pk=pk)
outage.send_outage_email() alert.agent.send_recovery_email()
outage.outage_email_sent = True alert.resolved_email_sent = djangotime.now()
outage.save(update_fields=["outage_email_sent"]) alert.save(update_fields=["resolved_email_sent"])
return "ok"
@app.task @app.task
def agent_recovery_email_task(pk): def agent_outage_sms_task(pk: int, alert_interval: Union[float, None] = None) -> str:
from alerts.models import Alert
alert = Alert.objects.get(pk=pk)
if not alert.sms_sent:
sleep(random.randint(1, 15)) sleep(random.randint(1, 15))
outage = AgentOutage.objects.get(pk=pk) alert.agent.send_outage_sms()
outage.send_recovery_email() alert.sms_sent = djangotime.now()
outage.recovery_email_sent = True alert.save(update_fields=["sms_sent"])
outage.save(update_fields=["recovery_email_sent"]) else:
if alert_interval:
# send an sms only if the last sms sent is older than alert interval
delta = djangotime.now() - dt.timedelta(days=alert_interval)
if alert.sms_sent < delta:
sleep(random.randint(1, 10))
alert.agent.send_outage_sms()
alert.sms_sent = djangotime.now()
alert.save(update_fields=["sms_sent"])
return "ok"
@app.task @app.task
def agent_outage_sms_task(pk): def agent_recovery_sms_task(pk: int) -> str:
from alerts.models import Alert
sleep(random.randint(1, 3)) sleep(random.randint(1, 3))
outage = AgentOutage.objects.get(pk=pk) alert = Alert.objects.get(pk=pk)
outage.send_outage_sms() alert.agent.send_recovery_sms()
outage.outage_sms_sent = True alert.resolved_sms_sent = djangotime.now()
outage.save(update_fields=["outage_sms_sent"]) alert.save(update_fields=["resolved_sms_sent"])
return "ok"
@app.task @app.task
def agent_recovery_sms_task(pk): def agent_outages_task() -> None:
sleep(random.randint(1, 3))
outage = AgentOutage.objects.get(pk=pk)
outage.send_recovery_sms()
outage.recovery_sms_sent = True
outage.save(update_fields=["recovery_sms_sent"])
@app.task
def agent_outages_task():
agents = Agent.objects.only( agents = Agent.objects.only(
"pk", "last_seen", "overdue_time", "overdue_email_alert", "overdue_text_alert" "pk",
"last_seen",
"offline_time",
"overdue_time",
"overdue_email_alert",
"overdue_text_alert",
"overdue_dashboard_alert",
) )
for agent in agents: for agent in agents:
if agent.overdue_email_alert or agent.overdue_text_alert:
if agent.status == "overdue": if agent.status == "overdue":
outages = AgentOutage.objects.filter(agent=agent) agent.handle_alert()
if outages and outages.last().is_active:
continue
outage = AgentOutage(agent=agent)
outage.save()
if agent.overdue_email_alert and not agent.maintenance_mode:
agent_outage_email_task.delay(pk=outage.pk)
if agent.overdue_text_alert and not agent.maintenance_mode:
agent_outage_sms_task.delay(pk=outage.pk)
@app.task @app.task
def install_salt_task(pk: int) -> None: def handle_agent_recovery_task(pk: int) -> None:
sleep(20) sleep(10)
agent = Agent.objects.get(pk=pk) from agents.models import RecoveryAction
asyncio.run(agent.nats_cmd({"func": "installsalt"}, wait=False))
action = RecoveryAction.objects.get(pk=pk)
if action.mode == "command":
data = {"func": "recoverycmd", "recoverycommand": action.command}
else:
data = {"func": "recover", "payload": {"mode": action.mode}}
asyncio.run(action.agent.nats_cmd(data, wait=False))
@app.task @app.task
def run_script_email_results_task( def run_script_email_results_task(
agentpk: int, scriptpk: int, nats_timeout: int, nats_data: dict, emails: List[str] agentpk: int, scriptpk: int, nats_timeout: int, emails: List[str]
): ):
agent = Agent.objects.get(pk=agentpk) agent = Agent.objects.get(pk=agentpk)
script = Script.objects.get(pk=scriptpk) script = Script.objects.get(pk=scriptpk)
nats_data["func"] = "runscriptfull" r = agent.run_script(scriptpk=script.pk, full=True, timeout=nats_timeout, wait=True)
r = asyncio.run(agent.nats_cmd(nats_data, timeout=nats_timeout))
if r == "timeout": if r == "timeout":
logger.error(f"{agent.hostname} timed out running script.") logger.error(f"{agent.hostname} timed out running script.")
return return

View File

@@ -4,22 +4,19 @@ from unittest.mock import patch
from model_bakery import baker from model_bakery import baker
from itertools import cycle from itertools import cycle
from typing import List
from packaging import version as pyver
from django.test import TestCase, override_settings
from django.conf import settings from django.conf import settings
from django.utils import timezone as djangotime
from logs.models import PendingAction from logs.models import PendingAction
from tacticalrmm.test import TacticalTestCase from tacticalrmm.test import TacticalTestCase
from .serializers import AgentSerializer from .serializers import AgentSerializer
from winupdate.serializers import WinUpdatePolicySerializer from winupdate.serializers import WinUpdatePolicySerializer
from .models import Agent from .models import Agent
from .tasks import ( from .tasks import auto_self_agent_update_task
agent_recovery_sms_task,
auto_self_agent_update_task,
sync_salt_modules_task,
batch_sync_modules_task,
)
from winupdate.models import WinUpdatePolicy from winupdate.models import WinUpdatePolicy
@@ -70,12 +67,34 @@ class TestAgentViews(TacticalTestCase):
@patch("agents.tasks.send_agent_update_task.delay") @patch("agents.tasks.send_agent_update_task.delay")
def test_update_agents(self, mock_task): def test_update_agents(self, mock_task):
url = "/agents/updateagents/" url = "/agents/updateagents/"
data = {"pks": [1, 2, 3, 5, 10], "version": "0.11.1"} baker.make_recipe(
"agents.agent",
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
version=settings.LATEST_AGENT_VER,
_quantity=15,
)
baker.make_recipe(
"agents.agent",
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
version="1.3.0",
_quantity=15,
)
pks: List[int] = list(
Agent.objects.only("pk", "version").values_list("pk", flat=True)
)
data = {"pks": pks}
expected: List[int] = [
i.pk
for i in Agent.objects.only("pk", "version")
if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
]
r = self.client.post(url, data, format="json") r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
mock_task.assert_called_with(pks=data["pks"], version=data["version"]) mock_task.assert_called_with(pks=expected)
self.check_not_authenticated("post", url) self.check_not_authenticated("post", url)
@@ -110,9 +129,8 @@ class TestAgentViews(TacticalTestCase):
self.check_not_authenticated("get", url) self.check_not_authenticated("get", url)
@patch("agents.models.Agent.nats_cmd") @patch("agents.models.Agent.nats_cmd")
@patch("agents.tasks.uninstall_agent_task.delay")
@patch("agents.views.reload_nats") @patch("agents.views.reload_nats")
def test_uninstall(self, reload_nats, mock_task, nats_cmd): def test_uninstall(self, reload_nats, nats_cmd):
url = "/agents/uninstall/" url = "/agents/uninstall/"
data = {"pk": self.agent.pk} data = {"pk": self.agent.pk}
@@ -121,13 +139,18 @@ class TestAgentViews(TacticalTestCase):
nats_cmd.assert_called_with({"func": "uninstall"}, wait=False) nats_cmd.assert_called_with({"func": "uninstall"}, wait=False)
reload_nats.assert_called_once() reload_nats.assert_called_once()
mock_task.assert_called_with(self.agent.salt_id, True)
self.check_not_authenticated("delete", url) self.check_not_authenticated("delete", url)
@patch("agents.models.Agent.nats_cmd") @patch("agents.models.Agent.nats_cmd")
def test_get_processes(self, mock_ret): def test_get_processes(self, mock_ret):
url = f"/agents/{self.agent.pk}/getprocs/" agent_old = baker.make_recipe("agents.online_agent", version="1.1.12")
url_old = f"/agents/{agent_old.pk}/getprocs/"
r = self.client.get(url_old)
self.assertEqual(r.status_code, 400)
agent = baker.make_recipe("agents.online_agent", version="1.2.0")
url = f"/agents/{agent.pk}/getprocs/"
with open( with open(
os.path.join(settings.BASE_DIR, "tacticalrmm/test_data/procs.json") os.path.join(settings.BASE_DIR, "tacticalrmm/test_data/procs.json")
@@ -137,9 +160,7 @@ class TestAgentViews(TacticalTestCase):
r = self.client.get(url) r = self.client.get(url)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
assert any(i["name"] == "Registry" for i in mock_ret.return_value) assert any(i["name"] == "Registry" for i in mock_ret.return_value)
assert any( assert any(i["membytes"] == 434655234324 for i in mock_ret.return_value)
i["memory_percent"] == 0.004843281375620747 for i in mock_ret.return_value
)
mock_ret.return_value = "timeout" mock_ret.return_value = "timeout"
r = self.client.get(url) r = self.client.get(url)
@@ -166,18 +187,44 @@ class TestAgentViews(TacticalTestCase):
self.check_not_authenticated("get", url) self.check_not_authenticated("get", url)
@patch("agents.models.Agent.nats_cmd") @patch("agents.models.Agent.nats_cmd")
def test_get_event_log(self, mock_ret): def test_get_event_log(self, nats_cmd):
url = f"/agents/{self.agent.pk}/geteventlog/Application/30/" url = f"/agents/{self.agent.pk}/geteventlog/Application/22/"
with open( with open(
os.path.join(settings.BASE_DIR, "tacticalrmm/test_data/appeventlog.json") os.path.join(settings.BASE_DIR, "tacticalrmm/test_data/appeventlog.json")
) as f: ) as f:
mock_ret.return_value = json.load(f) nats_cmd.return_value = json.load(f)
r = self.client.get(url) r = self.client.get(url)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
nats_cmd.assert_called_with(
{
"func": "eventlog",
"timeout": 30,
"payload": {
"logname": "Application",
"days": str(22),
},
},
timeout=32,
)
mock_ret.return_value = "timeout" url = f"/agents/{self.agent.pk}/geteventlog/Security/6/"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
nats_cmd.assert_called_with(
{
"func": "eventlog",
"timeout": 180,
"payload": {
"logname": "Security",
"days": str(6),
},
},
timeout=182,
)
nats_cmd.return_value = "timeout"
r = self.client.get(url) r = self.client.get(url)
self.assertEqual(r.status_code, 400) self.assertEqual(r.status_code, 400)
@@ -312,7 +359,6 @@ class TestAgentViews(TacticalTestCase):
r = self.client.post(url, data, format="json") r = self.client.post(url, data, format="json")
self.assertIn("rdp", r.json()["cmd"]) self.assertIn("rdp", r.json()["cmd"])
self.assertNotIn("power", r.json()["cmd"]) self.assertNotIn("power", r.json()["cmd"])
self.assertNotIn("ping", r.json()["cmd"])
data.update({"ping": 1, "power": 1}) data.update({"ping": 1, "power": 1})
r = self.client.post(url, data, format="json") r = self.client.post(url, data, format="json")
@@ -331,7 +377,7 @@ class TestAgentViews(TacticalTestCase):
r = self.client.post(url, data, format="json") r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
data["mode"] = "salt" data["mode"] = "mesh"
r = self.client.post(url, data, format="json") r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 400) self.assertEqual(r.status_code, 400)
self.assertIn("pending", r.json()) self.assertIn("pending", r.json())
@@ -351,7 +397,7 @@ class TestAgentViews(TacticalTestCase):
self.agent.version = "0.9.4" self.agent.version = "0.9.4"
self.agent.save(update_fields=["version"]) self.agent.save(update_fields=["version"])
data["mode"] = "salt" data["mode"] = "mesh"
r = self.client.post(url, data, format="json") r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 400) self.assertEqual(r.status_code, 400)
self.assertIn("0.9.5", r.json()) self.assertIn("0.9.5", r.json())
@@ -385,6 +431,7 @@ class TestAgentViews(TacticalTestCase):
"site": site.id, "site": site.id,
"monitoring_type": "workstation", "monitoring_type": "workstation",
"description": "asjdk234andasd", "description": "asjdk234andasd",
"offline_time": 4,
"overdue_time": 300, "overdue_time": 300,
"check_interval": 60, "check_interval": 60,
"overdue_email_alert": True, "overdue_email_alert": True,
@@ -483,42 +530,20 @@ class TestAgentViews(TacticalTestCase):
def test_overdue_action(self): def test_overdue_action(self):
url = "/agents/overdueaction/" url = "/agents/overdueaction/"
payload = {"pk": self.agent.pk, "alertType": "email", "action": "enabled"} payload = {"pk": self.agent.pk, "overdue_email_alert": True}
r = self.client.post(url, payload, format="json") r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
agent = Agent.objects.get(pk=self.agent.pk) agent = Agent.objects.get(pk=self.agent.pk)
self.assertTrue(agent.overdue_email_alert) self.assertTrue(agent.overdue_email_alert)
self.assertEqual(self.agent.hostname, r.data) self.assertEqual(self.agent.hostname, r.data)
payload.update({"alertType": "email", "action": "disabled"}) payload = {"pk": self.agent.pk, "overdue_text_alert": False}
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 200)
agent = Agent.objects.get(pk=self.agent.pk)
self.assertFalse(agent.overdue_email_alert)
self.assertEqual(self.agent.hostname, r.data)
payload.update({"alertType": "text", "action": "enabled"})
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 200)
agent = Agent.objects.get(pk=self.agent.pk)
self.assertTrue(agent.overdue_text_alert)
self.assertEqual(self.agent.hostname, r.data)
payload.update({"alertType": "text", "action": "disabled"})
r = self.client.post(url, payload, format="json") r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
agent = Agent.objects.get(pk=self.agent.pk) agent = Agent.objects.get(pk=self.agent.pk)
self.assertFalse(agent.overdue_text_alert) self.assertFalse(agent.overdue_text_alert)
self.assertEqual(self.agent.hostname, r.data) self.assertEqual(self.agent.hostname, r.data)
payload.update({"alertType": "email", "action": "523423"})
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 400)
payload.update({"alertType": "text", "action": "asdasd3434asdasd"})
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 400)
self.check_not_authenticated("post", url) self.check_not_authenticated("post", url)
def test_list_agents_no_detail(self): def test_list_agents_no_detail(self):
@@ -539,7 +564,7 @@ class TestAgentViews(TacticalTestCase):
self.check_not_authenticated("get", url) self.check_not_authenticated("get", url)
@patch("winupdate.tasks.bulk_check_for_updates_task.delay") """ @patch("winupdate.tasks.bulk_check_for_updates_task.delay")
@patch("scripts.tasks.handle_bulk_script_task.delay") @patch("scripts.tasks.handle_bulk_script_task.delay")
@patch("scripts.tasks.handle_bulk_command_task.delay") @patch("scripts.tasks.handle_bulk_command_task.delay")
@patch("agents.models.Agent.salt_batch_async") @patch("agents.models.Agent.salt_batch_async")
@@ -581,7 +606,7 @@ class TestAgentViews(TacticalTestCase):
r = self.client.post(url, payload, format="json") r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 400) self.assertEqual(r.status_code, 400)
""" payload = { payload = {
"mode": "command", "mode": "command",
"monType": "workstations", "monType": "workstations",
"target": "client", "target": "client",
@@ -595,7 +620,7 @@ class TestAgentViews(TacticalTestCase):
r = self.client.post(url, payload, format="json") r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
bulk_command.assert_called_with([self.agent.pk], "gpupdate /force", "cmd", 300) """ bulk_command.assert_called_with([self.agent.pk], "gpupdate /force", "cmd", 300)
payload = { payload = {
"mode": "command", "mode": "command",
@@ -653,7 +678,7 @@ class TestAgentViews(TacticalTestCase):
# TODO mock the script # TODO mock the script
self.check_not_authenticated("post", url) self.check_not_authenticated("post", url) """
@patch("agents.models.Agent.nats_cmd") @patch("agents.models.Agent.nats_cmd")
def test_recover_mesh(self, nats_cmd): def test_recover_mesh(self, nats_cmd):
@@ -680,6 +705,7 @@ class TestAgentViews(TacticalTestCase):
class TestAgentViewsNew(TacticalTestCase): class TestAgentViewsNew(TacticalTestCase):
def setUp(self): def setUp(self):
self.authenticate() self.authenticate()
self.setup_coresettings()
def test_agent_counts(self): def test_agent_counts(self):
url = "/agents/agent_counts/" url = "/agents/agent_counts/"
@@ -690,15 +716,12 @@ class TestAgentViewsNew(TacticalTestCase):
monitoring_type=cycle(["server", "workstation"]), monitoring_type=cycle(["server", "workstation"]),
_quantity=6, _quantity=6,
) )
agents = baker.make_recipe( baker.make_recipe(
"agents.overdue_agent", "agents.overdue_agent",
monitoring_type=cycle(["server", "workstation"]), monitoring_type=cycle(["server", "workstation"]),
_quantity=6, _quantity=6,
) )
# make an AgentOutage for every overdue agent
baker.make("agents.AgentOutage", agent=cycle(agents), _quantity=6)
# returned data should be this # returned data should be this
data = { data = {
"total_server_count": 6, "total_server_count": 6,
@@ -755,41 +778,6 @@ class TestAgentTasks(TacticalTestCase):
self.authenticate() self.authenticate()
self.setup_coresettings() self.setup_coresettings()
@patch("agents.models.Agent.salt_api_cmd")
def test_sync_salt_modules_task(self, salt_api_cmd):
self.agent = baker.make_recipe("agents.agent")
salt_api_cmd.return_value = {"return": [{f"{self.agent.salt_id}": []}]}
ret = sync_salt_modules_task.s(self.agent.pk).apply()
salt_api_cmd.assert_called_with(timeout=35, func="saltutil.sync_modules")
self.assertEqual(
ret.result, f"Successfully synced salt modules on {self.agent.hostname}"
)
self.assertEqual(ret.status, "SUCCESS")
salt_api_cmd.return_value = "timeout"
ret = sync_salt_modules_task.s(self.agent.pk).apply()
self.assertEqual(ret.result, f"Unable to sync modules {self.agent.salt_id}")
salt_api_cmd.return_value = "error"
ret = sync_salt_modules_task.s(self.agent.pk).apply()
self.assertEqual(ret.result, f"Unable to sync modules {self.agent.salt_id}")
@patch("agents.models.Agent.salt_batch_async", return_value=None)
@patch("agents.tasks.sleep", return_value=None)
def test_batch_sync_modules_task(self, mock_sleep, salt_batch_async):
# chunks of 50, should run 4 times
baker.make_recipe(
"agents.online_agent", last_seen=djangotime.now(), _quantity=60
)
baker.make_recipe(
"agents.overdue_agent",
last_seen=djangotime.now() - djangotime.timedelta(minutes=9),
_quantity=115,
)
ret = batch_sync_modules_task.s().apply()
self.assertEqual(salt_batch_async.call_count, 4)
self.assertEqual(ret.status, "SUCCESS")
@patch("agents.models.Agent.nats_cmd") @patch("agents.models.Agent.nats_cmd")
def test_agent_update(self, nats_cmd): def test_agent_update(self, nats_cmd):
from agents.tasks import agent_update from agents.tasks import agent_update
@@ -797,41 +785,55 @@ class TestAgentTasks(TacticalTestCase):
agent_noarch = baker.make_recipe( agent_noarch = baker.make_recipe(
"agents.agent", "agents.agent",
operating_system="Error getting OS", operating_system="Error getting OS",
version="1.1.11", version=settings.LATEST_AGENT_VER,
) )
r = agent_update(agent_noarch.pk) r = agent_update(agent_noarch.pk)
self.assertEqual(r, "noarch") self.assertEqual(r, "noarch")
self.assertEqual(
PendingAction.objects.filter(
agent=agent_noarch, action_type="agentupdate"
).count(),
0,
)
agent64_111 = baker.make_recipe( agent_1111 = baker.make_recipe(
"agents.agent", "agents.agent",
operating_system="Windows 10 Pro, 64 bit (build 19041.450)", operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
version="1.1.11", version="1.1.11",
) )
r = agent_update(agent_1111.pk)
self.assertEqual(r, "not supported")
r = agent_update(agent64_111.pk) agent64_1112 = baker.make_recipe(
self.assertEqual(r, "created")
action = PendingAction.objects.get(agent__pk=agent64_111.pk)
self.assertEqual(action.action_type, "agentupdate")
self.assertEqual(action.status, "pending")
self.assertEqual(action.details["url"], settings.DL_64)
self.assertEqual(
action.details["inno"], f"winagent-v{settings.LATEST_AGENT_VER}.exe"
)
self.assertEqual(action.details["version"], settings.LATEST_AGENT_VER)
agent64 = baker.make_recipe(
"agents.agent", "agents.agent",
operating_system="Windows 10 Pro, 64 bit (build 19041.450)", operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
version="1.1.12", version="1.1.12",
) )
r = agent_update(agent64_1112.pk)
self.assertEqual(r, "created")
action = PendingAction.objects.get(agent__pk=agent64_1112.pk)
self.assertEqual(action.action_type, "agentupdate")
self.assertEqual(action.status, "pending")
self.assertEqual(
action.details["url"],
"https://github.com/wh1te909/rmmagent/releases/download/v1.3.0/winagent-v1.3.0.exe",
)
self.assertEqual(action.details["inno"], "winagent-v1.3.0.exe")
self.assertEqual(action.details["version"], "1.3.0")
nats_cmd.assert_called_with(
{
"func": "agentupdate",
"payload": {
"url": "https://github.com/wh1te909/rmmagent/releases/download/v1.3.0/winagent-v1.3.0.exe",
"version": "1.3.0",
"inno": "winagent-v1.3.0.exe",
},
},
wait=False,
)
agent_64_130 = baker.make_recipe(
"agents.agent",
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
version="1.3.0",
)
nats_cmd.return_value = "ok" nats_cmd.return_value = "ok"
r = agent_update(agent64.pk) r = agent_update(agent_64_130.pk)
self.assertEqual(r, "created") self.assertEqual(r, "created")
nats_cmd.assert_called_with( nats_cmd.assert_called_with(
{ {
@@ -844,108 +846,34 @@ class TestAgentTasks(TacticalTestCase):
}, },
wait=False, wait=False,
) )
action = PendingAction.objects.get(agent__pk=agent_64_130.pk)
self.assertEqual(action.action_type, "agentupdate")
self.assertEqual(action.status, "pending")
""" @patch("agents.models.Agent.salt_api_async") @patch("agents.tasks.agent_update")
@patch("agents.tasks.sleep", return_value=None) @patch("agents.tasks.sleep", return_value=None)
def test_auto_self_agent_update_task(self, mock_sleep, salt_api_async): def test_auto_self_agent_update_task(self, mock_sleep, agent_update):
# test 64bit golang agent baker.make_recipe(
self.agent64 = baker.make_recipe(
"agents.agent", "agents.agent",
operating_system="Windows 10 Pro, 64 bit (build 19041.450)", operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
version="1.0.0", version=settings.LATEST_AGENT_VER,
_quantity=23,
) )
salt_api_async.return_value = True baker.make_recipe(
ret = auto_self_agent_update_task.s().apply()
salt_api_async.assert_called_with(
func="win_agent.do_agent_update_v2",
kwargs={
"inno": f"winagent-v{settings.LATEST_AGENT_VER}.exe",
"url": settings.DL_64,
},
)
self.assertEqual(ret.status, "SUCCESS")
self.agent64.delete()
salt_api_async.reset_mock()
# test 32bit golang agent
self.agent32 = baker.make_recipe(
"agents.agent",
operating_system="Windows 7 Professional, 32 bit (build 7601.24544)",
version="1.0.0",
)
salt_api_async.return_value = True
ret = auto_self_agent_update_task.s().apply()
salt_api_async.assert_called_with(
func="win_agent.do_agent_update_v2",
kwargs={
"inno": f"winagent-v{settings.LATEST_AGENT_VER}-x86.exe",
"url": settings.DL_32,
},
)
self.assertEqual(ret.status, "SUCCESS")
self.agent32.delete()
salt_api_async.reset_mock()
# test agent that has a null os field
self.agentNone = baker.make_recipe(
"agents.agent",
operating_system=None,
version="1.0.0",
)
ret = auto_self_agent_update_task.s().apply()
salt_api_async.assert_not_called()
self.agentNone.delete()
salt_api_async.reset_mock()
# test auto update disabled in global settings
self.agent64 = baker.make_recipe(
"agents.agent", "agents.agent",
operating_system="Windows 10 Pro, 64 bit (build 19041.450)", operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
version="1.0.0", version="1.3.0",
_quantity=33,
) )
self.coresettings.agent_auto_update = False self.coresettings.agent_auto_update = False
self.coresettings.save(update_fields=["agent_auto_update"]) self.coresettings.save(update_fields=["agent_auto_update"])
ret = auto_self_agent_update_task.s().apply()
salt_api_async.assert_not_called()
# reset core settings r = auto_self_agent_update_task.s().apply()
self.agent64.delete() self.assertEqual(agent_update.call_count, 0)
salt_api_async.reset_mock()
self.coresettings.agent_auto_update = True self.coresettings.agent_auto_update = True
self.coresettings.save(update_fields=["agent_auto_update"]) self.coresettings.save(update_fields=["agent_auto_update"])
# test 64bit python agent r = auto_self_agent_update_task.s().apply()
self.agent64py = baker.make_recipe( self.assertEqual(agent_update.call_count, 33)
"agents.agent",
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
version="0.11.1",
)
salt_api_async.return_value = True
ret = auto_self_agent_update_task.s().apply()
salt_api_async.assert_called_with(
func="win_agent.do_agent_update_v2",
kwargs={
"inno": "winagent-v0.11.2.exe",
"url": OLD_64_PY_AGENT,
},
)
self.assertEqual(ret.status, "SUCCESS")
self.agent64py.delete()
salt_api_async.reset_mock()
# test 32bit python agent
self.agent32py = baker.make_recipe(
"agents.agent",
operating_system="Windows 7 Professional, 32 bit (build 7601.24544)",
version="0.11.1",
)
salt_api_async.return_value = True
ret = auto_self_agent_update_task.s().apply()
salt_api_async.assert_called_with(
func="win_agent.do_agent_update_v2",
kwargs={
"inno": "winagent-v0.11.2-x86.exe",
"url": OLD_32_PY_AGENT,
},
)
self.assertEqual(ret.status, "SUCCESS") """

View File

@@ -7,6 +7,7 @@ import random
import string import string
import datetime as dt import datetime as dt
from packaging import version as pyver from packaging import version as pyver
from typing import List
from django.conf import settings from django.conf import settings
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
@@ -17,7 +18,7 @@ from rest_framework.views import APIView
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import status, generics from rest_framework import status, generics
from .models import Agent, AgentOutage, RecoveryAction, Note from .models import Agent, RecoveryAction, Note
from core.models import CoreSettings from core.models import CoreSettings
from scripts.models import Script from scripts.models import Script
from logs.models import AuditLog, PendingAction from logs.models import AuditLog, PendingAction
@@ -29,15 +30,15 @@ from .serializers import (
AgentEditSerializer, AgentEditSerializer,
NoteSerializer, NoteSerializer,
NotesSerializer, NotesSerializer,
AgentOverdueActionSerializer,
) )
from winupdate.serializers import WinUpdatePolicySerializer from winupdate.serializers import WinUpdatePolicySerializer
from .tasks import ( from .tasks import (
uninstall_agent_task,
send_agent_update_task, send_agent_update_task,
run_script_email_results_task, run_script_email_results_task,
) )
from winupdate.tasks import bulk_check_for_updates_task from winupdate.tasks import bulk_check_for_updates_task, bulk_install_updates_task
from scripts.tasks import handle_bulk_command_task, handle_bulk_script_task from scripts.tasks import handle_bulk_command_task, handle_bulk_script_task
from tacticalrmm.utils import notify_error, reload_nats from tacticalrmm.utils import notify_error, reload_nats
@@ -58,9 +59,13 @@ def get_agent_versions(request):
@api_view(["POST"]) @api_view(["POST"])
def update_agents(request): def update_agents(request):
pks = request.data["pks"] q = Agent.objects.filter(pk__in=request.data["pks"]).only("pk", "version")
version = request.data["version"] pks: List[int] = [
send_agent_update_task.delay(pks=pks, version=version) i.pk
for i in q
if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
]
send_agent_update_task.delay(pks=pks)
return Response("ok") return Response("ok")
@@ -72,10 +77,6 @@ def ping(request, pk):
r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=5)) r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=5))
if r == "pong": if r == "pong":
status = "online" status = "online"
else:
r = agent.salt_api_cmd(timeout=5, func="test.ping")
if isinstance(r, bool) and r:
status = "online"
return Response({"name": agent.hostname, "status": status}) return Response({"name": agent.hostname, "status": status})
@@ -86,13 +87,9 @@ def uninstall(request):
if agent.has_nats: if agent.has_nats:
asyncio.run(agent.nats_cmd({"func": "uninstall"}, wait=False)) asyncio.run(agent.nats_cmd({"func": "uninstall"}, wait=False))
salt_id = agent.salt_id
name = agent.hostname name = agent.hostname
has_nats = agent.has_nats
agent.delete() agent.delete()
reload_nats() reload_nats()
uninstall_agent_task.delay(salt_id, has_nats)
return Response(f"{name} will now be uninstalled.") return Response(f"{name} will now be uninstalled.")
@@ -100,11 +97,11 @@ def uninstall(request):
def edit_agent(request): def edit_agent(request):
agent = get_object_or_404(Agent, pk=request.data["id"]) agent = get_object_or_404(Agent, pk=request.data["id"])
old_site = agent.site.pk
a_serializer = AgentSerializer(instance=agent, data=request.data, partial=True) a_serializer = AgentSerializer(instance=agent, data=request.data, partial=True)
a_serializer.is_valid(raise_exception=True) a_serializer.is_valid(raise_exception=True)
a_serializer.save() a_serializer.save()
if "winupdatepolicy" in request.data.keys():
policy = agent.winupdatepolicy.get() policy = agent.winupdatepolicy.get()
p_serializer = WinUpdatePolicySerializer( p_serializer = WinUpdatePolicySerializer(
instance=policy, data=request.data["winupdatepolicy"][0] instance=policy, data=request.data["winupdatepolicy"][0]
@@ -112,11 +109,6 @@ def edit_agent(request):
p_serializer.is_valid(raise_exception=True) p_serializer.is_valid(raise_exception=True)
p_serializer.save() p_serializer.save()
# check if site changed and initiate generating correct policies
if old_site != request.data["site"]:
agent.generate_checks_from_policies(clear=True)
agent.generate_tasks_from_policies(clear=True)
return Response("ok") return Response("ok")
@@ -159,12 +151,12 @@ def agent_detail(request, pk):
@api_view() @api_view()
def get_processes(request, pk): def get_processes(request, pk):
agent = get_object_or_404(Agent, pk=pk) agent = get_object_or_404(Agent, pk=pk)
if not agent.has_nats: if pyver.parse(agent.version) < pyver.parse("1.2.0"):
return notify_error("Requires agent version 1.1.0 or greater") return notify_error("Requires agent version 1.2.0 or greater")
r = asyncio.run(agent.nats_cmd(data={"func": "procs"}, timeout=5)) r = asyncio.run(agent.nats_cmd(data={"func": "procs"}, timeout=5))
if r == "timeout": if r == "timeout":
return notify_error("Unable to contact the agent") return notify_error("Unable to contact the agent")
return Response(r) return Response(r)
@@ -191,15 +183,16 @@ def get_event_log(request, pk, logtype, days):
agent = get_object_or_404(Agent, pk=pk) agent = get_object_or_404(Agent, pk=pk)
if not agent.has_nats: if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater") return notify_error("Requires agent version 1.1.0 or greater")
timeout = 180 if logtype == "Security" else 30
data = { data = {
"func": "eventlog", "func": "eventlog",
"timeout": 30, "timeout": timeout,
"payload": { "payload": {
"logname": logtype, "logname": logtype,
"days": str(days), "days": str(days),
}, },
} }
r = asyncio.run(agent.nats_cmd(data, timeout=32)) r = asyncio.run(agent.nats_cmd(data, timeout=timeout + 2))
if r == "timeout": if r == "timeout":
return notify_error("Unable to contact the agent") return notify_error("Unable to contact the agent")
@@ -250,6 +243,7 @@ class AgentsTableList(generics.ListAPIView):
"overdue_text_alert", "overdue_text_alert",
"overdue_email_alert", "overdue_email_alert",
"overdue_time", "overdue_time",
"offline_time",
"last_seen", "last_seen",
"boot_time", "boot_time",
"logged_in_username", "logged_in_username",
@@ -298,6 +292,7 @@ def by_client(request, clientpk):
"overdue_text_alert", "overdue_text_alert",
"overdue_email_alert", "overdue_email_alert",
"overdue_time", "overdue_time",
"offline_time",
"last_seen", "last_seen",
"boot_time", "boot_time",
"logged_in_username", "logged_in_username",
@@ -327,6 +322,7 @@ def by_site(request, sitepk):
"overdue_text_alert", "overdue_text_alert",
"overdue_email_alert", "overdue_email_alert",
"overdue_time", "overdue_time",
"offline_time",
"last_seen", "last_seen",
"boot_time", "boot_time",
"logged_in_username", "logged_in_username",
@@ -341,26 +337,12 @@ def by_site(request, sitepk):
@api_view(["POST"]) @api_view(["POST"])
def overdue_action(request): def overdue_action(request):
pk = request.data["pk"] agent = get_object_or_404(Agent, pk=request.data["pk"])
alert_type = request.data["alertType"] serializer = AgentOverdueActionSerializer(
action = request.data["action"] instance=agent, data=request.data, partial=True
agent = get_object_or_404(Agent, pk=pk)
if alert_type == "email" and action == "enabled":
agent.overdue_email_alert = True
agent.save(update_fields=["overdue_email_alert"])
elif alert_type == "email" and action == "disabled":
agent.overdue_email_alert = False
agent.save(update_fields=["overdue_email_alert"])
elif alert_type == "text" and action == "enabled":
agent.overdue_text_alert = True
agent.save(update_fields=["overdue_text_alert"])
elif alert_type == "text" and action == "disabled":
agent.overdue_text_alert = False
agent.save(update_fields=["overdue_text_alert"])
else:
return Response(
{"error": "Something went wrong"}, status=status.HTTP_400_BAD_REQUEST
) )
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(agent.hostname) return Response(agent.hostname)
@@ -481,7 +463,7 @@ def install_agent(request):
f"GOARCH={goarch}", f"GOARCH={goarch}",
go_bin, go_bin,
"build", "build",
f"-ldflags=\"-X 'main.Inno={inno}'", f"-ldflags=\"-s -w -X 'main.Inno={inno}'",
f"-X 'main.Api={api}'", f"-X 'main.Api={api}'",
f"-X 'main.Client={client_id}'", f"-X 'main.Client={client_id}'",
f"-X 'main.Site={site_id}'", f"-X 'main.Site={site_id}'",
@@ -579,12 +561,10 @@ def install_agent(request):
"/VERYSILENT", "/VERYSILENT",
"/SUPPRESSMSGBOXES", "/SUPPRESSMSGBOXES",
"&&", "&&",
"timeout", "ping",
"/t", "127.0.0.1",
"10", "-n",
"/nobreak", "5",
">",
"NUL",
"&&", "&&",
r'"C:\Program Files\TacticalAgent\tacticalrmm.exe"', r'"C:\Program Files\TacticalAgent\tacticalrmm.exe"',
"-m", "-m",
@@ -611,8 +591,6 @@ def install_agent(request):
resp = { resp = {
"cmd": " ".join(str(i) for i in cmd), "cmd": " ".join(str(i) for i in cmd),
"url": download_url, "url": download_url,
"salt64": settings.SALT_64,
"salt32": settings.SALT_32,
} }
return Response(resp) return Response(resp)
@@ -673,17 +651,12 @@ def recover(request):
return notify_error("Only available in agent version greater than 0.9.5") return notify_error("Only available in agent version greater than 0.9.5")
if not agent.has_nats: if not agent.has_nats:
if mode == "tacagent" or mode == "checkrunner" or mode == "rpc": if mode == "tacagent" or mode == "rpc":
return notify_error("Requires agent version 1.1.0 or greater") return notify_error("Requires agent version 1.1.0 or greater")
# attempt a realtime recovery if supported, otherwise fall back to old recovery method # attempt a realtime recovery if supported, otherwise fall back to old recovery method
if agent.has_nats: if agent.has_nats:
if ( if mode == "tacagent" or mode == "mesh":
mode == "tacagent"
or mode == "checkrunner"
or mode == "salt"
or mode == "mesh"
):
data = {"func": "recover", "payload": {"mode": mode}} data = {"func": "recover", "payload": {"mode": mode}}
r = asyncio.run(agent.nats_cmd(data, timeout=10)) r = asyncio.run(agent.nats_cmd(data, timeout=10))
if r == "ok": if r == "ok":
@@ -729,19 +702,10 @@ def run_script(request):
script=script.name, script=script.name,
) )
data = {
"func": "runscript",
"timeout": request.data["timeout"],
"script_args": request.data["args"],
"payload": {
"code": script.code,
"shell": script.shell,
},
}
if output == "wait": if output == "wait":
r = asyncio.run(agent.nats_cmd(data, timeout=req_timeout)) r = agent.run_script(scriptpk=script.pk, timeout=req_timeout, wait=True)
return Response(r) return Response(r)
elif output == "email": elif output == "email":
if not pyver.parse(agent.version) >= pyver.parse("1.1.12"): if not pyver.parse(agent.version) >= pyver.parse("1.1.12"):
return notify_error("Requires agent version 1.1.12 or greater") return notify_error("Requires agent version 1.1.12 or greater")
@@ -753,12 +717,11 @@ def run_script(request):
agentpk=agent.pk, agentpk=agent.pk,
scriptpk=script.pk, scriptpk=script.pk,
nats_timeout=req_timeout, nats_timeout=req_timeout,
nats_data=data,
emails=emails, emails=emails,
) )
return Response(f"{script.name} will now be run on {agent.hostname}")
else: else:
asyncio.run(agent.nats_cmd(data, wait=False)) agent.run_script(scriptpk=script.pk, timeout=req_timeout)
return Response(f"{script.name} will now be run on {agent.hostname}") return Response(f"{script.name} will now be run on {agent.hostname}")
@@ -840,7 +803,7 @@ def bulk(request):
elif request.data["target"] == "agents": elif request.data["target"] == "agents":
q = Agent.objects.filter(pk__in=request.data["agentPKs"]) q = Agent.objects.filter(pk__in=request.data["agentPKs"])
elif request.data["target"] == "all": elif request.data["target"] == "all":
q = Agent.objects.all() q = Agent.objects.only("pk", "monitoring_type")
else: else:
return notify_error("Something went wrong") return notify_error("Something went wrong")
@@ -849,8 +812,7 @@ def bulk(request):
elif request.data["monType"] == "workstations": elif request.data["monType"] == "workstations":
q = q.filter(monitoring_type="workstation") q = q.filter(monitoring_type="workstation")
minions = [agent.salt_id for agent in q] agents: List[int] = [agent.pk for agent in q]
agents = [agent.pk for agent in q]
AuditLog.audit_bulk_action(request.user, request.data["mode"], request.data) AuditLog.audit_bulk_action(request.user, request.data["mode"], request.data)
@@ -868,14 +830,12 @@ def bulk(request):
return Response(f"{script.name} will now be run on {len(agents)} agents") return Response(f"{script.name} will now be run on {len(agents)} agents")
elif request.data["mode"] == "install": elif request.data["mode"] == "install":
r = Agent.salt_batch_async(minions=minions, func="win_agent.install_updates") bulk_install_updates_task.delay(agents)
if r == "timeout":
return notify_error("Salt API not running")
return Response( return Response(
f"Pending updates will now be installed on {len(agents)} agents" f"Pending updates will now be installed on {len(agents)} agents"
) )
elif request.data["mode"] == "scan": elif request.data["mode"] == "scan":
bulk_check_for_updates_task.delay(minions=minions) bulk_check_for_updates_task.delay(agents)
return Response(f"Patch status scan will now run on {len(agents)} agents") return Response(f"Patch status scan will now run on {len(agents)} agents")
return notify_error("Something went wrong") return notify_error("Something went wrong")
@@ -883,20 +843,43 @@ def bulk(request):
@api_view(["POST"]) @api_view(["POST"])
def agent_counts(request): def agent_counts(request):
server_offline_count = len(
[
agent
for agent in Agent.objects.filter(monitoring_type="server").only(
"pk",
"last_seen",
"overdue_time",
"offline_time",
)
if not agent.status == "online"
]
)
workstation_offline_count = len(
[
agent
for agent in Agent.objects.filter(monitoring_type="workstation").only(
"pk",
"last_seen",
"overdue_time",
"offline_time",
)
if not agent.status == "online"
]
)
return Response( return Response(
{ {
"total_server_count": Agent.objects.filter( "total_server_count": Agent.objects.filter(
monitoring_type="server" monitoring_type="server"
).count(), ).count(),
"total_server_offline_count": AgentOutage.objects.filter( "total_server_offline_count": server_offline_count,
recovery_time=None, agent__monitoring_type="server"
).count(),
"total_workstation_count": Agent.objects.filter( "total_workstation_count": Agent.objects.filter(
monitoring_type="workstation" monitoring_type="workstation"
).count(), ).count(),
"total_workstation_offline_count": AgentOutage.objects.filter( "total_workstation_offline_count": workstation_offline_count,
recovery_time=None, agent__monitoring_type="workstation"
).count(),
} }
) )

View File

@@ -1,6 +1,7 @@
from django.contrib import admin from django.contrib import admin
from .models import Alert from .models import Alert, AlertTemplate
admin.site.register(Alert) admin.site.register(Alert)
admin.site.register(AlertTemplate)

View File

@@ -0,0 +1,172 @@
# Generated by Django 3.1.4 on 2021-02-12 14:08
import django.contrib.postgres.fields
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('agents', '0029_delete_agentoutage'),
('clients', '0008_auto_20201103_1430'),
('autotasks', '0017_auto_20210210_1512'),
('scripts', '0005_auto_20201207_1606'),
('alerts', '0003_auto_20201021_1815'),
]
operations = [
migrations.AddField(
model_name='alert',
name='action_execution_time',
field=models.CharField(blank=True, max_length=100, null=True),
),
migrations.AddField(
model_name='alert',
name='action_retcode',
field=models.IntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='alert',
name='action_run',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='alert',
name='action_stderr',
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
model_name='alert',
name='action_stdout',
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
model_name='alert',
name='action_timeout',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='alert',
name='alert_type',
field=models.CharField(choices=[('availability', 'Availability'), ('check', 'Check'), ('task', 'Task'), ('custom', 'Custom')], default='availability', max_length=20),
),
migrations.AddField(
model_name='alert',
name='assigned_task',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='alert', to='autotasks.automatedtask'),
),
migrations.AddField(
model_name='alert',
name='email_sent',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='alert',
name='hidden',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='alert',
name='resolved_action_execution_time',
field=models.CharField(blank=True, max_length=100, null=True),
),
migrations.AddField(
model_name='alert',
name='resolved_action_retcode',
field=models.IntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='alert',
name='resolved_action_run',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='alert',
name='resolved_action_stderr',
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
model_name='alert',
name='resolved_action_stdout',
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
model_name='alert',
name='resolved_action_timeout',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='alert',
name='resolved_email_sent',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='alert',
name='resolved_on',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='alert',
name='resolved_sms_sent',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='alert',
name='sms_sent',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='alert',
name='snoozed',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='alert',
name='severity',
field=models.CharField(choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], default='info', max_length=30),
),
migrations.CreateModel(
name='AlertTemplate',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('is_active', models.BooleanField(default=True)),
('action_args', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=255, null=True), blank=True, default=list, null=True, size=None)),
('resolved_action_args', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=255, null=True), blank=True, default=list, null=True, size=None)),
('email_recipients', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=100), blank=True, default=list, null=True, size=None)),
('text_recipients', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=100), blank=True, default=list, null=True, size=None)),
('email_from', models.EmailField(blank=True, max_length=254, null=True)),
('agent_email_on_resolved', models.BooleanField(blank=True, default=False, null=True)),
('agent_text_on_resolved', models.BooleanField(blank=True, default=False, null=True)),
('agent_include_desktops', models.BooleanField(blank=True, default=False, null=True)),
('agent_always_email', models.BooleanField(blank=True, default=False, null=True)),
('agent_always_text', models.BooleanField(blank=True, default=False, null=True)),
('agent_always_alert', models.BooleanField(blank=True, default=False, null=True)),
('agent_periodic_alert_days', models.PositiveIntegerField(blank=True, default=0, null=True)),
('check_email_alert_severity', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], max_length=25), blank=True, default=list, size=None)),
('check_text_alert_severity', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], max_length=25), blank=True, default=list, size=None)),
('check_dashboard_alert_severity', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], max_length=25), blank=True, default=list, size=None)),
('check_email_on_resolved', models.BooleanField(blank=True, default=False, null=True)),
('check_text_on_resolved', models.BooleanField(blank=True, default=False, null=True)),
('check_always_email', models.BooleanField(blank=True, default=False, null=True)),
('check_always_text', models.BooleanField(blank=True, default=False, null=True)),
('check_always_alert', models.BooleanField(blank=True, default=False, null=True)),
('check_periodic_alert_days', models.PositiveIntegerField(blank=True, default=0, null=True)),
('task_email_alert_severity', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], max_length=25), blank=True, default=list, size=None)),
('task_text_alert_severity', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], max_length=25), blank=True, default=list, size=None)),
('task_dashboard_alert_severity', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], max_length=25), blank=True, default=list, size=None)),
('task_email_on_resolved', models.BooleanField(blank=True, default=False, null=True)),
('task_text_on_resolved', models.BooleanField(blank=True, default=False, null=True)),
('task_always_email', models.BooleanField(blank=True, default=False, null=True)),
('task_always_text', models.BooleanField(blank=True, default=False, null=True)),
('task_always_alert', models.BooleanField(blank=True, default=False, null=True)),
('task_periodic_alert_days', models.PositiveIntegerField(blank=True, default=0, null=True)),
('action', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='alert_template', to='scripts.script')),
('excluded_agents', models.ManyToManyField(blank=True, related_name='alert_exclusions', to='agents.Agent')),
('excluded_clients', models.ManyToManyField(blank=True, related_name='alert_exclusions', to='clients.Client')),
('excluded_sites', models.ManyToManyField(blank=True, related_name='alert_exclusions', to='clients.Site')),
('resolved_action', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='resolved_alert_template', to='scripts.script')),
],
),
]

View File

@@ -0,0 +1,31 @@
# Generated by Django 3.1.4 on 2021-02-12 17:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('alerts', '0004_auto_20210212_1408'),
]
operations = [
migrations.RemoveField(
model_name='alert',
name='action_timeout',
),
migrations.RemoveField(
model_name='alert',
name='resolved_action_timeout',
),
migrations.AddField(
model_name='alerttemplate',
name='action_timeout',
field=models.PositiveIntegerField(default=15),
),
migrations.AddField(
model_name='alerttemplate',
name='resolved_action_timeout',
field=models.PositiveIntegerField(default=15),
),
]

View File

@@ -1,5 +1,7 @@
from django.db import models from django.db import models
from django.contrib.postgres.fields import ArrayField
from django.db.models.fields import BooleanField, PositiveIntegerField
from django.utils import timezone as djangotime
SEVERITY_CHOICES = [ SEVERITY_CHOICES = [
("info", "Informational"), ("info", "Informational"),
@@ -7,6 +9,13 @@ SEVERITY_CHOICES = [
("error", "Error"), ("error", "Error"),
] ]
ALERT_TYPE_CHOICES = [
("availability", "Availability"),
("check", "Check"),
("task", "Task"),
("custom", "Custom"),
]
class Alert(models.Model): class Alert(models.Model):
agent = models.ForeignKey( agent = models.ForeignKey(
@@ -23,21 +32,253 @@ class Alert(models.Model):
null=True, null=True,
blank=True, blank=True,
) )
assigned_task = models.ForeignKey(
"autotasks.AutomatedTask",
related_name="alert",
on_delete=models.CASCADE,
null=True,
blank=True,
)
alert_type = models.CharField(
max_length=20, choices=ALERT_TYPE_CHOICES, default="availability"
)
message = models.TextField(null=True, blank=True) message = models.TextField(null=True, blank=True)
alert_time = models.DateTimeField(auto_now_add=True, null=True) alert_time = models.DateTimeField(auto_now_add=True, null=True, blank=True)
snoozed = models.BooleanField(default=False)
snooze_until = models.DateTimeField(null=True, blank=True) snooze_until = models.DateTimeField(null=True, blank=True)
resolved = models.BooleanField(default=False) resolved = models.BooleanField(default=False)
severity = models.CharField( resolved_on = models.DateTimeField(null=True, blank=True)
max_length=100, choices=SEVERITY_CHOICES, default="info" severity = models.CharField(max_length=30, choices=SEVERITY_CHOICES, default="info")
email_sent = models.DateTimeField(null=True, blank=True)
resolved_email_sent = models.DateTimeField(null=True, blank=True)
sms_sent = models.DateTimeField(null=True, blank=True)
resolved_sms_sent = models.DateTimeField(null=True, blank=True)
hidden = models.BooleanField(default=False)
action_run = models.DateTimeField(null=True, blank=True)
action_stdout = models.TextField(null=True, blank=True)
action_stderr = models.TextField(null=True, blank=True)
action_retcode = models.IntegerField(null=True, blank=True)
action_execution_time = models.CharField(max_length=100, null=True, blank=True)
resolved_action_run = models.DateTimeField(null=True, blank=True)
resolved_action_stdout = models.TextField(null=True, blank=True)
resolved_action_stderr = models.TextField(null=True, blank=True)
resolved_action_retcode = models.IntegerField(null=True, blank=True)
resolved_action_execution_time = models.CharField(
max_length=100, null=True, blank=True
) )
def __str__(self): def __str__(self):
return self.message return self.message
def resolve(self):
self.resolved = True
self.resolved_on = djangotime.now()
self.snoozed = False
self.snooze_until = None
self.save()
@classmethod @classmethod
def create_availability_alert(cls, agent): def create_availability_alert(cls, agent):
pass if not cls.objects.filter(agent=agent, resolved=False).exists():
return cls.objects.create(
agent=agent,
alert_type="availability",
severity="error",
message=f"{agent.hostname} in {agent.client.name}\\{agent.site.name} is Offline.",
hidden=True,
)
@classmethod @classmethod
def create_check_alert(cls, check): def create_check_alert(cls, check):
if not cls.objects.filter(assigned_check=check, resolved=False).exists():
return cls.objects.create(
assigned_check=check,
alert_type="check",
severity=check.alert_severity,
message=f"{check.agent.hostname} has a {check.check_type} check: {check.readable_desc} that failed.",
hidden=True,
)
@classmethod
def create_task_alert(cls, task):
if not cls.objects.filter(assigned_task=task, resolved=False).exists():
return cls.objects.create(
assigned_task=task,
alert_type="task",
severity=task.alert_severity,
message=f"{task.agent.hostname} has task: {task.name} that failed.",
hidden=True,
)
@classmethod
def create_custom_alert(cls, custom):
pass pass
class AlertTemplate(models.Model):
name = models.CharField(max_length=100)
is_active = models.BooleanField(default=True)
action = models.ForeignKey(
"scripts.Script",
related_name="alert_template",
blank=True,
null=True,
on_delete=models.SET_NULL,
)
action_args = ArrayField(
models.CharField(max_length=255, null=True, blank=True),
null=True,
blank=True,
default=list,
)
action_timeout = models.PositiveIntegerField(default=15)
resolved_action = models.ForeignKey(
"scripts.Script",
related_name="resolved_alert_template",
blank=True,
null=True,
on_delete=models.SET_NULL,
)
resolved_action_args = ArrayField(
models.CharField(max_length=255, null=True, blank=True),
null=True,
blank=True,
default=list,
)
resolved_action_timeout = models.PositiveIntegerField(default=15)
# overrides the global recipients
email_recipients = ArrayField(
models.CharField(max_length=100, blank=True),
null=True,
blank=True,
default=list,
)
text_recipients = ArrayField(
models.CharField(max_length=100, blank=True),
null=True,
blank=True,
default=list,
)
# overrides the from address
email_from = models.EmailField(blank=True, null=True)
# agent alert settings
agent_email_on_resolved = BooleanField(null=True, blank=True, default=False)
agent_text_on_resolved = BooleanField(null=True, blank=True, default=False)
agent_include_desktops = BooleanField(null=True, blank=True, default=False)
agent_always_email = BooleanField(null=True, blank=True, default=False)
agent_always_text = BooleanField(null=True, blank=True, default=False)
agent_always_alert = BooleanField(null=True, blank=True, default=False)
agent_periodic_alert_days = PositiveIntegerField(blank=True, null=True, default=0)
# check alert settings
check_email_alert_severity = ArrayField(
models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
blank=True,
default=list,
)
check_text_alert_severity = ArrayField(
models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
blank=True,
default=list,
)
check_dashboard_alert_severity = ArrayField(
models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
blank=True,
default=list,
)
check_email_on_resolved = BooleanField(null=True, blank=True, default=False)
check_text_on_resolved = BooleanField(null=True, blank=True, default=False)
check_always_email = BooleanField(null=True, blank=True, default=False)
check_always_text = BooleanField(null=True, blank=True, default=False)
check_always_alert = BooleanField(null=True, blank=True, default=False)
check_periodic_alert_days = PositiveIntegerField(blank=True, null=True, default=0)
# task alert settings
task_email_alert_severity = ArrayField(
models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
blank=True,
default=list,
)
task_text_alert_severity = ArrayField(
models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
blank=True,
default=list,
)
task_dashboard_alert_severity = ArrayField(
models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
blank=True,
default=list,
)
task_email_on_resolved = BooleanField(null=True, blank=True, default=False)
task_text_on_resolved = BooleanField(null=True, blank=True, default=False)
task_always_email = BooleanField(null=True, blank=True, default=False)
task_always_text = BooleanField(null=True, blank=True, default=False)
task_always_alert = BooleanField(null=True, blank=True, default=False)
task_periodic_alert_days = PositiveIntegerField(blank=True, null=True, default=0)
excluded_sites = models.ManyToManyField(
"clients.Site", related_name="alert_exclusions", blank=True
)
excluded_clients = models.ManyToManyField(
"clients.Client", related_name="alert_exclusions", blank=True
)
excluded_agents = models.ManyToManyField(
"agents.Agent", related_name="alert_exclusions", blank=True
)
def __str__(self):
return self.name
@property
def has_agent_settings(self) -> bool:
return (
self.agent_email_on_resolved
or self.agent_text_on_resolved
or self.agent_include_desktops
or self.agent_always_email
or self.agent_always_text
or self.agent_always_alert
or bool(self.agent_periodic_alert_days)
)
@property
def has_check_settings(self) -> bool:
return (
bool(self.check_email_alert_severity)
or bool(self.check_text_alert_severity)
or bool(self.check_dashboard_alert_severity)
or self.check_email_on_resolved
or self.check_text_on_resolved
or self.check_always_email
or self.check_always_text
or self.check_always_alert
or bool(self.check_periodic_alert_days)
)
@property
def has_task_settings(self) -> bool:
return (
bool(self.task_email_alert_severity)
or bool(self.task_text_alert_severity)
or bool(self.task_dashboard_alert_severity)
or self.task_email_on_resolved
or self.task_text_on_resolved
or self.task_always_email
or self.task_always_text
or self.task_always_alert
or bool(self.task_periodic_alert_days)
)
@property
def has_core_settings(self) -> bool:
return bool(self.email_from) or self.email_recipients or self.text_recipients
@property
def is_default_template(self) -> bool:
return self.default_alert_template.exists()

View File

@@ -1,19 +1,124 @@
from rest_framework.fields import SerializerMethodField
from rest_framework.serializers import ( from rest_framework.serializers import (
ModelSerializer, ModelSerializer,
ReadOnlyField, ReadOnlyField,
DateTimeField,
) )
from .models import Alert from clients.serializers import ClientSerializer, SiteSerializer
from automation.serializers import PolicySerializer
from tacticalrmm.utils import get_default_timezone
from .models import Alert, AlertTemplate
class AlertSerializer(ModelSerializer): class AlertSerializer(ModelSerializer):
hostname = ReadOnlyField(source="agent.hostname") hostname = SerializerMethodField(read_only=True)
client = ReadOnlyField(source="agent.client") client = SerializerMethodField(read_only=True)
site = ReadOnlyField(source="agent.site") site = SerializerMethodField(read_only=True)
alert_time = DateTimeField(format="iso-8601") alert_time = SerializerMethodField(read_only=True)
resolve_on = SerializerMethodField(read_only=True)
snoozed_until = SerializerMethodField(read_only=True)
def get_hostname(self, instance):
if instance.alert_type == "availability":
return instance.agent.hostname if instance.agent else ""
elif instance.alert_type == "check":
return (
instance.assigned_check.agent.hostname
if instance.assigned_check
else ""
)
elif instance.alert_type == "task":
return (
instance.assigned_task.agent.hostname if instance.assigned_task else ""
)
else:
return ""
def get_client(self, instance):
if instance.alert_type == "availability":
return instance.agent.client.name if instance.agent else ""
elif instance.alert_type == "check":
return (
instance.assigned_check.agent.client.name
if instance.assigned_check
else ""
)
elif instance.alert_type == "task":
return (
instance.assigned_task.agent.client.name
if instance.assigned_task
else ""
)
else:
return ""
def get_site(self, instance):
if instance.alert_type == "availability":
return instance.agent.site.name if instance.agent else ""
elif instance.alert_type == "check":
return (
instance.assigned_check.agent.site.name
if instance.assigned_check
else ""
)
elif instance.alert_type == "task":
return (
instance.assigned_task.agent.site.name if instance.assigned_task else ""
)
else:
return ""
def get_alert_time(self, instance):
if instance.alert_time:
return instance.alert_time.astimezone(get_default_timezone()).timestamp()
else:
return None
def get_resolve_on(self, instance):
if instance.resolved_on:
return instance.resolved_on.astimezone(get_default_timezone()).timestamp()
else:
return None
def get_snoozed_until(self, instance):
if instance.snooze_until:
return instance.snooze_until.astimezone(get_default_timezone()).timestamp()
return None
class Meta: class Meta:
model = Alert model = Alert
fields = "__all__" fields = "__all__"
class AlertTemplateSerializer(ModelSerializer):
agent_settings = ReadOnlyField(source="has_agent_settings")
check_settings = ReadOnlyField(source="has_check_settings")
task_settings = ReadOnlyField(source="has_task_settings")
core_settings = ReadOnlyField(source="has_core_settings")
default_template = ReadOnlyField(source="is_default_template")
action_name = ReadOnlyField(source="action.name")
resolved_action_name = ReadOnlyField(source="resolved_action.name")
applied_count = SerializerMethodField()
class Meta:
model = AlertTemplate
fields = "__all__"
def get_applied_count(self, instance):
count = 0
count += instance.policies.count()
count += instance.clients.count()
count += instance.sites.count()
return count
class AlertTemplateRelationSerializer(ModelSerializer):
policies = PolicySerializer(read_only=True, many=True)
clients = ClientSerializer(read_only=True, many=True)
sites = SiteSerializer(read_only=True, many=True)
class Meta:
model = AlertTemplate
fields = "__all__"

View File

@@ -0,0 +1,15 @@
from django.utils import timezone as djangotime
from tacticalrmm.celery import app
from alerts.models import Alert
@app.task
def unsnooze_alerts() -> str:
Alert.objects.filter(snoozed=True, snooze_until__lte=djangotime.now()).update(
snoozed=False, snooze_until=None
)
return "ok"

View File

@@ -1,3 +1,377 @@
from django.test import TestCase from datetime import datetime, timedelta
from core.models import CoreSettings
# Create your tests here. from django.utils import timezone as djangotime
from tacticalrmm.test import TacticalTestCase
from model_bakery import baker, seq
from .models import Alert, AlertTemplate
from .serializers import (
AlertSerializer,
AlertTemplateSerializer,
AlertTemplateRelationSerializer,
)
class TestAlertsViews(TacticalTestCase):
def setUp(self):
self.authenticate()
self.setup_coresettings()
def test_get_alerts(self):
url = "/alerts/alerts/"
# create check, task, and agent to test each serializer function
check = baker.make_recipe("checks.diskspace_check")
task = baker.make("autotasks.AutomatedTask")
agent = baker.make_recipe("agents.agent")
# setup data
alerts = baker.make(
"alerts.Alert",
agent=agent,
alert_time=seq(datetime.now(), timedelta(days=15)),
severity="warning",
_quantity=3,
)
baker.make(
"alerts.Alert",
assigned_check=check,
alert_time=seq(datetime.now(), timedelta(days=15)),
severity="error",
_quantity=7,
)
baker.make(
"alerts.Alert",
assigned_task=task,
snoozed=True,
snooze_until=djangotime.now(),
alert_time=seq(datetime.now(), timedelta(days=15)),
_quantity=2,
)
baker.make(
"alerts.Alert",
agent=agent,
resolved=True,
resolved_on=djangotime.now(),
alert_time=seq(datetime.now(), timedelta(days=15)),
_quantity=9,
)
# test top alerts for alerts icon
data = {"top": 3}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
self.assertEquals(resp.data["alerts"], AlertSerializer(alerts, many=True).data)
self.assertEquals(resp.data["alerts_count"], 10)
# test filter data
# test data and result counts
data = [
{
"filter": {
"timeFilter": 30,
"snoozedFilter": True,
"resolvedFilter": False,
},
"count": 12,
},
{
"filter": {
"timeFilter": 45,
"snoozedFilter": False,
"resolvedFilter": False,
},
"count": 10,
},
{
"filter": {
"severityFilter": ["error"],
"snoozedFilter": False,
"resolvedFilter": True,
"timeFilter": 20,
},
"count": 7,
},
{
"filter": {
"clientFilter": [],
"snoozedFilter": True,
"resolvedFilter": False,
},
"count": 0,
},
{"filter": {}, "count": 21},
{"filter": {"snoozedFilter": True, "resolvedFilter": False}, "count": 12},
]
for req in data:
resp = self.client.patch(url, req["filter"], format="json")
self.assertEqual(resp.status_code, 200)
self.assertEqual(len(resp.data), req["count"])
self.check_not_authenticated("patch", url)
def test_add_alert(self):
url = "/alerts/alerts/"
agent = baker.make_recipe("agents.agent")
data = {
"alert_time": datetime.now(),
"agent": agent.id,
"severity": "warning",
"alert_type": "availability",
}
resp = self.client.post(url, data, format="json")
self.assertEqual(resp.status_code, 200)
self.check_not_authenticated("post", url)
def test_get_alert(self):
# returns 404 for invalid alert pk
resp = self.client.get("/alerts/alerts/500/", format="json")
self.assertEqual(resp.status_code, 404)
alert = baker.make("alerts.Alert")
url = f"/alerts/alerts/{alert.pk}/"
resp = self.client.get(url, format="json")
serializer = AlertSerializer(alert)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data)
self.check_not_authenticated("get", url)
def test_update_alert(self):
# returns 404 for invalid alert pk
resp = self.client.put("/alerts/alerts/500/", format="json")
self.assertEqual(resp.status_code, 404)
alert = baker.make("alerts.Alert", resolved=False, snoozed=False)
url = f"/alerts/alerts/{alert.pk}/"
# test resolving alert
data = {
"id": alert.pk,
"type": "resolve",
}
resp = self.client.put(url, data, format="json")
self.assertEqual(resp.status_code, 200)
self.assertTrue(Alert.objects.get(pk=alert.pk).resolved)
self.assertTrue(Alert.objects.get(pk=alert.pk).resolved_on)
# test snoozing alert
data = {"id": alert.pk, "type": "snooze", "snooze_days": "30"}
resp = self.client.put(url, data, format="json")
self.assertEqual(resp.status_code, 200)
self.assertTrue(Alert.objects.get(pk=alert.pk).snoozed)
self.assertTrue(Alert.objects.get(pk=alert.pk).snooze_until)
# test snoozing alert without snooze_days
data = {"id": alert.pk, "type": "snooze"}
resp = self.client.put(url, data, format="json")
self.assertEqual(resp.status_code, 400)
# test unsnoozing alert
data = {"id": alert.pk, "type": "unsnooze"}
resp = self.client.put(url, data, format="json")
self.assertEqual(resp.status_code, 200)
self.assertFalse(Alert.objects.get(pk=alert.pk).snoozed)
self.assertFalse(Alert.objects.get(pk=alert.pk).snooze_until)
# test invalid type
data = {"id": alert.pk, "type": "invalid"}
resp = self.client.put(url, data, format="json")
self.assertEqual(resp.status_code, 400)
self.check_not_authenticated("put", url)
def test_delete_alert(self):
# returns 404 for invalid alert pk
resp = self.client.put("/alerts/alerts/500/", format="json")
self.assertEqual(resp.status_code, 404)
alert = baker.make("alerts.Alert")
# test delete alert
url = f"/alerts/alerts/{alert.pk}/"
resp = self.client.delete(url, format="json")
self.assertEqual(resp.status_code, 200)
self.assertFalse(Alert.objects.filter(pk=alert.pk).exists())
self.check_not_authenticated("delete", url)
def test_bulk_alert_actions(self):
url = "/alerts/bulk/"
# setup data
alerts = baker.make("alerts.Alert", resolved=False, _quantity=3)
# test invalid data
data = {"bulk_action": "invalid"}
resp = self.client.post(url, data, format="json")
self.assertEqual(resp.status_code, 400)
# test snooze without snooze days
data = {"bulk_action": "snooze"}
resp = self.client.post(url, data, format="json")
self.assertEqual(resp.status_code, 400)
# test bulk snoozing alerts
data = {
"bulk_action": "snooze",
"alerts": [alert.pk for alert in alerts],
"snooze_days": "30",
}
resp = self.client.post(url, data, format="json")
self.assertEqual(resp.status_code, 200)
self.assertFalse(Alert.objects.filter(snoozed=False).exists())
# test bulk resolving alerts
data = {"bulk_action": "resolve", "alerts": [alert.pk for alert in alerts]}
resp = self.client.post(url, data, format="json")
self.assertEqual(resp.status_code, 200)
self.assertFalse(Alert.objects.filter(resolved=False).exists())
self.assertTrue(Alert.objects.filter(snoozed=False).exists())
def test_get_alert_templates(self):
url = "/alerts/alerttemplates/"
alert_templates = baker.make("alerts.AlertTemplate", _quantity=3)
resp = self.client.get(url, format="json")
serializer = AlertTemplateSerializer(alert_templates, many=True)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data)
self.check_not_authenticated("get", url)
def test_add_alert_template(self):
url = "/alerts/alerttemplates/"
data = {
"name": "Test Template",
}
resp = self.client.post(url, data, format="json")
self.assertEqual(resp.status_code, 200)
self.check_not_authenticated("post", url)
def test_get_alert_template(self):
# returns 404 for invalid alert template pk
resp = self.client.get("/alerts/alerttemplates/500/", format="json")
self.assertEqual(resp.status_code, 404)
alert_template = baker.make("alerts.AlertTemplate")
url = f"/alerts/alerttemplates/{alert_template.pk}/"
resp = self.client.get(url, format="json")
serializer = AlertTemplateSerializer(alert_template)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data)
self.check_not_authenticated("get", url)
def test_update_alert_template(self):
# returns 404 for invalid alert pk
resp = self.client.put("/alerts/alerttemplates/500/", format="json")
self.assertEqual(resp.status_code, 404)
alert_template = baker.make("alerts.AlertTemplate")
url = f"/alerts/alerttemplates/{alert_template.pk}/"
# test data
data = {
"id": alert_template.pk,
"agent_email_on_resolved": True,
"agent_text_on_resolved": True,
"agent_include_desktops": True,
"agent_always_email": True,
"agent_always_text": True,
"agent_always_alert": True,
"agent_periodic_alert_days": "90",
}
resp = self.client.put(url, data, format="json")
self.assertEqual(resp.status_code, 200)
self.check_not_authenticated("put", url)
def test_delete_alert_template(self):
# returns 404 for invalid alert pk
resp = self.client.put("/alerts/alerttemplates/500/", format="json")
self.assertEqual(resp.status_code, 404)
alert_template = baker.make("alerts.AlertTemplate")
# test delete alert
url = f"/alerts/alerttemplates/{alert_template.pk}/"
resp = self.client.delete(url, format="json")
self.assertEqual(resp.status_code, 200)
self.assertFalse(AlertTemplate.objects.filter(pk=alert_template.pk).exists())
self.check_not_authenticated("delete", url)
def test_alert_template_related(self):
# setup data
alert_template = baker.make("alerts.AlertTemplate")
baker.make("clients.Client", alert_template=alert_template, _quantity=2)
baker.make("clients.Site", alert_template=alert_template, _quantity=3)
baker.make("automation.Policy", alert_template=alert_template)
core = CoreSettings.objects.first()
core.alert_template = alert_template
core.save()
url = f"/alerts/alerttemplates/{alert_template.pk}/related/"
resp = self.client.get(url, format="json")
serializer = AlertTemplateRelationSerializer(alert_template)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data)
self.assertEqual(len(resp.data["policies"]), 1)
self.assertEqual(len(resp.data["clients"]), 2)
self.assertEqual(len(resp.data["sites"]), 3)
self.assertTrue(
AlertTemplate.objects.get(pk=alert_template.pk).is_default_template
)
class TestAlertTasks(TacticalTestCase):
def test_unsnooze_alert_task(self):
from alerts.tasks import unsnooze_alerts
# these will be unsnoozed whent eh function is run
not_snoozed = baker.make(
"alerts.Alert",
snoozed=True,
snooze_until=seq(datetime.now(), timedelta(days=15)),
_quantity=5,
)
# these will still be snoozed after the function is run
snoozed = baker.make(
"alerts.Alert",
snoozed=True,
snooze_until=seq(datetime.now(), timedelta(days=-15)),
_quantity=5,
)
unsnooze_alerts()
self.assertFalse(
Alert.objects.filter(
pk__in=[alert.pk for alert in not_snoozed], snoozed=False
).exists()
)
self.assertTrue(
Alert.objects.filter(
pk__in=[alert.pk for alert in snoozed], snoozed=False
).exists()
)

View File

@@ -3,5 +3,9 @@ from . import views
urlpatterns = [ urlpatterns = [
path("alerts/", views.GetAddAlerts.as_view()), path("alerts/", views.GetAddAlerts.as_view()),
path("bulk/", views.BulkAlerts.as_view()),
path("alerts/<int:pk>/", views.GetUpdateDeleteAlert.as_view()), path("alerts/<int:pk>/", views.GetUpdateDeleteAlert.as_view()),
path("alerttemplates/", views.GetAddAlertTemplates.as_view()),
path("alerttemplates/<int:pk>/", views.GetUpdateDeleteAlertTemplate.as_view()),
path("alerttemplates/<int:pk>/related/", views.RelatedAlertTemplate.as_view()),
] ]

View File

@@ -1,18 +1,102 @@
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.db.models import Q
from datetime import datetime as dt
from django.utils import timezone as djangotime
from tacticalrmm.utils import notify_error
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import status
from .models import Alert from .models import Alert, AlertTemplate
from .serializers import AlertSerializer from .serializers import (
AlertSerializer,
AlertTemplateSerializer,
AlertTemplateRelationSerializer,
)
class GetAddAlerts(APIView): class GetAddAlerts(APIView):
def get(self, request): def patch(self, request):
alerts = Alert.objects.all()
# top 10 alerts for dashboard icon
if "top" in request.data.keys():
alerts = Alert.objects.filter(
resolved=False, snoozed=False, hidden=False
).order_by("alert_time")[: int(request.data["top"])]
count = Alert.objects.filter(
resolved=False, snoozed=False, hidden=False
).count()
return Response(
{
"alerts_count": count,
"alerts": AlertSerializer(alerts, many=True).data,
}
)
elif any(
key
in [
"timeFilter",
"clientFilter",
"severityFilter",
"resolvedFilter",
"snoozedFilter",
]
for key in request.data.keys()
):
clientFilter = Q()
severityFilter = Q()
timeFilter = Q()
resolvedFilter = Q()
snoozedFilter = Q()
if (
"snoozedFilter" in request.data.keys()
and not request.data["snoozedFilter"]
):
snoozedFilter = Q(snoozed=request.data["snoozedFilter"])
if (
"resolvedFilter" in request.data.keys()
and not request.data["resolvedFilter"]
):
resolvedFilter = Q(resolved=request.data["resolvedFilter"])
if "clientFilter" in request.data.keys():
from agents.models import Agent
from clients.models import Client
clients = Client.objects.filter(
pk__in=request.data["clientFilter"]
).values_list("id")
agents = Agent.objects.filter(site__client_id__in=clients).values_list(
"id"
)
clientFilter = Q(agent__in=agents)
if "severityFilter" in request.data.keys():
severityFilter = Q(severity__in=request.data["severityFilter"])
if "timeFilter" in request.data.keys():
timeFilter = Q(
alert_time__lte=djangotime.make_aware(dt.today()),
alert_time__gt=djangotime.make_aware(dt.today())
- djangotime.timedelta(days=int(request.data["timeFilter"])),
)
alerts = (
Alert.objects.filter(clientFilter)
.filter(severityFilter)
.filter(resolvedFilter)
.filter(snoozedFilter)
.filter(timeFilter)
)
return Response(AlertSerializer(alerts, many=True).data)
else:
alerts = Alert.objects.all()
return Response(AlertSerializer(alerts, many=True).data) return Response(AlertSerializer(alerts, many=True).data)
def post(self, request): def post(self, request):
@@ -32,7 +116,40 @@ class GetUpdateDeleteAlert(APIView):
def put(self, request, pk): def put(self, request, pk):
alert = get_object_or_404(Alert, pk=pk) alert = get_object_or_404(Alert, pk=pk)
serializer = AlertSerializer(instance=alert, data=request.data, partial=True) data = request.data
if "type" in data.keys():
if data["type"] == "resolve":
data = {
"resolved": True,
"resolved_on": djangotime.now(),
"snoozed": False,
}
# unable to set snooze_until to none in serialzier
alert.snooze_until = None
alert.save()
elif data["type"] == "snooze":
if "snooze_days" in data.keys():
data = {
"snoozed": True,
"snooze_until": djangotime.now()
+ djangotime.timedelta(days=int(data["snooze_days"])),
}
else:
return notify_error(
"Missing 'snoozed_days' when trying to snooze alert"
)
elif data["type"] == "unsnooze":
data = {"snoozed": False}
# unable to set snooze_until to none in serialzier
alert.snooze_until = None
alert.save()
else:
return notify_error("There was an error in the request data")
serializer = AlertSerializer(instance=alert, data=data, partial=True)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
serializer.save() serializer.save()
@@ -42,3 +159,68 @@ class GetUpdateDeleteAlert(APIView):
Alert.objects.get(pk=pk).delete() Alert.objects.get(pk=pk).delete()
return Response("ok") return Response("ok")
class BulkAlerts(APIView):
def post(self, request):
if request.data["bulk_action"] == "resolve":
Alert.objects.filter(id__in=request.data["alerts"]).update(
resolved=True,
resolved_on=djangotime.now(),
snoozed=False,
snooze_until=None,
)
return Response("ok")
elif request.data["bulk_action"] == "snooze":
if "snooze_days" in request.data.keys():
Alert.objects.filter(id__in=request.data["alerts"]).update(
snoozed=True,
snooze_until=djangotime.now()
+ djangotime.timedelta(days=int(request.data["snooze_days"])),
)
return Response("ok")
return notify_error("The request was invalid")
class GetAddAlertTemplates(APIView):
def get(self, request):
alert_templates = AlertTemplate.objects.all()
return Response(AlertTemplateSerializer(alert_templates, many=True).data)
def post(self, request):
serializer = AlertTemplateSerializer(data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("ok")
class GetUpdateDeleteAlertTemplate(APIView):
def get(self, request, pk):
alert_template = get_object_or_404(AlertTemplate, pk=pk)
return Response(AlertTemplateSerializer(alert_template).data)
def put(self, request, pk):
alert_template = get_object_or_404(AlertTemplate, pk=pk)
serializer = AlertTemplateSerializer(
instance=alert_template, 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(AlertTemplate, pk=pk).delete()
return Response("ok")
class RelatedAlertTemplate(APIView):
def get(self, request, pk):
alert_template = get_object_or_404(AlertTemplate, pk=pk)
return Response(AlertTemplateRelationSerializer(alert_template).data)

View File

@@ -1,5 +0,0 @@
from django.apps import AppConfig
class Apiv2Config(AppConfig):
name = "apiv2"

View File

@@ -1,38 +0,0 @@
from tacticalrmm.test import TacticalTestCase
from unittest.mock import patch
from model_bakery import baker
from itertools import cycle
class TestAPIv2(TacticalTestCase):
def setUp(self):
self.authenticate()
self.setup_coresettings()
@patch("agents.models.Agent.salt_api_cmd")
def test_sync_modules(self, mock_ret):
# setup data
agent = baker.make_recipe("agents.agent")
url = "/api/v2/saltminion/"
payload = {"agent_id": agent.agent_id}
mock_ret.return_value = "error"
r = self.client.patch(url, payload, format="json")
self.assertEqual(r.status_code, 400)
mock_ret.return_value = []
r = self.client.patch(url, payload, format="json")
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data, "Modules are already in sync")
mock_ret.return_value = ["modules.win_agent"]
r = self.client.patch(url, payload, format="json")
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data, "Successfully synced salt modules")
mock_ret.return_value = ["askdjaskdjasd", "modules.win_agent"]
r = self.client.patch(url, payload, format="json")
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data, "Successfully synced salt modules")
self.check_not_authenticated("patch", url)

View File

@@ -1,14 +0,0 @@
from django.urls import path
from . import views
from apiv3 import views as v3_views
urlpatterns = [
path("newagent/", v3_views.NewAgent.as_view()),
path("meshexe/", v3_views.MeshExe.as_view()),
path("saltminion/", v3_views.SaltMinion.as_view()),
path("<str:agentid>/saltminion/", v3_views.SaltMinion.as_view()),
path("sysinfo/", v3_views.SysInfo.as_view()),
path("hello/", v3_views.Hello.as_view()),
path("checkrunner/", views.CheckRunner.as_view()),
path("<str:agentid>/checkrunner/", views.CheckRunner.as_view()),
]

View File

@@ -1,41 +0,0 @@
from django.shortcuts import get_object_or_404
from django.utils import timezone as djangotime
from rest_framework.authentication import TokenAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from agents.models import Agent
from checks.models import Check
from checks.serializers import CheckRunnerGetSerializerV2
class CheckRunner(APIView):
"""
For the windows python agent
"""
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def get(self, request, agentid):
agent = get_object_or_404(Agent, agent_id=agentid)
agent.last_seen = djangotime.now()
agent.save(update_fields=["last_seen"])
checks = Check.objects.filter(agent__pk=agent.pk, overriden_by_policy=False)
ret = {
"agent": agent.pk,
"check_interval": agent.check_interval,
"checks": CheckRunnerGetSerializerV2(checks, many=True).data,
}
return Response(ret)
def patch(self, request):
check = get_object_or_404(Check, pk=request.data["id"])
check.last_run = djangotime.now()
check.save(update_fields=["last_run"])
status = check.handle_checkv2(request.data)
return Response(status)

View File

@@ -26,38 +26,6 @@ class TestAPIv3(TacticalTestCase):
self.check_not_authenticated("get", url) self.check_not_authenticated("get", url)
def test_get_salt_minion(self):
url = f"/api/v3/{self.agent.agent_id}/saltminion/"
url2 = f"/api/v2/{self.agent.agent_id}/saltminion/"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertIn("latestVer", r.json().keys())
self.assertIn("currentVer", r.json().keys())
self.assertIn("salt_id", r.json().keys())
self.assertIn("downloadURL", r.json().keys())
r2 = self.client.get(url2)
self.assertEqual(r2.status_code, 200)
self.check_not_authenticated("get", url)
self.check_not_authenticated("get", url2)
def test_get_mesh_info(self):
url = f"/api/v3/{self.agent.pk}/meshinfo/"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.check_not_authenticated("get", url)
def test_get_winupdater(self):
url = f"/api/v3/{self.agent.agent_id}/winupdater/"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.check_not_authenticated("get", url)
def test_sysinfo(self): def test_sysinfo(self):
# TODO replace this with golang wmi sample data # TODO replace this with golang wmi sample data
@@ -76,28 +44,11 @@ class TestAPIv3(TacticalTestCase):
self.check_not_authenticated("patch", url) self.check_not_authenticated("patch", url)
def test_hello_patch(self): def test_checkrunner_interval(self):
url = "/api/v3/hello/" url = f"/api/v3/{self.agent.agent_id}/checkinterval/"
payload = {
"agent_id": self.agent.agent_id,
"logged_in_username": "None",
"disks": [],
}
r = self.client.patch(url, payload, format="json")
self.assertEqual(r.status_code, 200)
payload["logged_in_username"] = "Bob"
r = self.client.patch(url, payload, format="json")
self.assertEqual(r.status_code, 200)
self.check_not_authenticated("patch", url)
@patch("agents.tasks.install_salt_task.delay")
def test_install_salt(self, mock_task):
url = f"/api/v3/{self.agent.agent_id}/installsalt/"
r = self.client.get(url, format="json") r = self.client.get(url, format="json")
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
mock_task.assert_called_with(self.agent.pk) self.assertEqual(
r.json(),
self.check_not_authenticated("get", url) {"agent": self.agent.pk, "check_interval": self.agent.check_interval},
)

View File

@@ -2,20 +2,18 @@ from django.urls import path
from . import views from . import views
urlpatterns = [ urlpatterns = [
path("checkin/", views.CheckIn.as_view()),
path("hello/", views.Hello.as_view()),
path("checkrunner/", views.CheckRunner.as_view()), path("checkrunner/", views.CheckRunner.as_view()),
path("<str:agentid>/checkrunner/", views.CheckRunner.as_view()), path("<str:agentid>/checkrunner/", views.CheckRunner.as_view()),
path("<str:agentid>/checkinterval/", views.CheckRunnerInterval.as_view()),
path("<int:pk>/<str:agentid>/taskrunner/", views.TaskRunner.as_view()), path("<int:pk>/<str:agentid>/taskrunner/", views.TaskRunner.as_view()),
path("saltminion/", views.SaltMinion.as_view()),
path("<str:agentid>/saltminion/", views.SaltMinion.as_view()),
path("<int:pk>/meshinfo/", views.MeshInfo.as_view()),
path("meshexe/", views.MeshExe.as_view()), path("meshexe/", views.MeshExe.as_view()),
path("sysinfo/", views.SysInfo.as_view()), path("sysinfo/", views.SysInfo.as_view()),
path("newagent/", views.NewAgent.as_view()), path("newagent/", views.NewAgent.as_view()),
path("winupdater/", views.WinUpdater.as_view()),
path("<str:agentid>/winupdater/", views.WinUpdater.as_view()),
path("software/", views.Software.as_view()), path("software/", views.Software.as_view()),
path("installer/", views.Installer.as_view()), path("installer/", views.Installer.as_view()),
path("<str:agentid>/installsalt/", views.InstallSalt.as_view()), path("checkin/", views.CheckIn.as_view()),
path("syncmesh/", views.SyncMeshNodeID.as_view()),
path("choco/", views.Choco.as_view()),
path("winupdates/", views.WinUpdates.as_view()),
path("superseded/", views.SupersededWinUpdate.as_view()),
] ]

View File

@@ -1,6 +1,6 @@
import asyncio import asyncio
import os import os
import requests import time
from loguru import logger from loguru import logger
from packaging import version as pyver from packaging import version as pyver
@@ -17,72 +17,58 @@ from rest_framework.authtoken.models import Token
from agents.models import Agent from agents.models import Agent
from checks.models import Check from checks.models import Check
from checks.utils import bytes2human
from autotasks.models import AutomatedTask from autotasks.models import AutomatedTask
from accounts.models import User from accounts.models import User
from winupdate.models import WinUpdatePolicy from winupdate.models import WinUpdate, WinUpdatePolicy
from software.models import InstalledSoftware from software.models import InstalledSoftware
from checks.serializers import CheckRunnerGetSerializerV3 from checks.serializers import CheckRunnerGetSerializer
from agents.serializers import WinAgentSerializer
from autotasks.serializers import TaskGOGetSerializer, TaskRunnerPatchSerializer from autotasks.serializers import TaskGOGetSerializer, TaskRunnerPatchSerializer
from winupdate.serializers import ApprovedUpdateSerializer from agents.serializers import WinAgentSerializer
from agents.tasks import (
agent_recovery_email_task,
agent_recovery_sms_task,
sync_salt_modules_task,
install_salt_task,
)
from winupdate.tasks import check_for_updates_task
from software.tasks import install_chocolatey
from checks.utils import bytes2human
from tacticalrmm.utils import notify_error, reload_nats, filter_software, SoftwareList from tacticalrmm.utils import notify_error, reload_nats, filter_software, SoftwareList
logger.configure(**settings.LOG_CONFIG) logger.configure(**settings.LOG_CONFIG)
class CheckIn(APIView): class CheckIn(APIView):
"""
The agent's checkin endpoint
patch: called every 45 to 110 seconds, handles agent updates and recovery
put: called every 5 to 10 minutes, handles basic system info
post: called once on windows service startup
"""
authentication_classes = [TokenAuthentication] authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
def patch(self, request): def patch(self, request):
updated = False
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
if pyver.parse(request.data["version"]) > pyver.parse(
agent.version
) or pyver.parse(request.data["version"]) == pyver.parse(
settings.LATEST_AGENT_VER
):
updated = True
agent.version = request.data["version"] agent.version = request.data["version"]
agent.last_seen = djangotime.now() agent.last_seen = djangotime.now()
agent.save(update_fields=["version", "last_seen"]) agent.save(update_fields=["version", "last_seen"])
if agent.agentoutages.exists() and agent.agentoutages.last().is_active: # change agent update pending status to completed if agent has just updated
last_outage = agent.agentoutages.last() if (
last_outage.recovery_time = djangotime.now() updated
last_outage.save(update_fields=["recovery_time"]) and agent.pendingactions.filter(
action_type="agentupdate", status="pending"
).exists()
):
agent.pendingactions.filter(
action_type="agentupdate", status="pending"
).update(status="completed")
if agent.overdue_email_alert: # handles any alerting actions
agent_recovery_email_task.delay(pk=last_outage.pk) agent.handle_alert(checkin=True)
if agent.overdue_text_alert:
agent_recovery_sms_task.delay(pk=last_outage.pk)
recovery = agent.recoveryactions.filter(last_run=None).last() recovery = agent.recoveryactions.filter(last_run=None).last()
if recovery is not None: if recovery is not None:
recovery.last_run = djangotime.now() recovery.last_run = djangotime.now()
recovery.save(update_fields=["last_run"]) recovery.save(update_fields=["last_run"])
return Response(recovery.send()) handle_agent_recovery_task.delay(pk=recovery.pk)
return Response("ok")
# handle agent update
if agent.pendingactions.filter(
action_type="agentupdate", status="pending"
).exists():
update = agent.pendingactions.filter(
action_type="agentupdate", status="pending"
).last()
update.status = "completed"
update.save(update_fields=["status"])
return Response(update.details)
# get any pending actions # get any pending actions
if agent.pendingactions.filter(status="pending").exists(): if agent.pendingactions.filter(status="pending").exists():
@@ -93,20 +79,13 @@ class CheckIn(APIView):
def put(self, request): def put(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
serializer = WinAgentSerializer(instance=agent, data=request.data, partial=True) serializer = WinAgentSerializer(instance=agent, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
if "disks" in request.data.keys(): if request.data["func"] == "disks":
disks = request.data["disks"] disks = request.data["disks"]
new = [] new = []
# python agent
if isinstance(disks, dict):
for k, v in disks.items():
new.append(v)
else:
# golang agent
for disk in disks: for disk in disks:
tmp = {} tmp = {}
for k, v in disk.items(): for _, _ in disk.items():
tmp["device"] = disk["device"] tmp["device"] = disk["device"]
tmp["fstype"] = disk["fstype"] tmp["fstype"] = disk["fstype"]
tmp["total"] = bytes2human(disk["total"]) tmp["total"] = bytes2human(disk["total"])
@@ -115,126 +94,173 @@ class CheckIn(APIView):
tmp["percent"] = int(disk["percent"]) tmp["percent"] = int(disk["percent"])
new.append(tmp) new.append(tmp)
serializer.is_valid(raise_exception=True)
serializer.save(disks=new) serializer.save(disks=new)
return Response("ok") return Response("ok")
if "logged_in_username" in request.data.keys(): if request.data["func"] == "loggedonuser":
if request.data["logged_in_username"] != "None": if request.data["logged_in_username"] != "None":
serializer.is_valid(raise_exception=True)
serializer.save(last_logged_in_user=request.data["logged_in_username"]) serializer.save(last_logged_in_user=request.data["logged_in_username"])
return Response("ok") return Response("ok")
if request.data["func"] == "software":
raw: SoftwareList = request.data["software"]
if not isinstance(raw, list):
return notify_error("err")
sw = filter_software(raw)
if not InstalledSoftware.objects.filter(agent=agent).exists():
InstalledSoftware(agent=agent, software=sw).save()
else:
s = agent.installedsoftware_set.first()
s.software = sw
s.save(update_fields=["software"])
return Response("ok")
serializer.is_valid(raise_exception=True)
serializer.save() serializer.save()
return Response("ok") return Response("ok")
# called once during tacticalagent windows service startup
def post(self, request): def post(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
serializer = WinAgentSerializer(instance=agent, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save(last_seen=djangotime.now())
sync_salt_modules_task.delay(agent.pk)
check_for_updates_task.apply_async(
queue="wupdate", kwargs={"pk": agent.pk, "wait": True}
)
if not agent.choco_installed: if not agent.choco_installed:
install_chocolatey.delay(agent.pk, wait=True) asyncio.run(agent.nats_cmd({"func": "installchoco"}, wait=False))
time.sleep(0.5)
asyncio.run(agent.nats_cmd({"func": "getwinupdates"}, wait=False))
return Response("ok") return Response("ok")
class Hello(APIView): class SyncMeshNodeID(APIView):
#### DEPRECATED, for agents <= 1.1.9 ####
"""
The agent's checkin endpoint
patch: called every 30 to 120 seconds
post: called on agent windows service startup
"""
authentication_classes = [TokenAuthentication] authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
def post(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
if agent.mesh_node_id != request.data["nodeid"]:
agent.mesh_node_id = request.data["nodeid"]
agent.save(update_fields=["mesh_node_id"])
return Response("ok")
class Choco(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
agent.choco_installed = request.data["installed"]
agent.save(update_fields=["choco_installed"])
return Response("ok")
class WinUpdates(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def put(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
reboot_policy: str = agent.get_patch_policy().reboot_after_install
reboot = False
if reboot_policy == "always":
reboot = True
if request.data["needs_reboot"]:
if reboot_policy == "required":
reboot = True
elif reboot_policy == "never":
agent.needs_reboot = True
agent.save(update_fields=["needs_reboot"])
if reboot:
asyncio.run(agent.nats_cmd({"func": "rebootnow"}, wait=False))
logger.info(f"{agent.hostname} is rebooting after updates were installed.")
agent.delete_superseded_updates()
return Response("ok")
def patch(self, request): def patch(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
serializer = WinAgentSerializer(instance=agent, data=request.data, partial=True) u = agent.winupdates.filter(guid=request.data["guid"]).last()
serializer.is_valid(raise_exception=True) success: bool = request.data["success"]
if success:
disks = request.data["disks"] u.result = "success"
new = [] u.downloaded = True
# python agent u.installed = True
if isinstance(disks, dict): u.date_installed = djangotime.now()
for k, v in disks.items(): u.save(
new.append(v) update_fields=[
else: "result",
# golang agent "downloaded",
for disk in disks: "installed",
tmp = {} "date_installed",
for k, v in disk.items(): ]
tmp["device"] = disk["device"]
tmp["fstype"] = disk["fstype"]
tmp["total"] = bytes2human(disk["total"])
tmp["used"] = bytes2human(disk["used"])
tmp["free"] = bytes2human(disk["free"])
tmp["percent"] = int(disk["percent"])
new.append(tmp)
if request.data["logged_in_username"] == "None":
serializer.save(last_seen=djangotime.now(), disks=new)
else:
serializer.save(
last_seen=djangotime.now(),
disks=new,
last_logged_in_user=request.data["logged_in_username"],
) )
else:
u.result = "failed"
u.save(update_fields=["result"])
if agent.agentoutages.exists() and agent.agentoutages.last().is_active: agent.delete_superseded_updates()
last_outage = agent.agentoutages.last()
last_outage.recovery_time = djangotime.now()
last_outage.save(update_fields=["recovery_time"])
if agent.overdue_email_alert:
agent_recovery_email_task.delay(pk=last_outage.pk)
if agent.overdue_text_alert:
agent_recovery_sms_task.delay(pk=last_outage.pk)
recovery = agent.recoveryactions.filter(last_run=None).last()
if recovery is not None:
recovery.last_run = djangotime.now()
recovery.save(update_fields=["last_run"])
return Response(recovery.send())
# handle agent update
if agent.pendingactions.filter(
action_type="agentupdate", status="pending"
).exists():
update = agent.pendingactions.filter(
action_type="agentupdate", status="pending"
).last()
update.status = "completed"
update.save(update_fields=["status"])
return Response(update.details)
# get any pending actions
if agent.pendingactions.filter(status="pending").exists():
agent.handle_pending_actions()
return Response("ok") return Response("ok")
def post(self, request): def post(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
updates = request.data["wua_updates"]
for update in updates:
if agent.winupdates.filter(guid=update["guid"]).exists():
u = agent.winupdates.filter(guid=update["guid"]).last()
u.downloaded = update["downloaded"]
u.installed = update["installed"]
u.save(update_fields=["downloaded", "installed"])
else:
try:
kb = "KB" + update["kb_article_ids"][0]
except:
continue
serializer = WinAgentSerializer(instance=agent, data=request.data, partial=True) WinUpdate(
serializer.is_valid(raise_exception=True) agent=agent,
serializer.save(last_seen=djangotime.now()) guid=update["guid"],
kb=kb,
title=update["title"],
installed=update["installed"],
downloaded=update["downloaded"],
description=update["description"],
severity=update["severity"],
categories=update["categories"],
category_ids=update["category_ids"],
kb_article_ids=update["kb_article_ids"],
more_info_urls=update["more_info_urls"],
support_url=update["support_url"],
revision_number=update["revision_number"],
).save()
sync_salt_modules_task.delay(agent.pk) agent.delete_superseded_updates()
check_for_updates_task.apply_async(
queue="wupdate", kwargs={"pk": agent.pk, "wait": True}
)
if not agent.choco_installed: # more superseded updates cleanup
install_chocolatey.delay(agent.pk, wait=True) if pyver.parse(agent.version) <= pyver.parse("1.4.2"):
for u in agent.winupdates.filter(
date_installed__isnull=True, result="failed"
).exclude(installed=True):
u.delete()
return Response("ok")
class SupersededWinUpdate(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
updates = agent.winupdates.filter(guid=request.data["guid"])
for u in updates:
u.delete()
return Response("ok") return Response("ok")
@@ -254,31 +280,28 @@ class CheckRunner(APIView):
ret = { ret = {
"agent": agent.pk, "agent": agent.pk,
"check_interval": agent.check_interval, "check_interval": agent.check_interval,
"checks": CheckRunnerGetSerializerV3(checks, many=True).data, "checks": CheckRunnerGetSerializer(checks, many=True).data,
} }
return Response(ret) return Response(ret)
def patch(self, request): def patch(self, request):
from logs.models import AuditLog
check = get_object_or_404(Check, pk=request.data["id"]) check = get_object_or_404(Check, pk=request.data["id"])
check.last_run = djangotime.now() check.last_run = djangotime.now()
check.save(update_fields=["last_run"]) check.save(update_fields=["last_run"])
status = check.handle_checkv2(request.data) status = check.handle_checkv2(request.data)
# create audit entry
AuditLog.objects.create(
username=check.agent.hostname,
agent=check.agent.hostname,
object_type="agent",
action="check_run",
message=f"{check.readable_desc} was run on {check.agent.hostname}. Status: {status}",
after_value=Check.serialize(check),
)
return Response(status) return Response(status)
class CheckRunnerInterval(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def get(self, request, agentid):
agent = get_object_or_404(Agent, agent_id=agentid)
return Response({"agent": agent.pk, "check_interval": agent.check_interval})
class TaskRunner(APIView): class TaskRunner(APIView):
""" """
For the windows golang agent For the windows golang agent
@@ -305,6 +328,8 @@ class TaskRunner(APIView):
serializer.save(last_run=djangotime.now()) serializer.save(last_run=djangotime.now())
new_task = AutomatedTask.objects.get(pk=task.pk) new_task = AutomatedTask.objects.get(pk=task.pk)
new_task.handle_alert()
AuditLog.objects.create( AuditLog.objects.create(
username=agent.hostname, username=agent.hostname,
agent=agent.hostname, agent=agent.hostname,
@@ -317,154 +342,6 @@ class TaskRunner(APIView):
return Response("ok") return Response("ok")
class SaltMinion(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def get(self, request, agentid):
agent = get_object_or_404(Agent, agent_id=agentid)
ret = {
"latestVer": settings.LATEST_SALT_VER,
"currentVer": agent.salt_ver,
"salt_id": agent.salt_id,
"downloadURL": agent.winsalt_dl,
}
return Response(ret)
def post(self, request):
# accept the salt key
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
if agent.salt_id != request.data["saltid"]:
return notify_error("Salt keys do not match")
try:
resp = requests.post(
f"http://{settings.SALT_HOST}:8123/run",
json=[
{
"client": "wheel",
"fun": "key.accept",
"match": request.data["saltid"],
"username": settings.SALT_USERNAME,
"password": settings.SALT_PASSWORD,
"eauth": "pam",
}
],
timeout=30,
)
except Exception:
return notify_error("No communication between agent and salt-api")
try:
data = resp.json()["return"][0]["data"]
minion = data["return"]["minions"][0]
except Exception:
return notify_error("Key error")
if data["success"] and minion == request.data["saltid"]:
return Response("Salt key was accepted")
else:
return notify_error("Not accepted")
def patch(self, request):
# sync modules
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
r = agent.salt_api_cmd(timeout=45, func="saltutil.sync_modules")
if r == "timeout" or r == "error":
return notify_error("Failed to sync salt modules")
if isinstance(r, list) and any("modules" in i for i in r):
return Response("Successfully synced salt modules")
elif isinstance(r, list) and not r:
return Response("Modules are already in sync")
else:
return notify_error(f"Failed to sync salt modules: {str(r)}")
def put(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
agent.salt_ver = request.data["ver"]
agent.save(update_fields=["salt_ver"])
return Response("ok")
class WinUpdater(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def get(self, request, agentid):
agent = get_object_or_404(Agent, agent_id=agentid)
agent.delete_superseded_updates()
patches = agent.winupdates.filter(action="approve").exclude(installed=True)
return Response(ApprovedUpdateSerializer(patches, many=True).data)
# agent sends patch results as it's installing them
def patch(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
kb = request.data["kb"]
results = request.data["results"]
update = agent.winupdates.get(kb=kb)
if results == "error" or results == "failed":
update.result = results
update.save(update_fields=["result"])
elif results == "success":
update.result = "success"
update.downloaded = True
update.installed = True
update.date_installed = djangotime.now()
update.save(
update_fields=[
"result",
"downloaded",
"installed",
"date_installed",
]
)
elif results == "alreadyinstalled":
update.result = "success"
update.downloaded = True
update.installed = True
update.save(update_fields=["result", "downloaded", "installed"])
return Response("ok")
# agent calls this after it's finished installing all patches
def post(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
reboot_policy = agent.get_patch_policy().reboot_after_install
reboot = False
if reboot_policy == "always":
reboot = True
if request.data["reboot"]:
if reboot_policy == "required":
reboot = True
elif reboot_policy == "never":
agent.needs_reboot = True
agent.save(update_fields=["needs_reboot"])
if reboot:
if agent.has_nats:
asyncio.run(agent.nats_cmd({"func": "rebootnow"}, wait=False))
else:
agent.salt_api_async(
func="system.reboot",
arg=7,
kwargs={"in_seconds": True},
)
logger.info(f"{agent.hostname} is rebooting after updates were installed.")
else:
check_for_updates_task.apply_async(
queue="wupdate", kwargs={"pk": agent.pk, "wait": False}
)
return Response("ok")
class SysInfo(APIView): class SysInfo(APIView):
authentication_classes = [TokenAuthentication] authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
@@ -480,29 +357,6 @@ class SysInfo(APIView):
return Response("ok") return Response("ok")
class MeshInfo(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def get(self, request, pk):
agent = get_object_or_404(Agent, pk=pk)
return Response(agent.mesh_node_id)
def patch(self, request, pk):
agent = get_object_or_404(Agent, pk=pk)
if "nodeidhex" in request.data:
# agent <= 1.1.0
nodeid = request.data["nodeidhex"]
else:
# agent >= 1.1.1
nodeid = request.data["nodeid"]
agent.mesh_node_id = nodeid
agent.save(update_fields=["mesh_node_id"])
return Response("ok")
class MeshExe(APIView): class MeshExe(APIView):
""" Sends the mesh exe to the installer """ """ Sends the mesh exe to the installer """
@@ -567,10 +421,6 @@ class NewAgent(APIView):
reload_nats() reload_nats()
# Generate policies for new agent
agent.generate_checks_from_policies()
agent.generate_tasks_from_policies()
# create agent install audit record # create agent install audit record
AuditLog.objects.create( AuditLog.objects.create(
username=request.user, username=request.user,
@@ -627,13 +477,3 @@ class Installer(APIView):
) )
return Response("ok") return Response("ok")
class InstallSalt(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def get(self, request, agentid):
agent = get_object_or_404(Agent, agent_id=agentid)
install_salt_task.delay(agent.pk)
return Response("ok")

View File

@@ -0,0 +1,20 @@
# Generated by Django 3.1.4 on 2021-02-12 14:08
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('alerts', '0004_auto_20210212_1408'),
('automation', '0006_delete_policyexclusions'),
]
operations = [
migrations.AddField(
model_name='policy',
name='alert_template',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='policies', to='alerts.alerttemplate'),
),
]

View File

@@ -1,6 +1,5 @@
from django.db import models from django.db import models
from agents.models import Agent from agents.models import Agent
from clients.models import Site, Client
from core.models import CoreSettings from core.models import CoreSettings
from logs.models import BaseAuditModel from logs.models import BaseAuditModel
@@ -10,6 +9,36 @@ class Policy(BaseAuditModel):
desc = models.CharField(max_length=255, null=True, blank=True) desc = models.CharField(max_length=255, null=True, blank=True)
active = models.BooleanField(default=False) active = models.BooleanField(default=False)
enforced = models.BooleanField(default=False) enforced = models.BooleanField(default=False)
alert_template = models.ForeignKey(
"alerts.AlertTemplate",
related_name="policies",
on_delete=models.SET_NULL,
null=True,
blank=True,
)
def save(self, *args, **kwargs):
from automation.tasks import generate_agent_checks_from_policies_task
# get old policy if exists
old_policy = type(self).objects.get(pk=self.pk) if self.pk else None
super(BaseAuditModel, self).save(*args, **kwargs)
# 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,
create_tasks=True,
)
def delete(self, *args, **kwargs):
from automation.tasks import generate_agent_checks_task
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)
@property @property
def is_default_server_policy(self): def is_default_server_policy(self):
@@ -58,6 +87,11 @@ class Policy(BaseAuditModel):
@staticmethod @staticmethod
def cascade_policy_tasks(agent): def cascade_policy_tasks(agent):
from autotasks.tasks import delete_win_task_schedule
from autotasks.models import AutomatedTask
from logs.models import PendingAction
# List of all tasks to be applied # List of all tasks to be applied
tasks = list() tasks = list()
added_task_pks = list() added_task_pks = list()
@@ -107,6 +141,35 @@ class Policy(BaseAuditModel):
tasks.append(task) tasks.append(task)
added_task_pks.append(task.pk) added_task_pks.append(task.pk)
# remove policy tasks from agent not included in policy
for task in agent.autotasks.filter(
parent_task__in=[
taskpk
for taskpk in agent_tasks_parent_pks
if taskpk not in added_task_pks
]
):
delete_win_task_schedule.delay(task.pk)
# 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"])
return [task for task in tasks if task.pk not in agent_tasks_parent_pks] return [task for task in tasks if task.pk not in agent_tasks_parent_pks]
@staticmethod @staticmethod
@@ -280,6 +343,15 @@ class Policy(BaseAuditModel):
+ eventlog_checks + eventlog_checks
) )
# remove policy checks from agent that fell out of policy scope
agent.agentchecks.filter(
parent_check__in=[
checkpk
for checkpk in agent_checks_parent_pks
if checkpk not in [check.pk for check in final_list]
]
).delete()
return [ return [
check for check in final_list if check.pk not in agent_checks_parent_pks check for check in final_list if check.pk not in agent_checks_parent_pks
] ]

View File

@@ -1,18 +1,14 @@
from django.db.models.base import Model
from rest_framework.serializers import ( from rest_framework.serializers import (
ModelSerializer, ModelSerializer,
SerializerMethodField, SerializerMethodField,
StringRelatedField,
ReadOnlyField, ReadOnlyField,
) )
from clients.serializers import ClientSerializer, SiteSerializer
from agents.serializers import AgentHostnameSerializer
from .models import Policy from .models import Policy
from agents.models import Agent
from autotasks.models import AutomatedTask from autotasks.models import AutomatedTask
from checks.models import Check from checks.models import Check
from clients.models import Client, Site from clients.models import Client
from winupdate.serializers import WinUpdatePolicySerializer from winupdate.serializers import WinUpdatePolicySerializer
@@ -24,15 +20,11 @@ class PolicySerializer(ModelSerializer):
class PolicyTableSerializer(ModelSerializer): class PolicyTableSerializer(ModelSerializer):
server_clients = ClientSerializer(many=True, read_only=True)
server_sites = SiteSerializer(many=True, read_only=True)
workstation_clients = ClientSerializer(many=True, read_only=True)
workstation_sites = SiteSerializer(many=True, read_only=True)
agents = AgentHostnameSerializer(many=True, read_only=True)
default_server_policy = ReadOnlyField(source="is_default_server_policy") default_server_policy = ReadOnlyField(source="is_default_server_policy")
default_workstation_policy = ReadOnlyField(source="is_default_workstation_policy") default_workstation_policy = ReadOnlyField(source="is_default_workstation_policy")
agents_count = SerializerMethodField(read_only=True) agents_count = SerializerMethodField(read_only=True)
winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True) winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True)
alert_template = ReadOnlyField(source="alert_template.id")
class Meta: class Meta:
model = Policy model = Policy
@@ -78,49 +70,16 @@ class PolicyCheckSerializer(ModelSerializer):
"assignedtask", "assignedtask",
"text_alert", "text_alert",
"email_alert", "email_alert",
"dashboard_alert",
) )
depth = 1 depth = 1
class AutoTasksFieldSerializer(ModelSerializer): class AutoTasksFieldSerializer(ModelSerializer):
assigned_check = PolicyCheckSerializer(read_only=True) assigned_check = PolicyCheckSerializer(read_only=True)
script = ReadOnlyField(source="script.id")
class Meta: class Meta:
model = AutomatedTask model = AutomatedTask
fields = ("id", "enabled", "name", "schedule", "assigned_check") fields = "__all__"
depth = 1
class AutoTaskPolicySerializer(ModelSerializer):
autotasks = AutoTasksFieldSerializer(many=True, read_only=True)
class Meta:
model = Policy
fields = (
"id",
"name",
"autotasks",
)
depth = 2
class RelatedClientPolicySerializer(ModelSerializer):
class Meta:
model = Client
fields = ("workstation_policy", "server_policy")
depth = 1
class RelatedSitePolicySerializer(ModelSerializer):
class Meta:
model = Site
fields = ("workstation_policy", "server_policy")
depth = 1
class RelatedAgentPolicySerializer(ModelSerializer):
class Meta:
model = Agent
fields = ("policy",)
depth = 1 depth = 1

View File

@@ -1,4 +1,5 @@
from automation.models import Policy from automation.models import Policy
from autotasks.models import AutomatedTask
from checks.models import Check from checks.models import Check
from agents.models import Agent from agents.models import Agent
@@ -6,75 +7,86 @@ from tacticalrmm.celery import app
@app.task @app.task
def generate_agent_checks_from_policies_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):
# copies the policy checks to all affected agents
#
# clear: clears all policy checks first
# create_tasks: also create tasks after checks are generated
###
policypk,
clear=False,
create_tasks=False,
):
policy = Policy.objects.get(pk=policypk) policy = Policy.objects.get(pk=policypk)
if policy.is_default_server_policy and policy.is_default_workstation_policy: if policy.is_default_server_policy and policy.is_default_workstation_policy:
agents = Agent.objects.all() agents = Agent.objects.prefetch_related("policy").only("pk", "monitoring_type")
elif policy.is_default_server_policy: elif policy.is_default_server_policy:
agents = Agent.objects.filter(monitoring_type="server") agents = Agent.objects.filter(monitoring_type="server").only(
"pk", "monitoring_type"
)
elif policy.is_default_workstation_policy: elif policy.is_default_workstation_policy:
agents = Agent.objects.filter(monitoring_type="workstation") agents = Agent.objects.filter(monitoring_type="workstation").only(
"pk", "monitoring_type"
)
else: else:
agents = policy.related_agents() agents = policy.related_agents().only("pk")
for agent in agents: for agent in agents:
agent.generate_checks_from_policies(clear=clear) agent.generate_checks_from_policies()
if create_tasks: if create_tasks:
agent.generate_tasks_from_policies( agent.generate_tasks_from_policies()
clear=clear,
)
@app.task @app.task
def generate_agent_checks_by_location_task( # generates policy checks on a list of agents and optionally generate automated tasks
location, mon_type, clear=False, create_tasks=False 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()
@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): for agent in Agent.objects.filter(**location).filter(monitoring_type=mon_type):
agent.generate_checks_from_policies(clear=clear) agent.generate_checks_from_policies()
if create_tasks: if create_tasks:
agent.generate_tasks_from_policies(clear=clear) agent.generate_tasks_from_policies()
@app.task @app.task
def generate_all_agent_checks_task(mon_type, clear=False, create_tasks=False): # 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): for agent in Agent.objects.filter(monitoring_type=mon_type):
agent.generate_checks_from_policies(clear=clear) agent.generate_checks_from_policies()
if create_tasks: if create_tasks:
agent.generate_tasks_from_policies(clear=clear) agent.generate_tasks_from_policies()
@app.task @app.task
# deletes a policy managed check from all agents
def delete_policy_check_task(checkpk): def delete_policy_check_task(checkpk):
Check.objects.filter(parent_check=checkpk).delete() Check.objects.filter(parent_check=checkpk).delete()
@app.task @app.task
# updates policy managed check fields on agents
def update_policy_check_fields_task(checkpk): def update_policy_check_fields_task(checkpk):
check = Check.objects.get(pk=checkpk) check = Check.objects.get(pk=checkpk)
Check.objects.filter(parent_check=checkpk).update( Check.objects.filter(parent_check=checkpk).update(
threshold=check.threshold, warning_threshold=check.warning_threshold,
error_threshold=check.error_threshold,
alert_severity=check.alert_severity,
name=check.name, name=check.name,
disk=check.disk,
fails_b4_alert=check.fails_b4_alert, fails_b4_alert=check.fails_b4_alert,
ip=check.ip, ip=check.ip,
script=check.script,
script_args=check.script_args, script_args=check.script_args,
info_return_codes=check.info_return_codes,
warning_return_codes=check.warning_return_codes,
timeout=check.timeout, timeout=check.timeout,
pass_if_start_pending=check.pass_if_start_pending, pass_if_start_pending=check.pass_if_start_pending,
pass_if_svc_not_exist=check.pass_if_svc_not_exist, pass_if_svc_not_exist=check.pass_if_svc_not_exist,
@@ -89,32 +101,31 @@ def update_policy_check_fields_task(checkpk):
search_last_days=check.search_last_days, search_last_days=check.search_last_days,
email_alert=check.email_alert, email_alert=check.email_alert,
text_alert=check.text_alert, text_alert=check.text_alert,
dashboard_alert=check.dashboard_alert,
) )
@app.task @app.task
def generate_agent_tasks_from_policies_task(policypk, clear=False): # generates policy tasks on agents affected by a policy
def generate_agent_tasks_from_policies_task(policypk):
policy = Policy.objects.get(pk=policypk) policy = Policy.objects.get(pk=policypk)
if policy.is_default_server_policy and policy.is_default_workstation_policy: if policy.is_default_server_policy and policy.is_default_workstation_policy:
agents = Agent.objects.all() agents = Agent.objects.prefetch_related("policy").only("pk", "monitoring_type")
elif policy.is_default_server_policy: elif policy.is_default_server_policy:
agents = Agent.objects.filter(monitoring_type="server") agents = Agent.objects.filter(monitoring_type="server").only(
"pk", "monitoring_type"
)
elif policy.is_default_workstation_policy: elif policy.is_default_workstation_policy:
agents = Agent.objects.filter(monitoring_type="workstation") agents = Agent.objects.filter(monitoring_type="workstation").only(
"pk", "monitoring_type"
)
else: else:
agents = policy.related_agents() agents = policy.related_agents().only("pk")
for agent in agents: for agent in agents:
agent.generate_tasks_from_policies(clear=clear) agent.generate_tasks_from_policies()
@app.task
def generate_agent_tasks_by_location_task(location, mon_type, clear=False):
for agent in Agent.objects.filter(**location).filter(monitoring_type=mon_type):
agent.generate_tasks_from_policies(clear=clear)
@app.task @app.task
@@ -135,13 +146,23 @@ def run_win_policy_autotask_task(task_pks):
@app.task @app.task
def update_policy_task_fields_task(taskpk, enabled): def update_policy_task_fields_task(taskpk, update_agent=False):
from autotasks.models import AutomatedTask
from autotasks.tasks import enable_or_disable_win_task from autotasks.tasks import enable_or_disable_win_task
tasks = AutomatedTask.objects.filter(parent_task=taskpk) task = AutomatedTask.objects.get(pk=taskpk)
tasks.update(enabled=enabled) 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 autotask in tasks: if update_agent:
enable_or_disable_win_task(autotask.pk, enabled) for task in AutomatedTask.objects.filter(parent_task=taskpk):
enable_or_disable_win_task.delay(task.pk, task.enabled)

View File

@@ -9,13 +9,10 @@ from .serializers import (
PolicyTableSerializer, PolicyTableSerializer,
PolicySerializer, PolicySerializer,
PolicyTaskStatusSerializer, PolicyTaskStatusSerializer,
AutoTaskPolicySerializer,
PolicyOverviewSerializer, PolicyOverviewSerializer,
PolicyCheckStatusSerializer, PolicyCheckStatusSerializer,
PolicyCheckSerializer, PolicyCheckSerializer,
RelatedAgentPolicySerializer, AutoTasksFieldSerializer,
RelatedSitePolicySerializer,
RelatedClientPolicySerializer,
) )
@@ -91,7 +88,7 @@ class TestPolicyViews(TacticalTestCase):
self.check_not_authenticated("post", url) self.check_not_authenticated("post", url)
@patch("automation.tasks.generate_agent_checks_from_policies_task.delay") @patch("automation.tasks.generate_agent_checks_from_policies_task.delay")
def test_update_policy(self, mock_checks_task): def test_update_policy(self, generate_agent_checks_from_policies_task):
# returns 404 for invalid policy pk # returns 404 for invalid policy pk
resp = self.client.put("/automation/policies/500/", format="json") resp = self.client.put("/automation/policies/500/", format="json")
self.assertEqual(resp.status_code, 404) self.assertEqual(resp.status_code, 404)
@@ -110,7 +107,7 @@ class TestPolicyViews(TacticalTestCase):
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
# only called if active or enforced are updated # only called if active or enforced are updated
mock_checks_task.assert_not_called() generate_agent_checks_from_policies_task.assert_not_called()
data = { data = {
"name": "Test Policy Update", "name": "Test Policy Update",
@@ -121,42 +118,43 @@ class TestPolicyViews(TacticalTestCase):
resp = self.client.put(url, data, format="json") resp = self.client.put(url, data, format="json")
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
mock_checks_task.assert_called_with( generate_agent_checks_from_policies_task.assert_called_with(
policypk=policy.pk, clear=True, create_tasks=True policypk=policy.pk, create_tasks=True
) )
self.check_not_authenticated("put", url) self.check_not_authenticated("put", url)
@patch("automation.tasks.generate_agent_checks_from_policies_task.delay") @patch("automation.tasks.generate_agent_checks_task.delay")
@patch("automation.tasks.generate_agent_tasks_from_policies_task.delay") def test_delete_policy(self, generate_agent_checks_task):
def test_delete_policy(self, mock_tasks_task, mock_checks_task):
# returns 404 for invalid policy pk # returns 404 for invalid policy pk
resp = self.client.delete("/automation/policies/500/", format="json") resp = self.client.delete("/automation/policies/500/", format="json")
self.assertEqual(resp.status_code, 404) self.assertEqual(resp.status_code, 404)
# setup data
policy = baker.make("automation.Policy") policy = baker.make("automation.Policy")
site = baker.make("clients.Site")
agents = baker.make_recipe(
"agents.agent", site=site, policy=policy, _quantity=3
)
url = f"/automation/policies/{policy.pk}/" url = f"/automation/policies/{policy.pk}/"
resp = self.client.delete(url, format="json") resp = self.client.delete(url, format="json")
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
mock_checks_task.assert_called_with(policypk=policy.pk, clear=True) generate_agent_checks_task.assert_called_with(
mock_tasks_task.assert_called_with(policypk=policy.pk, clear=True) [agent.pk for agent in agents], create_tasks=True
)
self.check_not_authenticated("delete", url) self.check_not_authenticated("delete", url)
def test_get_all_policy_tasks(self): def test_get_all_policy_tasks(self):
# returns 404 for invalid policy pk
resp = self.client.get("/automation/500/policyautomatedtasks/", format="json")
self.assertEqual(resp.status_code, 404)
# create policy with tasks # create policy with tasks
policy = baker.make("automation.Policy") policy = baker.make("automation.Policy")
baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3) tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
url = f"/automation/{policy.pk}/policyautomatedtasks/" url = f"/automation/{policy.pk}/policyautomatedtasks/"
resp = self.client.get(url, format="json") resp = self.client.get(url, format="json")
serializer = AutoTaskPolicySerializer(policy) serializer = AutoTasksFieldSerializer(tasks, many=True)
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data) self.assertEqual(resp.data, serializer.data)
@@ -182,8 +180,9 @@ class TestPolicyViews(TacticalTestCase):
self.check_not_authenticated("get", url) self.check_not_authenticated("get", url)
def test_get_policy_check_status(self): def test_get_policy_check_status(self):
# set data # setup data
agent = baker.make_recipe("agents.agent") site = baker.make("clients.Site")
agent = baker.make_recipe("agents.agent", site=site)
policy = baker.make("automation.Policy") policy = baker.make("automation.Policy")
policy_diskcheck = baker.make_recipe("checks.diskspace_check", policy=policy) policy_diskcheck = baker.make_recipe("checks.diskspace_check", policy=policy)
managed_check = baker.make_recipe( managed_check = baker.make_recipe(
@@ -248,274 +247,6 @@ class TestPolicyViews(TacticalTestCase):
self.check_not_authenticated("get", url) self.check_not_authenticated("get", url)
@patch("agents.models.Agent.generate_checks_from_policies")
@patch("automation.tasks.generate_agent_checks_by_location_task.delay")
def test_update_policy_add(
self,
mock_checks_location_task,
mock_checks_task,
):
url = f"/automation/related/"
# data setup
policy = baker.make("automation.Policy")
client = baker.make("clients.Client")
site = baker.make("clients.Site", client=client)
agent = baker.make_recipe("agents.agent", site=site)
# test add client to policy data
client_server_payload = {
"type": "client",
"pk": agent.client.pk,
"server_policy": policy.pk,
}
client_workstation_payload = {
"type": "client",
"pk": agent.client.pk,
"workstation_policy": policy.pk,
}
# test add site to policy data
site_server_payload = {
"type": "site",
"pk": agent.site.pk,
"server_policy": policy.pk,
}
site_workstation_payload = {
"type": "site",
"pk": agent.site.pk,
"workstation_policy": policy.pk,
}
# test add agent to policy data
agent_payload = {"type": "agent", "pk": agent.pk, "policy": policy.pk}
# test client server policy add
resp = self.client.post(url, client_server_payload, format="json")
self.assertEqual(resp.status_code, 200)
# called because the relation changed
mock_checks_location_task.assert_called_with(
location={"site__client_id": client.id},
mon_type="server",
clear=True,
create_tasks=True,
)
mock_checks_location_task.reset_mock()
# test client workstation policy add
resp = self.client.post(url, client_workstation_payload, format="json")
self.assertEqual(resp.status_code, 200)
# called because the relation changed
mock_checks_location_task.assert_called_with(
location={"site__client_id": client.id},
mon_type="workstation",
clear=True,
create_tasks=True,
)
mock_checks_location_task.reset_mock()
# test site add server policy
resp = self.client.post(url, site_server_payload, format="json")
self.assertEqual(resp.status_code, 200)
# called because the relation changed
mock_checks_location_task.assert_called_with(
location={"site_id": site.id},
mon_type="server",
clear=True,
create_tasks=True,
)
mock_checks_location_task.reset_mock()
# test site add workstation policy
resp = self.client.post(url, site_workstation_payload, format="json")
self.assertEqual(resp.status_code, 200)
# called because the relation changed
mock_checks_location_task.assert_called_with(
location={"site_id": site.id},
mon_type="workstation",
clear=True,
create_tasks=True,
)
mock_checks_location_task.reset_mock()
# test agent add
resp = self.client.post(url, agent_payload, format="json")
self.assertEqual(resp.status_code, 200)
# called because the relation changed
mock_checks_task.assert_called_with(clear=True)
mock_checks_task.reset_mock()
# Adding the same relations shouldn't trigger mocks
resp = self.client.post(url, client_server_payload, format="json")
self.assertEqual(resp.status_code, 200)
resp = self.client.post(url, client_workstation_payload, format="json")
self.assertEqual(resp.status_code, 200)
mock_checks_location_task.assert_not_called()
resp = self.client.post(url, site_server_payload, format="json")
self.assertEqual(resp.status_code, 200)
resp = self.client.post(url, site_workstation_payload, format="json")
self.assertEqual(resp.status_code, 200)
mock_checks_location_task.assert_not_called()
resp = self.client.post(url, agent_payload, format="json")
self.assertEqual(resp.status_code, 200)
# called because the relation changed
mock_checks_task.assert_not_called()
# test remove client from policy data
client_server_payload = {"type": "client", "pk": client.pk, "server_policy": 0}
client_workstation_payload = {
"type": "client",
"pk": client.pk,
"workstation_policy": 0,
}
# test remove site from policy data
site_server_payload = {"type": "site", "pk": site.pk, "server_policy": 0}
site_workstation_payload = {
"type": "site",
"pk": site.pk,
"workstation_policy": 0,
}
# test remove agent from policy
agent_payload = {"type": "agent", "pk": agent.pk, "policy": 0}
# test client server policy remove
resp = self.client.post(url, client_server_payload, format="json")
self.assertEqual(resp.status_code, 200)
# called because the relation changed
mock_checks_location_task.assert_called_with(
location={"site__client_id": client.id},
mon_type="server",
clear=True,
create_tasks=True,
)
mock_checks_location_task.reset_mock()
# test client workstation policy remove
resp = self.client.post(url, client_workstation_payload, format="json")
self.assertEqual(resp.status_code, 200)
# called because the relation changed
mock_checks_location_task.assert_called_with(
location={"site__client_id": client.id},
mon_type="workstation",
clear=True,
create_tasks=True,
)
mock_checks_location_task.reset_mock()
# test site remove server policy
resp = self.client.post(url, site_server_payload, format="json")
self.assertEqual(resp.status_code, 200)
# called because the relation changed
mock_checks_location_task.assert_called_with(
location={"site_id": site.id},
mon_type="server",
clear=True,
create_tasks=True,
)
mock_checks_location_task.reset_mock()
# test site remove workstation policy
resp = self.client.post(url, site_workstation_payload, format="json")
self.assertEqual(resp.status_code, 200)
# called because the relation changed
mock_checks_location_task.assert_called_with(
location={"site_id": site.id},
mon_type="workstation",
clear=True,
create_tasks=True,
)
mock_checks_location_task.reset_mock()
# test agent remove
resp = self.client.post(url, agent_payload, format="json")
self.assertEqual(resp.status_code, 200)
# called because the relation changed
mock_checks_task.assert_called_with(clear=True)
mock_checks_task.reset_mock()
# adding the same relations shouldn't trigger mocks
resp = self.client.post(url, client_server_payload, format="json")
self.assertEqual(resp.status_code, 200)
resp = self.client.post(url, client_workstation_payload, format="json")
self.assertEqual(resp.status_code, 200)
# shouldn't be called since nothing changed
mock_checks_location_task.assert_not_called()
resp = self.client.post(url, site_server_payload, format="json")
self.assertEqual(resp.status_code, 200)
resp = self.client.post(url, site_workstation_payload, format="json")
self.assertEqual(resp.status_code, 200)
# shouldn't be called since nothing changed
mock_checks_location_task.assert_not_called()
resp = self.client.post(url, agent_payload, format="json")
self.assertEqual(resp.status_code, 200)
# shouldn't be called since nothing changed
mock_checks_task.assert_not_called()
self.check_not_authenticated("post", url)
def test_get_relation_by_type(self):
url = f"/automation/related/"
# data setup
policy = baker.make("automation.Policy")
client = baker.make("clients.Client", workstation_policy=policy)
site = baker.make("clients.Site", server_policy=policy)
agent = baker.make_recipe("agents.agent", site=site, policy=policy)
client_payload = {"type": "client", "pk": client.pk}
# test add site to policy
site_payload = {"type": "site", "pk": site.pk}
# test add agent to policy
agent_payload = {"type": "agent", "pk": agent.pk}
# test client relation get
serializer = RelatedClientPolicySerializer(client)
resp = self.client.patch(url, client_payload, format="json")
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data)
# test site relation get
serializer = RelatedSitePolicySerializer(site)
resp = self.client.patch(url, site_payload, format="json")
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data)
# test agent relation get
serializer = RelatedAgentPolicySerializer(agent)
resp = self.client.patch(url, agent_payload, format="json")
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data)
invalid_payload = {"type": "bad_type", "pk": 5}
resp = self.client.patch(url, invalid_payload, format="json")
self.assertEqual(resp.status_code, 400)
self.check_not_authenticated("patch", url)
def test_get_policy_task_status(self): def test_get_policy_task_status(self):
# policy with a task # policy with a task
@@ -749,11 +480,10 @@ class TestPolicyTasks(TacticalTestCase):
# setup data # setup data
policy = baker.make("automation.Policy", active=True) policy = baker.make("automation.Policy", active=True)
checks = self.create_checks(policy=policy) checks = self.create_checks(policy=policy)
site = baker.make("clients.Site") agent = baker.make_recipe("agents.agent", policy=policy)
agent = baker.make_recipe("agents.agent", site=site, policy=policy)
# test policy assigned to agent # test policy assigned to agent
generate_agent_checks_from_policies_task(policy.id, clear=True) generate_agent_checks_from_policies_task(policy.id)
# make sure all checks were created. should be 7 # make sure all checks were created. should be 7
agent_checks = Agent.objects.get(pk=agent.id).agentchecks.all() agent_checks = Agent.objects.get(pk=agent.id).agentchecks.all()
@@ -766,16 +496,19 @@ class TestPolicyTasks(TacticalTestCase):
if check.check_type == "diskspace": if check.check_type == "diskspace":
self.assertEqual(check.parent_check, checks[0].id) self.assertEqual(check.parent_check, checks[0].id)
self.assertEqual(check.disk, checks[0].disk) self.assertEqual(check.disk, checks[0].disk)
self.assertEqual(check.threshold, checks[0].threshold) self.assertEqual(check.error_threshold, checks[0].error_threshold)
self.assertEqual(check.warning_threshold, checks[0].warning_threshold)
elif check.check_type == "ping": elif check.check_type == "ping":
self.assertEqual(check.parent_check, checks[1].id) self.assertEqual(check.parent_check, checks[1].id)
self.assertEqual(check.ip, checks[1].ip) self.assertEqual(check.ip, checks[1].ip)
elif check.check_type == "cpuload": elif check.check_type == "cpuload":
self.assertEqual(check.parent_check, checks[2].id) self.assertEqual(check.parent_check, checks[2].id)
self.assertEqual(check.threshold, checks[2].threshold) self.assertEqual(check.error_threshold, checks[0].error_threshold)
self.assertEqual(check.warning_threshold, checks[0].warning_threshold)
elif check.check_type == "memory": elif check.check_type == "memory":
self.assertEqual(check.parent_check, checks[3].id) self.assertEqual(check.parent_check, checks[3].id)
self.assertEqual(check.threshold, checks[3].threshold) self.assertEqual(check.error_threshold, checks[0].error_threshold)
self.assertEqual(check.warning_threshold, checks[0].warning_threshold)
elif check.check_type == "winsvc": elif check.check_type == "winsvc":
self.assertEqual(check.parent_check, checks[4].id) self.assertEqual(check.parent_check, checks[4].id)
self.assertEqual(check.svc_name, checks[4].svc_name) self.assertEqual(check.svc_name, checks[4].svc_name)
@@ -811,71 +544,245 @@ class TestPolicyTasks(TacticalTestCase):
7, 7,
) )
def test_generating_agent_policy_checks_by_location(self): @patch("automation.tasks.generate_agent_checks_by_location_task.delay")
from .tasks import generate_agent_checks_by_location_task def test_generating_agent_policy_checks_by_location(
self, generate_agent_checks_by_location_task
):
from automation.tasks import (
generate_agent_checks_by_location_task as generate_agent_checks,
)
# setup data # setup data
policy = baker.make("automation.Policy", active=True) policy = baker.make("automation.Policy", active=True)
self.create_checks(policy=policy) self.create_checks(policy=policy)
clients = baker.make(
"clients.Client",
_quantity=2,
server_policy=policy,
workstation_policy=policy,
)
sites = baker.make("clients.Site", client=cycle(clients), _quantity=4)
server_agent = baker.make_recipe("agents.server_agent", site=sites[0])
workstation_agent = baker.make_recipe("agents.workstation_agent", site=sites[2])
agent1 = baker.make_recipe("agents.server_agent", site=sites[1])
agent2 = baker.make_recipe("agents.workstation_agent", site=sites[3])
generate_agent_checks_by_location_task( baker.make(
{"site_id": sites[0].id}, "autotasks.AutomatedTask", policy=policy, name=seq("Task"), _quantity=3
"server", )
clear=True,
server_agent = baker.make_recipe("agents.server_agent")
workstation_agent = baker.make_recipe("agents.workstation_agent")
# no checks should be preset on agents
self.assertEqual(Agent.objects.get(pk=server_agent.id).agentchecks.count(), 0)
self.assertEqual(
Agent.objects.get(pk=workstation_agent.id).agentchecks.count(), 0
)
# set workstation policy on client and policy checks should be there
workstation_agent.client.workstation_policy = policy
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",
create_tasks=True,
)
generate_agent_checks_by_location_task.reset_mock()
generate_agent_checks(
location={"site__client_id": workstation_agent.client.pk},
mon_type="workstation",
create_tasks=True, create_tasks=True,
) )
# server_agent should have policy checks and the other agents should not # make sure the checks were added
self.assertEqual(
Agent.objects.get(pk=workstation_agent.id).agentchecks.count(), 7
)
self.assertEqual(Agent.objects.get(pk=server_agent.id).agentchecks.count(), 0)
# remove workstation policy from client
workstation_agent.client.workstation_policy = None
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",
create_tasks=True,
)
generate_agent_checks_by_location_task.reset_mock()
generate_agent_checks(
location={"site__client_id": workstation_agent.client.pk},
mon_type="workstation",
create_tasks=True,
)
# make sure the checks were removed
self.assertEqual(
Agent.objects.get(pk=workstation_agent.id).agentchecks.count(), 0
)
self.assertEqual(Agent.objects.get(pk=server_agent.id).agentchecks.count(), 0)
# set server policy on client and policy checks should be there
server_agent.client.server_policy = policy
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",
create_tasks=True,
)
generate_agent_checks_by_location_task.reset_mock()
generate_agent_checks(
location={"site__client_id": server_agent.client.pk},
mon_type="server",
create_tasks=True,
)
# make sure checks were added
self.assertEqual(Agent.objects.get(pk=server_agent.id).agentchecks.count(), 7) self.assertEqual(Agent.objects.get(pk=server_agent.id).agentchecks.count(), 7)
self.assertEqual( self.assertEqual(
Agent.objects.get(pk=workstation_agent.id).agentchecks.count(), 0 Agent.objects.get(pk=workstation_agent.id).agentchecks.count(), 0
) )
self.assertEqual(Agent.objects.get(pk=agent1.id).agentchecks.count(), 0)
generate_agent_checks_by_location_task( # remove server policy from client
{"site__client_id": clients[0].id}, server_agent.client.server_policy = None
"workstation", server_agent.client.save()
clear=True,
# 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",
create_tasks=True, create_tasks=True,
) )
# workstation_agent should now have policy checks and the other agents should not generate_agent_checks_by_location_task.reset_mock()
generate_agent_checks(
location={"site__client_id": server_agent.client.pk},
mon_type="server",
create_tasks=True,
)
# make sure checks were removed
self.assertEqual(Agent.objects.get(pk=server_agent.id).agentchecks.count(), 0)
self.assertEqual(
Agent.objects.get(pk=workstation_agent.id).agentchecks.count(), 0
)
# set workstation policy on site and policy checks should be there
workstation_agent.site.workstation_policy = policy
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",
create_tasks=True,
)
generate_agent_checks_by_location_task.reset_mock()
generate_agent_checks(
location={"site_id": workstation_agent.site.pk},
mon_type="workstation",
create_tasks=True,
)
# make sure checks were added on workstation
self.assertEqual( self.assertEqual(
Agent.objects.get(pk=workstation_agent.id).agentchecks.count(), 7 Agent.objects.get(pk=workstation_agent.id).agentchecks.count(), 7
) )
self.assertEqual(Agent.objects.get(pk=server_agent.id).agentchecks.count(), 7) self.assertEqual(Agent.objects.get(pk=server_agent.id).agentchecks.count(), 0)
self.assertEqual(Agent.objects.get(pk=agent1.id).agentchecks.count(), 0)
self.assertEqual(Agent.objects.get(pk=agent2.id).agentchecks.count(), 0)
def test_generating_policy_checks_for_all_agents(self): # remove workstation policy from site
from .tasks import generate_all_agent_checks_task workstation_agent.site.workstation_policy = None
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",
create_tasks=True,
)
generate_agent_checks_by_location_task.reset_mock()
generate_agent_checks(
location={"site_id": workstation_agent.site.pk},
mon_type="workstation",
create_tasks=True,
)
# make sure checks were removed
self.assertEqual(
Agent.objects.get(pk=workstation_agent.id).agentchecks.count(), 0
)
self.assertEqual(Agent.objects.get(pk=server_agent.id).agentchecks.count(), 0)
# set server policy on site and policy checks should be there
server_agent.site.server_policy = policy
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",
create_tasks=True,
)
generate_agent_checks_by_location_task.reset_mock()
generate_agent_checks(
location={"site_id": server_agent.site.pk},
mon_type="server",
create_tasks=True,
)
# make sure checks were added
self.assertEqual(Agent.objects.get(pk=server_agent.id).agentchecks.count(), 7)
self.assertEqual(
Agent.objects.get(pk=workstation_agent.id).agentchecks.count(), 0
)
# remove server policy from site
server_agent.site.server_policy = None
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",
create_tasks=True,
)
generate_agent_checks_by_location_task.reset_mock()
generate_agent_checks(
location={"site_id": server_agent.site.pk},
mon_type="server",
create_tasks=True,
)
# make sure checks were removed
self.assertEqual(Agent.objects.get(pk=server_agent.id).agentchecks.count(), 0)
self.assertEqual(
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
):
from .tasks import generate_all_agent_checks_task as generate_all_checks
from core.models import CoreSettings from core.models import CoreSettings
# setup data # setup data
policy = baker.make("automation.Policy", active=True) policy = baker.make("automation.Policy", active=True)
self.create_checks(policy=policy) self.create_checks(policy=policy)
site = baker.make("clients.Site") server_agents = baker.make_recipe("agents.server_agent", _quantity=3)
server_agents = baker.make_recipe("agents.server_agent", site=site, _quantity=3) workstation_agents = baker.make_recipe("agents.workstation_agent", _quantity=4)
workstation_agents = baker.make_recipe(
"agents.workstation_agent", site=site, _quantity=4
)
core = CoreSettings.objects.first() core = CoreSettings.objects.first()
core.server_policy = policy core.server_policy = policy
core.workstation_policy = policy
core.save() core.save()
generate_all_agent_checks_task("server", clear=True, create_tasks=True) 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)
# all servers should have 7 checks # all servers should have 7 checks
for agent in server_agents: for agent in server_agents:
@@ -884,24 +791,50 @@ class TestPolicyTasks(TacticalTestCase):
for agent in workstation_agents: for agent in workstation_agents:
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 0) self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 0)
generate_all_agent_checks_task("workstation", clear=True, create_tasks=True) core.server_policy = None
core.workstation_policy = policy
core.save()
# all agents should have 7 checks now 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)
# all workstations should have 7 checks
for agent in server_agents: for agent in server_agents:
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 7) self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 0)
for agent in workstation_agents: for agent in workstation_agents:
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 7) self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 7)
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)
# nothing should have the checks
for agent in server_agents:
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 0)
for agent in workstation_agents:
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 0)
def test_delete_policy_check(self): def test_delete_policy_check(self):
from .tasks import delete_policy_check_task from .tasks import delete_policy_check_task
from .models import Policy from .models import Policy
policy = baker.make("automation.Policy", active=True) policy = baker.make("automation.Policy", active=True)
self.create_checks(policy=policy) self.create_checks(policy=policy)
site = baker.make("clients.Site") agent = baker.make_recipe("agents.server_agent", policy=policy)
agent = baker.make_recipe("agents.server_agent", site=site, policy=policy)
agent.generate_checks_from_policies()
# make sure agent has 7 checks # make sure agent has 7 checks
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 7) self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 7)
@@ -926,7 +859,6 @@ class TestPolicyTasks(TacticalTestCase):
policy = baker.make("automation.Policy", active=True) policy = baker.make("automation.Policy", active=True)
self.create_checks(policy=policy) self.create_checks(policy=policy)
agent = baker.make_recipe("agents.server_agent", policy=policy) agent = baker.make_recipe("agents.server_agent", policy=policy)
agent.generate_checks_from_policies()
# make sure agent has 7 checks # make sure agent has 7 checks
self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 7) self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 7)
@@ -958,10 +890,9 @@ class TestPolicyTasks(TacticalTestCase):
tasks = baker.make( tasks = baker.make(
"autotasks.AutomatedTask", policy=policy, name=seq("Task"), _quantity=3 "autotasks.AutomatedTask", policy=policy, name=seq("Task"), _quantity=3
) )
site = baker.make("clients.Site") agent = baker.make_recipe("agents.server_agent", policy=policy)
agent = baker.make_recipe("agents.server_agent", site=site, policy=policy)
generate_agent_tasks_from_policies_task(policy.id, clear=True) generate_agent_tasks_from_policies_task(policy.id)
agent_tasks = Agent.objects.get(pk=agent.id).autotasks.all() agent_tasks = Agent.objects.get(pk=agent.id).autotasks.all()
@@ -980,63 +911,19 @@ class TestPolicyTasks(TacticalTestCase):
self.assertEqual(task.parent_task, tasks[2].id) self.assertEqual(task.parent_task, tasks[2].id)
self.assertEqual(task.name, tasks[2].name) self.assertEqual(task.name, tasks[2].name)
def test_generate_agent_tasks_by_location(self):
from .tasks import generate_agent_tasks_by_location_task
# setup data
policy = baker.make("automation.Policy", active=True)
baker.make(
"autotasks.AutomatedTask", policy=policy, name=seq("Task"), _quantity=3
)
clients = baker.make(
"clients.Client",
_quantity=2,
server_policy=policy,
workstation_policy=policy,
)
sites = baker.make("clients.Site", client=cycle(clients), _quantity=4)
server_agent = baker.make_recipe("agents.server_agent", site=sites[0])
workstation_agent = baker.make_recipe("agents.workstation_agent", site=sites[2])
agent1 = baker.make_recipe("agents.agent", site=sites[1])
agent2 = baker.make_recipe("agents.agent", site=sites[3])
generate_agent_tasks_by_location_task(
{"site_id": sites[0].id}, "server", clear=True
)
# all servers in site1 and site2 should have 3 tasks
self.assertEqual(
Agent.objects.get(pk=workstation_agent.id).autotasks.count(), 0
)
self.assertEqual(Agent.objects.get(pk=server_agent.id).autotasks.count(), 3)
self.assertEqual(Agent.objects.get(pk=agent1.id).autotasks.count(), 0)
self.assertEqual(Agent.objects.get(pk=agent2.id).autotasks.count(), 0)
generate_agent_tasks_by_location_task(
{"site__client_id": clients[0].id}, "workstation", clear=True
)
# all workstations in Default1 should have 3 tasks
self.assertEqual(
Agent.objects.get(pk=workstation_agent.id).autotasks.count(), 3
)
self.assertEqual(Agent.objects.get(pk=server_agent.id).autotasks.count(), 3)
self.assertEqual(Agent.objects.get(pk=agent1.id).autotasks.count(), 0)
self.assertEqual(Agent.objects.get(pk=agent2.id).autotasks.count(), 0)
@patch("autotasks.tasks.delete_win_task_schedule.delay") @patch("autotasks.tasks.delete_win_task_schedule.delay")
def test_delete_policy_tasks(self, delete_win_task_schedule): def test_delete_policy_tasks(self, delete_win_task_schedule):
from .tasks import delete_policy_autotask_task from .tasks import delete_policy_autotask_task
policy = baker.make("automation.Policy", active=True) policy = baker.make("automation.Policy", active=True)
tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3) tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
site = baker.make("clients.Site") agent = baker.make_recipe("agents.server_agent", policy=policy)
agent = baker.make_recipe("agents.server_agent", site=site, policy=policy)
agent.generate_tasks_from_policies()
delete_policy_autotask_task(tasks[0].id) delete_policy_autotask_task(tasks[0].id)
delete_win_task_schedule.assert_called_with(agent.autotasks.first().id) delete_win_task_schedule.assert_called_with(
agent.autotasks.get(parent_task=tasks[0].id).id
)
@patch("autotasks.tasks.run_win_task.delay") @patch("autotasks.tasks.run_win_task.delay")
def test_run_policy_task(self, run_win_task): def test_run_policy_task(self, run_win_task):
@@ -1051,25 +938,46 @@ class TestPolicyTasks(TacticalTestCase):
for task in tasks: for task in tasks:
run_win_task.assert_any_call(task.id) run_win_task.assert_any_call(task.id)
@patch("agents.models.Agent.nats_cmd") @patch("autotasks.tasks.enable_or_disable_win_task.delay")
def test_update_policy_tasks(self, nats_cmd): def test_update_policy_tasks(self, enable_or_disable_win_task):
from .tasks import update_policy_task_fields_task from .tasks import update_policy_task_fields_task
from autotasks.models import AutomatedTask
nats_cmd.return_value = "ok"
# setup data # setup data
policy = baker.make("automation.Policy", active=True) policy = baker.make("automation.Policy", active=True)
tasks = baker.make( tasks = baker.make(
"autotasks.AutomatedTask", enabled=True, policy=policy, _quantity=3 "autotasks.AutomatedTask", enabled=True, policy=policy, _quantity=3
) )
site = baker.make("clients.Site") agent = baker.make_recipe("agents.server_agent", policy=policy)
agent = baker.make_recipe("agents.server_agent", site=site, policy=policy)
agent.generate_tasks_from_policies()
tasks[0].enabled = False tasks[0].enabled = False
tasks[0].save() tasks[0].save()
update_policy_task_fields_task(tasks[0].id, enabled=False) update_policy_task_fields_task(tasks[0].id)
enable_or_disable_win_task.assert_not_called()
self.assertFalse(AutomatedTask.objects.get(parent_task=tasks[0].id).enabled) self.assertFalse(agent.autotasks.get(parent_task=tasks[0].id).enabled)
update_policy_task_fields_task(tasks[0].id, update_agent=True)
enable_or_disable_win_task.assert_called_with(
agent.autotasks.get(parent_task=tasks[0].id).id, False
)
@patch("agents.models.Agent.generate_tasks_from_policies")
@patch("agents.models.Agent.generate_checks_from_policies")
def test_generate_agent_checks_with_agentpks(self, generate_checks, generate_tasks):
from automation.tasks import generate_agent_checks_task
agents = baker.make_recipe("agents.agent", _quantity=5)
# reset because creating agents triggers it
generate_checks.reset_mock()
generate_tasks.reset_mock()
generate_agent_checks_task([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)
self.assertEquals(generate_checks.call_count, 5)
self.assertEquals(generate_checks.call_count, 5)

View File

@@ -4,7 +4,6 @@ from . import views
urlpatterns = [ urlpatterns = [
path("policies/", views.GetAddPolicies.as_view()), path("policies/", views.GetAddPolicies.as_view()),
path("policies/<int:pk>/related/", views.GetRelated.as_view()), path("policies/<int:pk>/related/", views.GetRelated.as_view()),
path("related/", views.GetRelated.as_view()),
path("policies/overview/", views.OverviewPolicy.as_view()), path("policies/overview/", views.OverviewPolicy.as_view()),
path("policies/<int:pk>/", views.GetUpdateDeletePolicy.as_view()), path("policies/<int:pk>/", views.GetUpdateDeletePolicy.as_view()),
path("<int:pk>/policychecks/", views.PolicyCheck.as_view()), path("<int:pk>/policychecks/", views.PolicyCheck.as_view()),

View File

@@ -2,11 +2,10 @@ from django.shortcuts import get_object_or_404
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import status
from .models import Policy from .models import Policy
from agents.models import Agent from agents.models import Agent
from clients.models import Client, Site from clients.models import Client
from checks.models import Check from checks.models import Check
from autotasks.models import AutomatedTask from autotasks.models import AutomatedTask
from winupdate.models import WinUpdatePolicy from winupdate.models import WinUpdatePolicy
@@ -22,16 +21,10 @@ from .serializers import (
PolicyCheckStatusSerializer, PolicyCheckStatusSerializer,
PolicyCheckSerializer, PolicyCheckSerializer,
PolicyTaskStatusSerializer, PolicyTaskStatusSerializer,
AutoTaskPolicySerializer, AutoTasksFieldSerializer,
RelatedClientPolicySerializer,
RelatedSitePolicySerializer,
RelatedAgentPolicySerializer,
) )
from .tasks import ( from .tasks import (
generate_agent_checks_from_policies_task,
generate_agent_checks_by_location_task,
generate_agent_tasks_from_policies_task,
run_win_policy_autotask_task, run_win_policy_autotask_task,
) )
@@ -72,30 +65,14 @@ class GetUpdateDeletePolicy(APIView):
def put(self, request, pk): def put(self, request, pk):
policy = get_object_or_404(Policy, pk=pk) policy = get_object_or_404(Policy, pk=pk)
old_active = policy.active
old_enforced = policy.enforced
serializer = PolicySerializer(instance=policy, data=request.data, partial=True) serializer = PolicySerializer(instance=policy, data=request.data, partial=True)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
saved_policy = serializer.save() serializer.save()
# Generate agent checks only if active and enforced were changed
if saved_policy.active != old_active or saved_policy.enforced != old_enforced:
generate_agent_checks_from_policies_task.delay(
policypk=policy.pk,
clear=(not saved_policy.active or not saved_policy.enforced),
create_tasks=(saved_policy.active != old_active),
)
return Response("ok") return Response("ok")
def delete(self, request, pk): def delete(self, request, pk):
policy = get_object_or_404(Policy, pk=pk) get_object_or_404(Policy, pk=pk).delete()
# delete all managed policy checks off of agents
generate_agent_checks_from_policies_task.delay(policypk=policy.pk, clear=True)
generate_agent_tasks_from_policies_task.delay(policypk=policy.pk, clear=True)
policy.delete()
return Response("ok") return Response("ok")
@@ -104,8 +81,8 @@ class PolicyAutoTask(APIView):
# tasks associated with policy # tasks associated with policy
def get(self, request, pk): def get(self, request, pk):
policy = get_object_or_404(Policy, pk=pk) tasks = AutomatedTask.objects.filter(policy=pk)
return Response(AutoTaskPolicySerializer(policy).data) return Response(AutoTasksFieldSerializer(tasks, many=True).data)
# get status of all tasks # get status of all tasks
def patch(self, request, task): def patch(self, request, task):
@@ -184,213 +161,12 @@ class GetRelated(APIView):
).data ).data
response["agents"] = AgentHostnameSerializer( response["agents"] = AgentHostnameSerializer(
policy.related_agents(), policy.related_agents().only("pk", "hostname"),
many=True, many=True,
).data ).data
return Response(response) return Response(response)
# update agents, clients, sites to policy
def post(self, request):
related_type = request.data["type"]
pk = request.data["pk"]
# workstation policy is set
if (
"workstation_policy" in request.data
and request.data["workstation_policy"] != 0
):
policy = get_object_or_404(Policy, pk=request.data["workstation_policy"])
if related_type == "client":
client = get_object_or_404(Client, pk=pk)
# Check and see if workstation policy changed and regenerate policies
if (
not client.workstation_policy
or client.workstation_policy
and client.workstation_policy.pk != policy.pk
):
client.workstation_policy = policy
client.save()
generate_agent_checks_by_location_task.delay(
location={"site__client_id": client.id},
mon_type="workstation",
clear=True,
create_tasks=True,
)
if related_type == "site":
site = get_object_or_404(Site, pk=pk)
# Check and see if workstation policy changed and regenerate policies
if (
not site.workstation_policy
or site.workstation_policy
and site.workstation_policy.pk != policy.pk
):
site.workstation_policy = policy
site.save()
generate_agent_checks_by_location_task.delay(
location={"site_id": site.id},
mon_type="workstation",
clear=True,
create_tasks=True,
)
# server policy is set
if "server_policy" in request.data and request.data["server_policy"] != 0:
policy = get_object_or_404(Policy, pk=request.data["server_policy"])
if related_type == "client":
client = get_object_or_404(Client, pk=pk)
# Check and see if server policy changed and regenerate policies
if (
not client.server_policy
or client.server_policy
and client.server_policy.pk != policy.pk
):
client.server_policy = policy
client.save()
generate_agent_checks_by_location_task.delay(
location={"site__client_id": client.id},
mon_type="server",
clear=True,
create_tasks=True,
)
if related_type == "site":
site = get_object_or_404(Site, pk=pk)
# Check and see if server policy changed and regenerate policies
if (
not site.server_policy
or site.server_policy
and site.server_policy.pk != policy.pk
):
site.server_policy = policy
site.save()
generate_agent_checks_by_location_task.delay(
location={"site_id": site.id},
mon_type="server",
clear=True,
create_tasks=True,
)
# If workstation policy was cleared
if (
"workstation_policy" in request.data
and request.data["workstation_policy"] == 0
):
if related_type == "client":
client = get_object_or_404(Client, pk=pk)
# Check if workstation policy is set and update it to None
if client.workstation_policy:
client.workstation_policy = None
client.save()
generate_agent_checks_by_location_task.delay(
location={"site__client_id": client.id},
mon_type="workstation",
clear=True,
create_tasks=True,
)
if related_type == "site":
site = get_object_or_404(Site, pk=pk)
# Check if workstation policy is set and update it to None
if site.workstation_policy:
site.workstation_policy = None
site.save()
generate_agent_checks_by_location_task.delay(
location={"site_id": site.id},
mon_type="workstation",
clear=True,
create_tasks=True,
)
# server policy cleared
if "server_policy" in request.data and request.data["server_policy"] == 0:
if related_type == "client":
client = get_object_or_404(Client, pk=pk)
# Check if server policy is set and update it to None
if client.server_policy:
client.server_policy = None
client.save()
generate_agent_checks_by_location_task.delay(
location={"site__client_id": client.id},
mon_type="server",
clear=True,
create_tasks=True,
)
if related_type == "site":
site = get_object_or_404(Site, pk=pk)
# Check if server policy is set and update it to None
if site.server_policy:
site.server_policy = None
site.save()
generate_agent_checks_by_location_task.delay(
location={"site_id": site.pk},
mon_type="server",
clear=True,
create_tasks=True,
)
# agent policies
if related_type == "agent":
agent = get_object_or_404(Agent, pk=pk)
if "policy" in request.data and request.data["policy"] != 0:
policy = Policy.objects.get(pk=request.data["policy"])
# Check and see if policy changed and regenerate policies
if not agent.policy or agent.policy and agent.policy.pk != policy.pk:
agent.policy = policy
agent.save()
agent.generate_checks_from_policies(clear=True)
agent.generate_tasks_from_policies(clear=True)
else:
if agent.policy:
agent.policy = None
agent.save()
agent.generate_checks_from_policies(clear=True)
agent.generate_tasks_from_policies(clear=True)
return Response("ok")
# view to get policies set on client, site, and workstation
def patch(self, request):
related_type = request.data["type"]
# client, site, or agent pk
pk = request.data["pk"]
if related_type == "agent":
agent = Agent.objects.get(pk=pk)
return Response(RelatedAgentPolicySerializer(agent).data)
if related_type == "site":
site = Site.objects.get(pk=pk)
return Response(RelatedSitePolicySerializer(site).data)
if related_type == "client":
client = Client.objects.get(pk=pk)
return Response(RelatedClientPolicySerializer(client).data)
content = {"error": "Data was submitted incorrectly"}
return Response(content, status=status.HTTP_400_BAD_REQUEST)
class UpdatePatchPolicy(APIView): class UpdatePatchPolicy(APIView):
@@ -422,11 +198,15 @@ class UpdatePatchPolicy(APIView):
agents = None agents = None
if "client" in request.data: if "client" in request.data:
agents = Agent.objects.filter(site__client_id=request.data["client"]) agents = Agent.objects.prefetch_related("winupdatepolicy").filter(
site__client_id=request.data["client"]
)
elif "site" in request.data: elif "site" in request.data:
agents = Agent.objects.filter(site_id=request.data["site"]) agents = Agent.objects.prefetch_related("winupdatepolicy").filter(
site_id=request.data["site"]
)
else: else:
agents = Agent.objects.all() agents = Agent.objects.prefetch_related("winupdatepolicy").only("pk")
for agent in agents: for agent in agents:
winupdatepolicy = agent.winupdatepolicy.get() winupdatepolicy = agent.winupdatepolicy.get()

View File

@@ -7,7 +7,7 @@ class Command(BaseCommand):
help = "Checks for orphaned tasks on all agents and removes them" help = "Checks for orphaned tasks on all agents and removes them"
def handle(self, *args, **kwargs): def handle(self, *args, **kwargs):
agents = Agent.objects.all() agents = Agent.objects.only("pk", "last_seen", "overdue_time", "offline_time")
online = [i for i in agents if i.status == "online"] online = [i for i in agents if i.status == "online"]
for agent in online: for agent in online:
remove_orphaned_win_tasks.delay(agent.pk) remove_orphaned_win_tasks.delay(agent.pk)

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.4 on 2021-01-27 22:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('autotasks', '0010_migrate_days_to_bitdays'),
]
operations = [
migrations.AddField(
model_name='automatedtask',
name='alert_severity',
field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], default='None', max_length=30, null=True),
),
]

View File

@@ -0,0 +1,33 @@
# Generated by Django 3.1.4 on 2021-01-28 04:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('autotasks', '0011_automatedtask_alert_severity'),
]
operations = [
migrations.AddField(
model_name='automatedtask',
name='email_alert',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='automatedtask',
name='email_sent',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='automatedtask',
name='text_alert',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='automatedtask',
name='text_sent',
field=models.DateTimeField(blank=True, null=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.4 on 2021-01-29 03:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('autotasks', '0012_auto_20210128_0417'),
]
operations = [
migrations.AlterField(
model_name='automatedtask',
name='alert_severity',
field=models.CharField(choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], default='info', max_length=30),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.4 on 2021-01-29 21:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('autotasks', '0013_auto_20210129_0307'),
]
operations = [
migrations.AddField(
model_name='automatedtask',
name='dashboard_alert',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.1.4 on 2021-02-05 17:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('autotasks', '0014_automatedtask_dashboard_alert'),
]
operations = [
migrations.AddField(
model_name='automatedtask',
name='resolved_email_sent',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='automatedtask',
name='resolved_text_sent',
field=models.DateTimeField(blank=True, null=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.4 on 2021-02-05 21:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('autotasks', '0015_auto_20210205_1728'),
]
operations = [
migrations.AddField(
model_name='automatedtask',
name='status',
field=models.CharField(choices=[('passing', 'Passing'), ('failing', 'Failing'), ('pending', 'Pending')], default='pending', max_length=30),
),
]

View File

@@ -0,0 +1,29 @@
# Generated by Django 3.1.4 on 2021-02-10 15:12
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('autotasks', '0016_automatedtask_status'),
]
operations = [
migrations.RemoveField(
model_name='automatedtask',
name='email_sent',
),
migrations.RemoveField(
model_name='automatedtask',
name='resolved_email_sent',
),
migrations.RemoveField(
model_name='automatedtask',
name='resolved_text_sent',
),
migrations.RemoveField(
model_name='automatedtask',
name='text_sent',
),
]

View File

@@ -3,13 +3,20 @@ import random
import string import string
import datetime as dt import datetime as dt
from django.utils import timezone as djangotime
from django.conf import settings
from django.db import models from django.db import models
from django.contrib.postgres.fields import ArrayField from django.contrib.postgres.fields import ArrayField
from django.db.models.fields import DateTimeField from django.db.models.fields import DateTimeField
from automation.models import Policy
from logs.models import BaseAuditModel from logs.models import BaseAuditModel
from tacticalrmm.utils import bitdays_to_string from tacticalrmm.utils import bitdays_to_string
from loguru import logger
from alerts.models import SEVERITY_CHOICES
logger.configure(**settings.LOG_CONFIG)
RUN_TIME_DAY_CHOICES = [ RUN_TIME_DAY_CHOICES = [
(0, "Monday"), (0, "Monday"),
(1, "Tuesday"), (1, "Tuesday"),
@@ -33,6 +40,12 @@ SYNC_STATUS_CHOICES = [
("pendingdeletion", "Pending Deletion on Agent"), ("pendingdeletion", "Pending Deletion on Agent"),
] ]
TASK_STATUS_CHOICES = [
("passing", "Passing"),
("failing", "Failing"),
("pending", "Pending"),
]
class AutomatedTask(BaseAuditModel): class AutomatedTask(BaseAuditModel):
agent = models.ForeignKey( agent = models.ForeignKey(
@@ -43,7 +56,7 @@ class AutomatedTask(BaseAuditModel):
blank=True, blank=True,
) )
policy = models.ForeignKey( policy = models.ForeignKey(
Policy, "automation.Policy",
related_name="autotasks", related_name="autotasks",
null=True, null=True,
blank=True, blank=True,
@@ -94,9 +107,18 @@ class AutomatedTask(BaseAuditModel):
execution_time = models.CharField(max_length=100, default="0.0000") execution_time = models.CharField(max_length=100, default="0.0000")
last_run = models.DateTimeField(null=True, blank=True) last_run = models.DateTimeField(null=True, blank=True)
enabled = models.BooleanField(default=True) enabled = models.BooleanField(default=True)
status = models.CharField(
max_length=30, choices=TASK_STATUS_CHOICES, default="pending"
)
sync_status = models.CharField( sync_status = models.CharField(
max_length=100, choices=SYNC_STATUS_CHOICES, default="notsynced" max_length=100, choices=SYNC_STATUS_CHOICES, default="notsynced"
) )
alert_severity = models.CharField(
max_length=30, choices=SEVERITY_CHOICES, default="info"
)
email_alert = models.BooleanField(default=False)
text_alert = models.BooleanField(default=False)
dashboard_alert = models.BooleanField(default=False)
def __str__(self): def __str__(self):
return self.name return self.name
@@ -141,22 +163,50 @@ class AutomatedTask(BaseAuditModel):
def create_policy_task(self, agent=None, policy=None): def create_policy_task(self, agent=None, policy=None):
from .tasks import create_win_task_schedule from .tasks import create_win_task_schedule
# 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 # exit if neither are set or if both are set
if not agent and not policy or agent and policy: if not agent and not policy or agent and policy:
return return
assigned_check = None assigned_check = None
# get correct assigned check to task if set
if agent and self.assigned_check: if agent and self.assigned_check:
assigned_check = agent.agentchecks.get(parent_check=self.assigned_check.pk) # 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: elif policy and self.assigned_check:
assigned_check = policy.policychecks.get(name=self.assigned_check.name) 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( task = AutomatedTask.objects.create(
agent=agent, agent=agent,
policy=policy, policy=policy,
managed_by_policy=bool(agent), managed_by_policy=bool(agent),
parent_task=(self.pk if agent else None), 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=self.script,
script_args=self.script_args, script_args=self.script_args,
assigned_check=assigned_check, assigned_check=assigned_check,
@@ -173,3 +223,215 @@ class AutomatedTask(BaseAuditModel):
) )
create_win_task_schedule.delay(task.pk) create_win_task_schedule.delay(task.pk)
def handle_alert(self) -> None:
from alerts.models import Alert, AlertTemplate
from autotasks.tasks import (
handle_task_email_alert,
handle_task_sms_alert,
handle_resolved_task_sms_alert,
handle_resolved_task_email_alert,
)
self.status = "failing" if self.retcode != 0 else "passing"
self.save()
# return if agent is in maintenance mode
if self.agent.maintenance_mode:
return
# see if agent has an alert template and use that
alert_template = self.agent.get_alert_template()
# resolve alert if it exists
if self.status == "passing":
if Alert.objects.filter(assigned_task=self, resolved=False).exists():
alert = Alert.objects.get(assigned_task=self, resolved=False)
alert.resolve()
# check if resolved email should be send
if (
not alert.resolved_email_sent
and self.email_alert
or alert_template
and alert_template.task_email_on_resolved
):
handle_resolved_task_email_alert.delay(pk=alert.pk)
# check if resolved text should be sent
if (
not alert.resolved_sms_sent
and self.text_alert
or alert_template
and alert_template.task_text_on_resolved
):
handle_resolved_task_sms_alert.delay(pk=alert.pk)
# check if resolved script should be run
if (
alert_template
and alert_template.resolved_action
and not alert.resolved_action_run
):
r = self.agent.run_script(
scriptpk=alert_template.resolved_action.pk,
args=alert_template.resolved_action_args,
timeout=alert_template.resolved_action_timeout,
wait=True,
full=True,
run_on_any=True,
)
# command was successful
if type(r) == dict:
alert.resolved_action_retcode = r["retcode"]
alert.resolved_action_stdout = r["stdout"]
alert.resolved_action_stderr = r["stderr"]
alert.resolved_action_execution_time = "{:.4f}".format(
r["execution_time"]
)
alert.resolved_action_run = djangotime.now()
alert.save()
else:
logger.error(
f"Resolved action: {alert_template.action.name} failed to run on any agent for {self.agent.hostname} resolved alert for task: {self.name}"
)
# create alert if task is failing
else:
if not Alert.objects.filter(assigned_task=self, resolved=False).exists():
alert = Alert.create_task_alert(self)
else:
alert = Alert.objects.get(assigned_task=self, resolved=False)
# check if alert severity changed on task and update the alert
if self.alert_severity != alert.severity:
alert.severity = self.alert_severity
alert.save(update_fields=["severity"])
# create alert in dashboard if enabled
if (
self.dashboard_alert
or alert_template
and alert_template.task_always_alert
):
alert.hidden = False
alert.save()
# send email if enabled
if (
not alert.email_sent
and self.email_alert
or alert_template
and self.alert_severity in alert_template.task_email_alert_severity
and alert_template.check_always_email
):
handle_task_email_alert.delay(
pk=alert.pk,
alert_template=alert_template.check_periodic_alert_days
if alert_template
else None,
)
# send text if enabled
if (
not alert.sms_sent
and self.text_alert
or alert_template
and self.alert_severity in alert_template.task_text_alert_severity
and alert_template.check_always_text
):
handle_task_sms_alert.delay(
pk=alert.pk,
alert_template=alert_template.check_periodic_alert_days
if alert_template
else None,
)
# check if any scripts should be run
if alert_template and alert_template.action and not alert.action_run:
r = self.agent.run_script(
scriptpk=alert_template.action.pk,
args=alert_template.action_args,
timeout=alert_template.action_timeout,
wait=True,
full=True,
run_on_any=True,
)
# command was successful
if type(r) == dict:
alert.action_retcode = r["retcode"]
alert.action_stdout = r["stdout"]
alert.action_stderr = r["stderr"]
alert.action_execution_time = "{:.4f}".format(r["execution_time"])
alert.action_run = djangotime.now()
alert.save()
else:
logger.error(
f"Failure action: {alert_template.action.name} failed to run on any agent for {self.agent.hostname} failure alert for task: {self.name}"
)
def send_email(self):
from core.models import CoreSettings
CORE = CoreSettings.objects.first()
alert_template = self.agent.get_alert_template()
if self.agent:
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Failed"
else:
subject = f"{self} Failed"
body = (
subject
+ f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
)
CORE.send_mail(subject, body, alert_template)
def send_sms(self):
from core.models import CoreSettings
CORE = CoreSettings.objects.first()
alert_template = self.agent.get_alert_template()
if self.agent:
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Failed"
else:
subject = f"{self} Failed"
body = (
subject
+ f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
)
CORE.send_sms(body, alert_template=alert_template)
def send_resolved_email(self):
from core.models import CoreSettings
alert_template = self.agent.get_alert_template()
CORE = CoreSettings.objects.first()
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved"
body = (
subject
+ f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
)
CORE.send_mail(subject, body, alert_template=alert_template)
def send_resolved_sms(self):
from core.models import CoreSettings
alert_template = self.agent.get_alert_template()
CORE = CoreSettings.objects.first()
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved"
body = (
subject
+ f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
)
CORE.send_sms(body, alert_template=alert_template)

View File

@@ -6,6 +6,9 @@ from django.conf import settings
import pytz import pytz
from django.utils import timezone as djangotime from django.utils import timezone as djangotime
from packaging import version as pyver from packaging import version as pyver
from typing import Union
import random
from time import sleep
from .models import AutomatedTask from .models import AutomatedTask
from logs.models import PendingAction from logs.models import PendingAction
@@ -76,9 +79,14 @@ def create_win_task_schedule(pk, pending_action=False):
return "error" return "error"
r = asyncio.run(task.agent.nats_cmd(nats_data, timeout=10)) r = asyncio.run(task.agent.nats_cmd(nats_data, timeout=10))
if r != "ok": if r != "ok":
# don't create pending action if this task was initiated by a pending action # don't create pending action if this task was initiated by a pending action
if not 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( PendingAction(
agent=task.agent, agent=task.agent,
action_type="taskaction", action_type="taskaction",
@@ -144,6 +152,7 @@ def enable_or_disable_win_task(pk, action, pending_action=False):
task.sync_status = "synced" task.sync_status = "synced"
task.save(update_fields=["sync_status"]) task.save(update_fields=["sync_status"])
return "ok" return "ok"
@@ -157,9 +166,13 @@ def delete_win_task_schedule(pk, pending_action=False):
} }
r = asyncio.run(task.agent.nats_cmd(nats_data, timeout=10)) r = asyncio.run(task.agent.nats_cmd(nats_data, timeout=10))
if r != "ok": 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 # don't create pending action if this task was initiated by a pending action
if not 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( PendingAction(
agent=task.agent, agent=task.agent,
action_type="taskaction", action_type="taskaction",
@@ -168,7 +181,7 @@ def delete_win_task_schedule(pk, pending_action=False):
task.sync_status = "pendingdeletion" task.sync_status = "pendingdeletion"
task.save(update_fields=["sync_status"]) task.save(update_fields=["sync_status"])
return return "timeout"
# complete pending action since it was successful # complete pending action since it was successful
if pending_action: if pending_action:
@@ -176,6 +189,9 @@ def delete_win_task_schedule(pk, pending_action=False):
pendingaction.status = "completed" pendingaction.status = "completed"
pendingaction.save(update_fields=["status"]) 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()
return "ok" return "ok"
@@ -230,3 +246,85 @@ def remove_orphaned_win_tasks(agentpk):
logger.info(f"Removed orphaned task {task} from {agent.hostname}") logger.info(f"Removed orphaned task {task} from {agent.hostname}")
logger.info(f"Orphaned task cleanup finished on {agent.hostname}") logger.info(f"Orphaned task cleanup finished on {agent.hostname}")
@app.task
def handle_task_email_alert(pk: int, alert_interval: Union[float, None] = None) -> str:
from alerts.models import Alert
alert = Alert.objects.get(pk=pk)
# first time sending email
if not alert.email_sent:
sleep(random.randint(1, 10))
alert.assigned_task.send_email()
alert.email_sent = djangotime.now()
alert.save(update_fields=["email_sent"])
else:
if alert_interval:
# send an email only if the last email sent is older than alert interval
delta = djangotime.now() - dt.timedelta(days=alert_interval)
if alert.email_sent < delta:
sleep(random.randint(1, 10))
alert.assigned_task.send_email()
alert.email_sent = djangotime.now()
alert.save(update_fields=["email_sent"])
return "ok"
@app.task
def handle_task_sms_alert(pk: int, alert_interval: Union[float, None] = None) -> str:
from alerts.models import Alert
alert = Alert.objects.get(pk=pk)
# first time sending text
if not alert.sms_sent:
sleep(random.randint(1, 3))
alert.assigned_task.send_sms()
alert.sms_sent = djangotime.now()
alert.save(update_fields=["sms_sent"])
else:
if alert_interval:
# send a text only if the last text sent is older than alert interval
delta = djangotime.now() - dt.timedelta(days=alert_interval)
if alert.sms_sent < delta:
sleep(random.randint(1, 3))
alert.assigned_task.send_sms()
alert.sms_sent = djangotime.now()
alert.save(update_fields=["sms_sent"])
return "ok"
@app.task
def handle_resolved_task_sms_alert(pk: int) -> str:
from alerts.models import Alert
alert = Alert.objects.get(pk=pk)
# first time sending text
if not alert.resolved_sms_sent:
sleep(random.randint(1, 3))
alert.assigned_task.send_resolved_sms()
alert.resolved_sms_sent = djangotime.now()
alert.save(update_fields=["resolved_sms_sent"])
return "ok"
@app.task
def handle_resolved_task_email_alert(pk: int) -> str:
from alerts.models import Alert
alert = Alert.objects.get(pk=pk)
# first time sending email
if not alert.resolved_email_sent:
sleep(random.randint(1, 10))
alert.assigned_task.send_resolved_email()
alert.resolved_email_sent = djangotime.now()
alert.save(update_fields=["resolved_email_sent"])
return "ok"

View File

@@ -150,7 +150,9 @@ class TestAutotaskViews(TacticalTestCase):
resp = self.client.patch(url, data, format="json") resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
update_policy_task_fields_task.assert_called_with(policy_task.id, True) update_policy_task_fields_task.assert_called_with(
policy_task.id, update_agent=True
)
self.check_not_authenticated("patch", url) self.check_not_authenticated("patch", url)

View File

@@ -81,6 +81,20 @@ class AutoTask(APIView):
} }
return Response(AutoTaskSerializer(agent, context=ctx).data) return Response(AutoTaskSerializer(agent, context=ctx).data)
def put(self, request, pk):
from automation.tasks import update_policy_task_fields_task
task = get_object_or_404(AutomatedTask, pk=pk)
serializer = TaskSerializer(instance=task, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
if task.policy:
update_policy_task_fields_task.delay(task.pk)
return Response("ok")
def patch(self, request, pk): def patch(self, request, pk):
from automation.tasks import update_policy_task_fields_task from automation.tasks import update_policy_task_fields_task
@@ -93,7 +107,7 @@ class AutoTask(APIView):
enable_or_disable_win_task.delay(pk=task.pk, action=action) enable_or_disable_win_task.delay(pk=task.pk, action=action)
else: else:
update_policy_task_fields_task.delay(task.pk, action) update_policy_task_fields_task.delay(task.pk, update_agent=True)
task.enabled = action task.enabled = action
task.save(update_fields=["enabled"]) task.save(update_fields=["enabled"])

View File

@@ -1,5 +1,6 @@
from django.contrib import admin from django.contrib import admin
from .models import Check from .models import Check, CheckHistory
admin.site.register(Check) admin.site.register(Check)
admin.site.register(CheckHistory)

View File

@@ -1,15 +1,20 @@
from .models import Check from model_bakery.recipe import Recipe
from model_bakery.recipe import Recipe, seq
check = Recipe(Check) check = Recipe("checks.Check")
diskspace_check = check.extend(check_type="diskspace", disk="C:", threshold=75) diskspace_check = check.extend(
check_type="diskspace", disk="C:", warning_threshold=30, error_threshold=75
)
cpuload_check = check.extend(check_type="cpuload", threshold=75) cpuload_check = check.extend(
check_type="cpuload", warning_threshold=30, error_threshold=75
)
ping_check = check.extend(check_type="ping", ip="10.10.10.10") ping_check = check.extend(check_type="ping", ip="10.10.10.10")
memory_check = check.extend(check_type="memory", threshold=75) memory_check = check.extend(
check_type="memory", warning_threshold=30, error_threshold=75
)
winsvc_check = check.extend( winsvc_check = check.extend(
check_type="winsvc", check_type="winsvc",

View File

@@ -0,0 +1,30 @@
# Generated by Django 3.1.4 on 2021-01-09 02:56
import django.contrib.postgres.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("checks", "0010_auto_20200922_1344"),
]
operations = [
migrations.AddField(
model_name="check",
name="run_history",
field=django.contrib.postgres.fields.ArrayField(
base_field=django.contrib.postgres.fields.ArrayField(
base_field=models.PositiveIntegerField(),
blank=True,
null=True,
size=None,
),
blank=True,
default=list,
null=True,
size=None,
),
),
]

View File

@@ -0,0 +1,39 @@
# Generated by Django 3.1.4 on 2021-01-09 21:36
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("checks", "0010_auto_20200922_1344"),
]
operations = [
migrations.CreateModel(
name="CheckHistory",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("x", models.DateTimeField()),
("y", models.PositiveIntegerField()),
("results", models.JSONField(blank=True, null=True)),
(
"check_history",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="check_history",
to="checks.check",
),
),
],
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.4 on 2021-01-10 05:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("checks", "0011_checkhistory"),
]
operations = [
migrations.AlterField(
model_name="checkhistory",
name="y",
field=models.PositiveIntegerField(blank=True, null=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.4 on 2021-01-10 05:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("checks", "0012_auto_20210110_0503"),
]
operations = [
migrations.AlterField(
model_name="checkhistory",
name="y",
field=models.PositiveIntegerField(null=True),
),
]

View File

@@ -0,0 +1,13 @@
# Generated by Django 3.1.4 on 2021-01-10 18:08
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("checks", "0013_auto_20210110_0505"),
("checks", "0011_check_run_history"),
]
operations = []

View File

@@ -0,0 +1,27 @@
# Generated by Django 3.1.4 on 2021-01-10 18:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("checks", "0014_merge_20210110_1808"),
]
operations = [
migrations.RemoveField(
model_name="check",
name="run_history",
),
migrations.AlterField(
model_name="checkhistory",
name="x",
field=models.DateTimeField(auto_now_add=True),
),
migrations.AlterField(
model_name="checkhistory",
name="y",
field=models.PositiveIntegerField(blank=True, default=None, null=True),
),
]

View File

@@ -0,0 +1,43 @@
# Generated by Django 3.1.4 on 2021-01-23 01:49
import django.contrib.postgres.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('checks', '0015_auto_20210110_1808'),
]
operations = [
migrations.RemoveField(
model_name='check',
name='threshold',
),
migrations.AddField(
model_name='check',
name='alert_severity',
field=models.CharField(choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], default='warning', max_length=15),
),
migrations.AddField(
model_name='check',
name='error_threshold',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='check',
name='info_return_codes',
field=django.contrib.postgres.fields.ArrayField(base_field=models.PositiveIntegerField(), blank=True, default=list, null=True, size=None),
),
migrations.AddField(
model_name='check',
name='warning_return_codes',
field=django.contrib.postgres.fields.ArrayField(base_field=models.PositiveIntegerField(), blank=True, default=list, null=True, size=None),
),
migrations.AddField(
model_name='check',
name='warning_threshold',
field=models.PositiveIntegerField(blank=True, default=0, null=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.4 on 2021-01-29 21:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('checks', '0016_auto_20210123_0149'),
]
operations = [
migrations.AddField(
model_name='check',
name='dashboard_alert',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.4 on 2021-02-05 16:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('checks', '0017_check_dashboard_alert'),
]
operations = [
migrations.AlterField(
model_name='check',
name='alert_severity',
field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], default='warning', max_length=15, null=True),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.1.4 on 2021-02-05 17:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('checks', '0018_auto_20210205_1647'),
]
operations = [
migrations.AddField(
model_name='check',
name='resolved_email_sent',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='check',
name='resolved_text_sent',
field=models.DateTimeField(blank=True, null=True),
),
]

View File

@@ -0,0 +1,29 @@
# Generated by Django 3.1.4 on 2021-02-10 15:12
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('checks', '0019_auto_20210205_1728'),
]
operations = [
migrations.RemoveField(
model_name='check',
name='email_sent',
),
migrations.RemoveField(
model_name='check',
name='resolved_email_sent',
),
migrations.RemoveField(
model_name='check',
name='resolved_text_sent',
),
migrations.RemoveField(
model_name='check',
name='text_sent',
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 3.1.4 on 2021-02-12 14:29
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('checks', '0020_auto_20210210_1512'),
]
operations = [
migrations.AlterField(
model_name='check',
name='error_threshold',
field=models.PositiveIntegerField(blank=True, default=0, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(99)]),
),
migrations.AlterField(
model_name='check',
name='warning_threshold',
field=models.PositiveIntegerField(blank=True, default=0, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(99)]),
),
]

View File

@@ -5,15 +5,29 @@ import json
import pytz import pytz
from statistics import mean from statistics import mean
from django.utils import timezone as djangotime
from django.db import models from django.db import models
from django.conf import settings from django.conf import settings
from django.contrib.postgres.fields import ArrayField
from django.core.validators import MinValueValidator, MaxValueValidator from django.core.validators import MinValueValidator, MaxValueValidator
from django.contrib.postgres.fields import ArrayField
from rest_framework.fields import JSONField
from typing import List, Any
from typing import Union
from loguru import logger
from core.models import CoreSettings from core.models import CoreSettings
from logs.models import BaseAuditModel from logs.models import BaseAuditModel
from .tasks import handle_check_email_alert_task, handle_check_sms_alert_task from .tasks import (
handle_check_email_alert_task,
handle_check_sms_alert_task,
handle_resolved_check_email_alert_task,
handle_resolved_check_sms_alert_task,
)
from .utils import bytes2human from .utils import bytes2human
from alerts.models import SEVERITY_CHOICES
logger.configure(**settings.LOG_CONFIG)
CHECK_TYPE_CHOICES = [ CHECK_TYPE_CHOICES = [
("diskspace", "Disk Space Check"), ("diskspace", "Disk Space Check"),
@@ -83,18 +97,34 @@ class Check(BaseAuditModel):
last_run = models.DateTimeField(null=True, blank=True) last_run = models.DateTimeField(null=True, blank=True)
email_alert = models.BooleanField(default=False) email_alert = models.BooleanField(default=False)
text_alert = models.BooleanField(default=False) text_alert = models.BooleanField(default=False)
dashboard_alert = models.BooleanField(default=False)
fails_b4_alert = models.PositiveIntegerField(default=1) fails_b4_alert = models.PositiveIntegerField(default=1)
fail_count = models.PositiveIntegerField(default=0) fail_count = models.PositiveIntegerField(default=0)
email_sent = models.DateTimeField(null=True, blank=True)
text_sent = models.DateTimeField(null=True, blank=True)
outage_history = models.JSONField(null=True, blank=True) # store outage_history = models.JSONField(null=True, blank=True) # store
extra_details = models.JSONField(null=True, blank=True) extra_details = models.JSONField(null=True, blank=True)
# check specific fields # check specific fields
# for eventlog, script, ip, and service alert severity
alert_severity = models.CharField(
max_length=15,
choices=SEVERITY_CHOICES,
default="warning",
null=True,
blank=True,
)
# threshold percent for diskspace, cpuload or memory check # threshold percent for diskspace, cpuload or memory check
threshold = models.PositiveIntegerField( error_threshold = models.PositiveIntegerField(
null=True, blank=True, validators=[MinValueValidator(1), MaxValueValidator(99)] validators=[MinValueValidator(0), MaxValueValidator(99)],
null=True,
blank=True,
default=0,
)
warning_threshold = models.PositiveIntegerField(
null=True,
blank=True,
validators=[MinValueValidator(0), MaxValueValidator(99)],
default=0,
) )
# diskcheck i.e C:, D: etc # diskcheck i.e C:, D: etc
disk = models.CharField(max_length=2, null=True, blank=True) disk = models.CharField(max_length=2, null=True, blank=True)
@@ -114,6 +144,18 @@ class Check(BaseAuditModel):
blank=True, blank=True,
default=list, default=list,
) )
info_return_codes = ArrayField(
models.PositiveIntegerField(),
null=True,
blank=True,
default=list,
)
warning_return_codes = ArrayField(
models.PositiveIntegerField(),
null=True,
blank=True,
default=list,
)
timeout = models.PositiveIntegerField(null=True, blank=True) timeout = models.PositiveIntegerField(null=True, blank=True)
stdout = models.TextField(null=True, blank=True) stdout = models.TextField(null=True, blank=True)
stderr = models.TextField(null=True, blank=True) stderr = models.TextField(null=True, blank=True)
@@ -158,11 +200,25 @@ class Check(BaseAuditModel):
@property @property
def readable_desc(self): def readable_desc(self):
if self.check_type == "diskspace": if self.check_type == "diskspace":
return f"{self.get_check_type_display()}: Drive {self.disk} < {self.threshold}%"
text = ""
if self.warning_threshold:
text += f" Warning Threshold: {self.warning_threshold}%"
if self.error_threshold:
text += f" Error Threshold: {self.error_threshold}%"
return f"{self.get_check_type_display()}: Drive {self.disk} < {text}"
elif self.check_type == "ping": elif self.check_type == "ping":
return f"{self.get_check_type_display()}: {self.name}" return f"{self.get_check_type_display()}: {self.name}"
elif self.check_type == "cpuload" or self.check_type == "memory": elif self.check_type == "cpuload" or self.check_type == "memory":
return f"{self.get_check_type_display()} > {self.threshold}%"
text = ""
if self.warning_threshold:
text += f" Warning Threshold: {self.warning_threshold}%"
if self.error_threshold:
text += f" Error Threshold: {self.error_threshold}%"
return f"{self.get_check_type_display()} > {text}"
elif self.check_type == "winsvc": elif self.check_type == "winsvc":
return f"{self.get_check_type_display()}: {self.svc_display_name}" return f"{self.get_check_type_display()}: {self.svc_display_name}"
elif self.check_type == "eventlog": elif self.check_type == "eventlog":
@@ -187,15 +243,13 @@ class Check(BaseAuditModel):
return self.last_run return self.last_run
@property @property
def non_editable_fields(self): def non_editable_fields(self) -> List[str]:
return [ return [
"check_type", "check_type",
"status", "status",
"more_info", "more_info",
"last_run", "last_run",
"fail_count", "fail_count",
"email_sent",
"text_sent",
"outage_history", "outage_history",
"extra_details", "extra_details",
"stdout", "stdout",
@@ -214,7 +268,148 @@ class Check(BaseAuditModel):
"modified_time", "modified_time",
] ]
def handle_alert(self) -> None:
from alerts.models import Alert, AlertTemplate
# return if agent is in maintenance mode
if self.agent.maintenance_mode:
return
# see if agent has an alert template and use that
alert_template: Union[AlertTemplate, None] = self.agent.get_alert_template()
# resolve alert if it exists
if self.status == "passing":
if Alert.objects.filter(assigned_check=self, resolved=False).exists():
alert = Alert.objects.get(assigned_check=self, resolved=False)
alert.resolve()
# check if a resolved email notification should be send
if (
alert_template
and alert_template.check_email_on_resolved
and not alert.resolved_email_sent
):
handle_resolved_check_email_alert_task.delay(pk=alert.pk)
# check if resolved text should be sent
if (
alert_template
and alert_template.check_text_on_resolved
and not alert.resolved_sms_sent
):
handle_resolved_check_sms_alert_task.delay(pk=alert.pk)
# check if resolved script should be run
if (
alert_template
and alert_template.resolved_action
and not alert.resolved_action_run
):
r = self.agent.run_script(
scriptpk=alert_template.resolved_action.pk,
args=alert_template.resolved_action_args,
timeout=alert_template.resolved_action_timeout,
wait=True,
full=True,
run_on_any=True,
)
# command was successful
if type(r) == dict:
alert.resolved_action_retcode = r["retcode"]
alert.resolved_action_stdout = r["stdout"]
alert.resolved_action_stderr = r["stderr"]
alert.resolved_action_execution_time = "{:.4f}".format(
r["execution_time"]
)
alert.resolved_action_run = djangotime.now()
alert.save()
else:
logger.error(
f"Resolved action: {alert_template.action.name} failed to run on any agent for {self.agent.hostname} resolved alert for {self.check_type} check"
)
elif self.fail_count >= self.fails_b4_alert:
if not Alert.objects.filter(assigned_check=self, resolved=False).exists():
alert = Alert.create_check_alert(self)
else:
alert = Alert.objects.get(assigned_check=self, resolved=False)
# check if alert severity changed on check and update the alert
if self.alert_severity != alert.severity:
alert.severity = self.alert_severity
alert.save(update_fields=["severity"])
# create alert in dashboard if enabled
if (
self.dashboard_alert
or alert_template
and self.alert_severity in alert_template.check_dashboard_alert_severity
and alert_template.check_always_alert
):
alert.hidden = False
alert.save()
# send email if enabled
if (
not alert.email_sent
and self.email_alert
or alert_template
and self.alert_severity in alert_template.check_email_alert_severity
and alert_template.check_always_email
):
handle_check_email_alert_task.delay(
pk=alert.pk,
alert_interval=alert_template.check_periodic_alert_days
if alert_template
else None,
)
# send text if enabled
if (
not alert.sms_sent
and self.text_alert
or alert_template
and self.alert_severity in alert_template.check_text_alert_severity
and alert_template.check_always_text
):
handle_check_sms_alert_task.delay(
pk=alert.pk,
alert_interval=alert_template.check_periodic_alert_days
if alert_template
else None,
)
# check if any scripts should be run
if alert_template and alert_template.action and not alert.action_run:
r = self.agent.run_script(
scriptpk=alert_template.action.pk,
args=alert_template.action_args,
timeout=alert_template.action_timeout,
wait=True,
full=True,
run_on_any=True,
)
# command was successful
if type(r) == dict:
alert.action_retcode = r["retcode"]
alert.action_stdout = r["stdout"]
alert.action_stderr = r["stderr"]
alert.action_execution_time = "{:.4f}".format(r["execution_time"])
alert.action_run = djangotime.now()
alert.save()
else:
logger.error(
f"Failure action: {alert_template.action.name} failed to run on any agent for {self.agent.hostname} failure alert for {self.check_type} check{r}"
)
def add_check_history(self, value: int, more_info: Any = None) -> None:
CheckHistory.objects.create(check_history=self, y=value, results=more_info)
def handle_checkv2(self, data): def handle_checkv2(self, data):
# cpuload or mem checks # cpuload or mem checks
if self.check_type == "cpuload" or self.check_type == "memory": if self.check_type == "cpuload" or self.check_type == "memory":
@@ -227,11 +422,18 @@ class Check(BaseAuditModel):
avg = int(mean(self.history)) avg = int(mean(self.history))
if avg > self.threshold: if self.error_threshold and avg > self.error_threshold:
self.status = "failing" self.status = "failing"
self.alert_severity = "error"
elif self.warning_threshold and avg > self.warning_threshold:
self.status = "failing"
self.alert_severity = "warning"
else: else:
self.status = "passing" self.status = "passing"
# add check history
self.add_check_history(data["percent"])
# diskspace checks # diskspace checks
elif self.check_type == "diskspace": elif self.check_type == "diskspace":
if data["exists"]: if data["exists"]:
@@ -239,14 +441,26 @@ class Check(BaseAuditModel):
total = bytes2human(data["total"]) total = bytes2human(data["total"])
free = bytes2human(data["free"]) free = bytes2human(data["free"])
if (100 - percent_used) < self.threshold: if self.error_threshold and (100 - percent_used) < self.error_threshold:
self.status = "failing" self.status = "failing"
self.alert_severity = "error"
elif (
self.warning_threshold
and (100 - percent_used) < self.warning_threshold
):
self.status = "failing"
self.alert_severity = "warning"
else: else:
self.status = "passing" self.status = "passing"
self.more_info = f"Total: {total}B, Free: {free}B" self.more_info = f"Total: {total}B, Free: {free}B"
# add check history
self.add_check_history(100 - percent_used)
else: else:
self.status = "failing" self.status = "failing"
self.alert_severity = "error"
self.more_info = f"Disk {self.disk} does not exist" self.more_info = f"Disk {self.disk} does not exist"
self.save(update_fields=["more_info"]) self.save(update_fields=["more_info"])
@@ -263,8 +477,15 @@ class Check(BaseAuditModel):
# golang agent # golang agent
self.execution_time = "{:.4f}".format(data["runtime"]) self.execution_time = "{:.4f}".format(data["runtime"])
if data["retcode"] != 0: if data["retcode"] in self.info_return_codes:
self.alert_severity = "info"
self.status = "failing" self.status = "failing"
elif data["retcode"] in self.warning_return_codes:
self.alert_severity = "warning"
self.status = "failing"
elif data["retcode"] != 0:
self.status = "failing"
self.alert_severity = "error"
else: else:
self.status = "passing" self.status = "passing"
@@ -277,6 +498,17 @@ class Check(BaseAuditModel):
] ]
) )
# add check history
self.add_check_history(
1 if self.status == "failing" else 0,
{
"retcode": data["retcode"],
"stdout": data["stdout"][:60],
"stderr": data["stderr"][:60],
"execution_time": self.execution_time,
},
)
# ping checks # ping checks
elif self.check_type == "ping": elif self.check_type == "ping":
success = ["Reply", "bytes", "time", "TTL"] success = ["Reply", "bytes", "time", "TTL"]
@@ -293,6 +525,10 @@ class Check(BaseAuditModel):
self.more_info = output self.more_info = output
self.save(update_fields=["more_info"]) self.save(update_fields=["more_info"])
self.add_check_history(
1 if self.status == "failing" else 0, self.more_info[:60]
)
# windows service checks # windows service checks
elif self.check_type == "winsvc": elif self.check_type == "winsvc":
svc_stat = data["status"] svc_stat = data["status"]
@@ -332,6 +568,10 @@ class Check(BaseAuditModel):
self.save(update_fields=["more_info"]) self.save(update_fields=["more_info"])
self.add_check_history(
1 if self.status == "failing" else 0, self.more_info[:60]
)
elif self.check_type == "eventlog": elif self.check_type == "eventlog":
log = [] log = []
is_wildcard = self.event_id_is_wildcard is_wildcard = self.event_id_is_wildcard
@@ -391,62 +631,24 @@ class Check(BaseAuditModel):
self.extra_details = {"log": log} self.extra_details = {"log": log}
self.save(update_fields=["extra_details"]) self.save(update_fields=["extra_details"])
self.add_check_history(
1 if self.status == "failing" else 0,
"Events Found:" + str(len(self.extra_details["log"])),
)
# handle status # handle status
if self.status == "failing": if self.status == "failing":
self.fail_count += 1 self.fail_count += 1
self.save(update_fields=["status", "fail_count"]) self.save(update_fields=["status", "fail_count", "alert_severity"])
elif self.status == "passing": elif self.status == "passing":
if self.fail_count != 0:
self.fail_count = 0 self.fail_count = 0
self.save(update_fields=["status", "fail_count"]) self.save(update_fields=["status", "fail_count", "alert_severity"])
else:
self.save(update_fields=["status"])
if self.fail_count >= self.fails_b4_alert: self.handle_alert()
if self.email_alert:
handle_check_email_alert_task.delay(self.pk)
if self.text_alert:
handle_check_sms_alert_task.delay(self.pk)
return self.status return self.status
def handle_check(self, data):
if self.check_type != "cpuload" and self.check_type != "memory":
if data["status"] == "passing" and self.fail_count != 0:
self.fail_count = 0
self.save(update_fields=["fail_count"])
elif data["status"] == "failing":
self.fail_count += 1
self.save(update_fields=["fail_count"])
else:
self.history.append(data["percent"])
if len(self.history) > 15:
self.history = self.history[-15:]
self.save(update_fields=["history"])
avg = int(mean(self.history))
if avg > self.threshold:
self.status = "failing"
self.fail_count += 1
self.save(update_fields=["status", "fail_count"])
else:
self.status = "passing"
if self.fail_count != 0:
self.fail_count = 0
self.save(update_fields=["status", "fail_count"])
else:
self.save(update_fields=["status"])
if self.email_alert and self.fail_count >= self.fails_b4_alert:
handle_check_email_alert_task.delay(self.pk)
@staticmethod @staticmethod
def serialize(check): def serialize(check):
# serializes the check and returns json # serializes the check and returns json
@@ -480,17 +682,22 @@ class Check(BaseAuditModel):
managed_by_policy=bool(agent), managed_by_policy=bool(agent),
parent_check=(self.pk if agent else None), parent_check=(self.pk if agent else None),
name=self.name, name=self.name,
alert_severity=self.alert_severity,
check_type=self.check_type, check_type=self.check_type,
email_alert=self.email_alert, email_alert=self.email_alert,
dashboard_alert=self.dashboard_alert,
text_alert=self.text_alert, text_alert=self.text_alert,
fails_b4_alert=self.fails_b4_alert, fails_b4_alert=self.fails_b4_alert,
extra_details=self.extra_details, extra_details=self.extra_details,
threshold=self.threshold, error_threshold=self.error_threshold,
warning_threshold=self.warning_threshold,
disk=self.disk, disk=self.disk,
ip=self.ip, ip=self.ip,
script=self.script, script=self.script,
script_args=self.script_args, script_args=self.script_args,
timeout=self.timeout, timeout=self.timeout,
info_return_codes=self.info_return_codes,
warning_return_codes=self.warning_return_codes,
svc_name=self.svc_name, svc_name=self.svc_name,
svc_display_name=self.svc_display_name, svc_display_name=self.svc_display_name,
pass_if_start_pending=self.pass_if_start_pending, pass_if_start_pending=self.pass_if_start_pending,
@@ -532,19 +739,27 @@ class Check(BaseAuditModel):
def send_email(self): def send_email(self):
CORE = CoreSettings.objects.first() CORE = CoreSettings.objects.first()
alert_template = self.agent.get_alert_template()
body: str = ""
if self.agent: if self.agent:
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Failed" subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Failed"
else: else:
subject = f"{self} Failed" subject = f"{self} Failed"
if self.check_type == "diskspace": if self.check_type == "diskspace":
text = ""
if self.warning_threshold:
text += f" Warning Threshold: {self.warning_threshold}%"
if self.error_threshold:
text += f" Error Threshold: {self.error_threshold}%"
percent_used = [ percent_used = [
d["percent"] for d in self.agent.disks if d["device"] == self.disk d["percent"] for d in self.agent.disks if d["device"] == self.disk
][0] ][0]
percent_free = 100 - percent_used percent_free = 100 - percent_used
body = subject + f" - Free: {percent_free}%, Threshold: {self.threshold}%" body = subject + f" - Free: {percent_free}%, {text}"
elif self.check_type == "script": elif self.check_type == "script":
@@ -558,26 +773,29 @@ class Check(BaseAuditModel):
body = self.more_info body = self.more_info
elif self.check_type == "cpuload" or self.check_type == "memory": elif self.check_type == "cpuload" or self.check_type == "memory":
text = ""
if self.warning_threshold:
text += f" Warning Threshold: {self.warning_threshold}%"
if self.error_threshold:
text += f" Error Threshold: {self.error_threshold}%"
avg = int(mean(self.history)) avg = int(mean(self.history))
if self.check_type == "cpuload": if self.check_type == "cpuload":
body = ( body = subject + f" - Average CPU utilization: {avg}%, {text}"
subject
+ f" - Average CPU utilization: {avg}%, Threshold: {self.threshold}%"
)
elif self.check_type == "memory": elif self.check_type == "memory":
body = ( body = subject + f" - Average memory usage: {avg}%, {text}"
subject
+ f" - Average memory usage: {avg}%, Threshold: {self.threshold}%"
)
elif self.check_type == "winsvc": elif self.check_type == "winsvc":
try:
status = list( status = list(
filter(lambda x: x["name"] == self.svc_name, self.agent.services) filter(lambda x: x["name"] == self.svc_name, self.agent.services)
)[0]["status"] )[0]["status"]
# catch services that don't exist if policy check
except:
status = "Unknown"
body = subject + f" - Status: {status.upper()}" body = subject + f" - Status: {status.upper()}"
@@ -603,11 +821,13 @@ class Check(BaseAuditModel):
except: except:
continue continue
CORE.send_mail(subject, body) CORE.send_mail(subject, body, alert_template=alert_template)
def send_sms(self): def send_sms(self):
CORE = CoreSettings.objects.first() CORE = CoreSettings.objects.first()
alert_template = self.agent.get_alert_template()
body: str = ""
if self.agent: if self.agent:
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Failed" subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Failed"
@@ -615,27 +835,33 @@ class Check(BaseAuditModel):
subject = f"{self} Failed" subject = f"{self} Failed"
if self.check_type == "diskspace": if self.check_type == "diskspace":
text = ""
if self.warning_threshold:
text += f" Warning Threshold: {self.warning_threshold}%"
if self.error_threshold:
text += f" Error Threshold: {self.error_threshold}%"
percent_used = [ percent_used = [
d["percent"] for d in self.agent.disks if d["device"] == self.disk d["percent"] for d in self.agent.disks if d["device"] == self.disk
][0] ][0]
percent_free = 100 - percent_used percent_free = 100 - percent_used
body = subject + f" - Free: {percent_free}%, Threshold: {self.threshold}%" body = subject + f" - Free: {percent_free}%, {text}"
elif self.check_type == "script": elif self.check_type == "script":
body = subject + f" - Return code: {self.retcode}" body = subject + f" - Return code: {self.retcode}"
elif self.check_type == "ping": elif self.check_type == "ping":
body = subject body = subject
elif self.check_type == "cpuload" or self.check_type == "memory": elif self.check_type == "cpuload" or self.check_type == "memory":
text = ""
if self.warning_threshold:
text += f" Warning Threshold: {self.warning_threshold}%"
if self.error_threshold:
text += f" Error Threshold: {self.error_threshold}%"
avg = int(mean(self.history)) avg = int(mean(self.history))
if self.check_type == "cpuload": if self.check_type == "cpuload":
body = ( body = subject + f" - Average CPU utilization: {avg}%, {text}"
subject
+ f" - Average CPU utilization: {avg}%, Threshold: {self.threshold}%"
)
elif self.check_type == "memory": elif self.check_type == "memory":
body = ( body = subject + f" - Average memory usage: {avg}%, {text}"
subject
+ f" - Average memory usage: {avg}%, Threshold: {self.threshold}%"
)
elif self.check_type == "winsvc": elif self.check_type == "winsvc":
status = list( status = list(
filter(lambda x: x["name"] == self.svc_name, self.agent.services) filter(lambda x: x["name"] == self.svc_name, self.agent.services)
@@ -644,4 +870,32 @@ class Check(BaseAuditModel):
elif self.check_type == "eventlog": elif self.check_type == "eventlog":
body = subject body = subject
CORE.send_sms(body) CORE.send_sms(body, alert_template=alert_template)
def send_resolved_email(self):
CORE = CoreSettings.objects.first()
alert_template = self.agent.get_alert_template()
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved"
body = f"{self} is now back to normal"
CORE.send_mail(subject, body, alert_template=alert_template)
def send_resolved_sms(self):
CORE = CoreSettings.objects.first()
alert_template = self.agent.get_alert_template()
subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved"
CORE.send_sms(subject, alert_template=alert_template)
class CheckHistory(models.Model):
check_history = models.ForeignKey(
Check,
related_name="check_history",
on_delete=models.CASCADE,
)
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

View File

@@ -1,8 +1,8 @@
import validators as _v import validators as _v
import pytz
from rest_framework import serializers from rest_framework import serializers
from .models import Check from .models import Check, CheckHistory
from autotasks.models import AutomatedTask from autotasks.models import AutomatedTask
from scripts.serializers import ScriptSerializer, ScriptCheckSerializer from scripts.serializers import ScriptSerializer, ScriptCheckSerializer
@@ -40,9 +40,11 @@ class CheckSerializer(serializers.ModelSerializer):
check_type = val["check_type"] check_type = val["check_type"]
except KeyError: except KeyError:
return val return val
# disk checks # disk checks
# make sure no duplicate diskchecks exist for an agent/policy # make sure no duplicate diskchecks exist for an agent/policy
if check_type == "diskspace" and not self.instance: # only on create if check_type == "diskspace":
if not self.instance: # only on create
checks = ( checks = (
Check.objects.filter(**self.context) Check.objects.filter(**self.context)
.filter(check_type="diskspace") .filter(check_type="diskspace")
@@ -54,6 +56,20 @@ class CheckSerializer(serializers.ModelSerializer):
f"A disk check for Drive {val['disk']} already exists!" f"A disk check for Drive {val['disk']} already exists!"
) )
if not val["warning_threshold"] and not val["error_threshold"]:
raise serializers.ValidationError(
f"Warning threshold or Error Threshold must be set"
)
if (
val["warning_threshold"] < val["error_threshold"]
and val["warning_threshold"] > 0
and val["error_threshold"] > 0
):
raise serializers.ValidationError(
f"Warning threshold must be greater than Error Threshold"
)
# ping checks # ping checks
if check_type == "ping": if check_type == "ping":
if ( if (
@@ -75,6 +91,20 @@ class CheckSerializer(serializers.ModelSerializer):
"A cpuload check for this agent already exists" "A cpuload check for this agent already exists"
) )
if not val["warning_threshold"] and not val["error_threshold"]:
raise serializers.ValidationError(
f"Warning threshold or Error Threshold must be set"
)
if (
val["warning_threshold"] > val["error_threshold"]
and val["warning_threshold"] > 0
and val["error_threshold"] > 0
):
raise serializers.ValidationError(
f"Warning threshold must be less than Error Threshold"
)
if check_type == "memory" and not self.instance: if check_type == "memory" and not self.instance:
if ( if (
Check.objects.filter(**self.context, check_type="memory") Check.objects.filter(**self.context, check_type="memory")
@@ -85,6 +115,20 @@ class CheckSerializer(serializers.ModelSerializer):
"A memory check for this agent already exists" "A memory check for this agent already exists"
) )
if not val["warning_threshold"] and not val["error_threshold"]:
raise serializers.ValidationError(
f"Warning threshold or Error Threshold must be set"
)
if (
val["warning_threshold"] > val["error_threshold"]
and val["warning_threshold"] > 0
and val["error_threshold"] > 0
):
raise serializers.ValidationError(
f"Warning threshold must be less than Error Threshold"
)
return val return val
@@ -95,101 +139,7 @@ class AssignedTaskCheckRunnerField(serializers.ModelSerializer):
class CheckRunnerGetSerializer(serializers.ModelSerializer): class CheckRunnerGetSerializer(serializers.ModelSerializer):
# for the windows agent
# only send data needed for agent to run a check # only send data needed for agent to run a check
assigned_task = serializers.SerializerMethodField()
script = ScriptSerializer(read_only=True)
def get_assigned_task(self, obj):
if obj.assignedtask.exists():
# this will not break agents on version 0.10.2 or lower
# newer agents once released will properly handle multiple tasks assigned to a check
task = obj.assignedtask.first()
return AssignedTaskCheckRunnerField(task).data
class Meta:
model = Check
exclude = [
"policy",
"managed_by_policy",
"overriden_by_policy",
"parent_check",
"name",
"more_info",
"last_run",
"email_alert",
"text_alert",
"fails_b4_alert",
"fail_count",
"email_sent",
"text_sent",
"outage_history",
"extra_details",
"stdout",
"stderr",
"retcode",
"execution_time",
"svc_display_name",
"svc_policy_mode",
"created_by",
"created_time",
"modified_by",
"modified_time",
"history",
]
class CheckRunnerGetSerializerV2(serializers.ModelSerializer):
# for the windows __python__ agent
# only send data needed for agent to run a check
assigned_tasks = serializers.SerializerMethodField()
script = ScriptSerializer(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 = [
"policy",
"managed_by_policy",
"overriden_by_policy",
"parent_check",
"name",
"more_info",
"last_run",
"email_alert",
"text_alert",
"fails_b4_alert",
"fail_count",
"email_sent",
"text_sent",
"outage_history",
"extra_details",
"stdout",
"stderr",
"retcode",
"execution_time",
"svc_display_name",
"svc_policy_mode",
"created_by",
"created_time",
"modified_by",
"modified_time",
"history",
]
class CheckRunnerGetSerializerV3(serializers.ModelSerializer):
# for the windows __golang__ agent
# only send data needed for agent to run a check
# the difference here is in the script serializer
# script checks no longer rely on salt and are executed directly by the go agent
assigned_tasks = serializers.SerializerMethodField() assigned_tasks = serializers.SerializerMethodField()
script = ScriptCheckSerializer(read_only=True) script = ScriptCheckSerializer(read_only=True)
@@ -212,8 +162,6 @@ class CheckRunnerGetSerializerV3(serializers.ModelSerializer):
"text_alert", "text_alert",
"fails_b4_alert", "fails_b4_alert",
"fail_count", "fail_count",
"email_sent",
"text_sent",
"outage_history", "outage_history",
"extra_details", "extra_details",
"stdout", "stdout",
@@ -237,3 +185,15 @@ class CheckResultsSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Check model = Check
fields = "__all__" fields = "__all__"
class CheckHistorySerializer(serializers.ModelSerializer):
x = serializers.SerializerMethodField()
def get_x(self, obj):
return obj.x.astimezone(pytz.timezone(self.context["timezone"])).isoformat()
# used for return large amounts of graph data
class Meta:
model = CheckHistory
fields = ("x", "y", "results")

View File

@@ -1,58 +1,101 @@
import datetime as dt import datetime as dt
import random import random
from time import sleep from time import sleep
from typing import Union
from tacticalrmm.celery import app from tacticalrmm.celery import app
from django.utils import timezone as djangotime from django.utils import timezone as djangotime
from agents.models import Agent
@app.task @app.task
def handle_check_email_alert_task(pk): def handle_check_email_alert_task(pk, alert_interval: Union[float, None] = None) -> str:
from .models import Check from alerts.models import Alert
check = Check.objects.get(pk=pk) alert = Alert.objects.get(pk=pk)
if not check.agent.maintenance_mode:
# first time sending email # first time sending email
if not check.email_sent: if not alert.email_sent:
sleep(random.randint(1, 10)) sleep(random.randint(1, 10))
check.send_email() alert.assigned_check.send_email()
check.email_sent = djangotime.now() alert.email_sent = djangotime.now()
check.save(update_fields=["email_sent"]) alert.save(update_fields=["email_sent"])
else: else:
# send an email only if the last email sent is older than 24 hours if alert_interval:
delta = djangotime.now() - dt.timedelta(hours=24) # send an email only if the last email sent is older than alert interval
if check.email_sent < delta: delta = djangotime.now() - dt.timedelta(days=alert_interval)
if alert.email_sent < delta:
sleep(random.randint(1, 10)) sleep(random.randint(1, 10))
check.send_email() alert.assigned_check.send_email()
check.email_sent = djangotime.now() alert.email_sent = djangotime.now()
check.save(update_fields=["email_sent"]) alert.save(update_fields=["email_sent"])
return "ok" return "ok"
@app.task @app.task
def handle_check_sms_alert_task(pk): def handle_check_sms_alert_task(pk, alert_interval: Union[float, None] = None) -> str:
from .models import Check from alerts.models import Alert
check = Check.objects.get(pk=pk) alert = Alert.objects.get(pk=pk)
if not check.agent.maintenance_mode:
# first time sending text # first time sending text
if not check.text_sent: if not alert.sms_sent:
sleep(random.randint(1, 3)) sleep(random.randint(1, 3))
check.send_sms() alert.assigned_check.send_sms()
check.text_sent = djangotime.now() alert.sms_sent = djangotime.now()
check.save(update_fields=["text_sent"]) alert.save(update_fields=["sms_sent"])
else: else:
if alert_interval:
# send a text only if the last text sent is older than 24 hours # send a text only if the last text sent is older than 24 hours
delta = djangotime.now() - dt.timedelta(hours=24) delta = djangotime.now() - dt.timedelta(days=alert_interval)
if check.text_sent < delta: if alert.sms_sent < delta:
sleep(random.randint(1, 3)) sleep(random.randint(1, 3))
check.send_sms() alert.assigned_check.send_sms()
check.text_sent = djangotime.now() alert.sms_sent = djangotime.now()
check.save(update_fields=["text_sent"]) alert.save(update_fields=["sms_sent"])
return "ok"
@app.task
def handle_resolved_check_sms_alert_task(pk: int) -> str:
from alerts.models import Alert
alert = Alert.objects.get(pk=pk)
# first time sending text
if not alert.resolved_sms_sent:
sleep(random.randint(1, 3))
alert.assigned_check.send_resolved_sms()
alert.resolved_sms_sent = djangotime.now()
alert.save(update_fields=["resolved_sms_sent"])
return "ok"
@app.task
def handle_resolved_check_email_alert_task(pk: int) -> str:
from alerts.models import Alert
alert = Alert.objects.get(pk=pk)
# first time sending email
if not alert.resolved_email_sent:
sleep(random.randint(1, 10))
alert.assigned_check.send_resolved_email()
alert.resolved_email_sent = djangotime.now()
alert.save(update_fields=["resolved_email_sent"])
return "ok"
@app.task
def prune_check_history(older_than_days: int) -> str:
from .models import CheckHistory
CheckHistory.objects.filter(
x__lt=djangotime.make_aware(dt.datetime.today())
- djangotime.timedelta(days=older_than_days)
).delete()
return "ok" return "ok"

View File

@@ -1,13 +1,16 @@
from checks.models import CheckHistory
from tacticalrmm.test import TacticalTestCase from tacticalrmm.test import TacticalTestCase
from .serializers import CheckSerializer from .serializers import CheckSerializer
from django.utils import timezone as djangotime
from unittest.mock import patch
from model_bakery import baker from model_bakery import baker
from itertools import cycle
class TestCheckViews(TacticalTestCase): class TestCheckViews(TacticalTestCase):
def setUp(self): def setUp(self):
self.authenticate() self.authenticate()
self.setup_coresettings()
def test_get_disk_check(self): def test_get_disk_check(self):
# setup data # setup data
@@ -20,7 +23,7 @@ class TestCheckViews(TacticalTestCase):
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data) self.assertEqual(resp.data, serializer.data)
self.check_not_authenticated("post", url) self.check_not_authenticated("get", url)
def test_add_disk_check(self): def test_add_disk_check(self):
# setup data # setup data
@@ -33,7 +36,8 @@ class TestCheckViews(TacticalTestCase):
"check": { "check": {
"check_type": "diskspace", "check_type": "diskspace",
"disk": "C:", "disk": "C:",
"threshold": 55, "error_threshold": 55,
"warning_threshold": 0,
"fails_b4_alert": 3, "fails_b4_alert": 3,
}, },
} }
@@ -47,7 +51,8 @@ class TestCheckViews(TacticalTestCase):
"check": { "check": {
"check_type": "diskspace", "check_type": "diskspace",
"disk": "C:", "disk": "C:",
"threshold": 55, "error_threshold": 55,
"warning_threshold": 0,
"fails_b4_alert": 3, "fails_b4_alert": 3,
}, },
} }
@@ -55,6 +60,38 @@ class TestCheckViews(TacticalTestCase):
resp = self.client.post(url, invalid_payload, format="json") resp = self.client.post(url, invalid_payload, format="json")
self.assertEqual(resp.status_code, 400) self.assertEqual(resp.status_code, 400)
# this should fail because both error and warning threshold are 0
invalid_payload = {
"pk": agent.pk,
"check": {
"check_type": "diskspace",
"disk": "C:",
"error_threshold": 0,
"warning_threshold": 0,
"fails_b4_alert": 3,
},
}
resp = self.client.post(url, invalid_payload, format="json")
self.assertEqual(resp.status_code, 400)
# this should fail because both error is greater than warning threshold
invalid_payload = {
"pk": agent.pk,
"check": {
"check_type": "diskspace",
"disk": "C:",
"error_threshold": 50,
"warning_threshold": 30,
"fails_b4_alert": 3,
},
}
resp = self.client.post(url, invalid_payload, format="json")
self.assertEqual(resp.status_code, 400)
self.check_not_authenticated("post", url)
def test_add_cpuload_check(self): def test_add_cpuload_check(self):
url = "/checks/checks/" url = "/checks/checks/"
agent = baker.make_recipe("agents.agent") agent = baker.make_recipe("agents.agent")
@@ -62,7 +99,8 @@ class TestCheckViews(TacticalTestCase):
"pk": agent.pk, "pk": agent.pk,
"check": { "check": {
"check_type": "cpuload", "check_type": "cpuload",
"threshold": 66, "error_threshold": 66,
"warning_threshold": 0,
"fails_b4_alert": 9, "fails_b4_alert": 9,
}, },
} }
@@ -70,7 +108,7 @@ class TestCheckViews(TacticalTestCase):
resp = self.client.post(url, payload, format="json") resp = self.client.post(url, payload, format="json")
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
payload["threshold"] = 87 payload["error_threshold"] = 87
resp = self.client.post(url, payload, format="json") resp = self.client.post(url, payload, format="json")
self.assertEqual(resp.status_code, 400) self.assertEqual(resp.status_code, 400)
self.assertEqual( self.assertEqual(
@@ -78,6 +116,36 @@ class TestCheckViews(TacticalTestCase):
"A cpuload check for this agent already exists", "A cpuload check for this agent already exists",
) )
# should fail because both error and warning thresholds are 0
invalid_payload = {
"pk": agent.pk,
"check": {
"check_type": "cpuload",
"error_threshold": 0,
"warning_threshold": 0,
"fails_b4_alert": 9,
},
}
resp = self.client.post(url, invalid_payload, format="json")
self.assertEqual(resp.status_code, 400)
# should fail because error is less than warning
invalid_payload = {
"pk": agent.pk,
"check": {
"check_type": "cpuload",
"error_threshold": 10,
"warning_threshold": 50,
"fails_b4_alert": 9,
},
}
resp = self.client.post(url, invalid_payload, format="json")
self.assertEqual(resp.status_code, 400)
self.check_not_authenticated("post", url)
def test_add_memory_check(self): def test_add_memory_check(self):
url = "/checks/checks/" url = "/checks/checks/"
agent = baker.make_recipe("agents.agent") agent = baker.make_recipe("agents.agent")
@@ -85,7 +153,8 @@ class TestCheckViews(TacticalTestCase):
"pk": agent.pk, "pk": agent.pk,
"check": { "check": {
"check_type": "memory", "check_type": "memory",
"threshold": 78, "error_threshold": 78,
"warning_threshold": 0,
"fails_b4_alert": 1, "fails_b4_alert": 1,
}, },
} }
@@ -93,7 +162,7 @@ class TestCheckViews(TacticalTestCase):
resp = self.client.post(url, payload, format="json") resp = self.client.post(url, payload, format="json")
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
payload["threshold"] = 55 payload["error_threshold"] = 55
resp = self.client.post(url, payload, format="json") resp = self.client.post(url, payload, format="json")
self.assertEqual(resp.status_code, 400) self.assertEqual(resp.status_code, 400)
self.assertEqual( self.assertEqual(
@@ -101,6 +170,34 @@ class TestCheckViews(TacticalTestCase):
"A memory check for this agent already exists", "A memory check for this agent already exists",
) )
# should fail because both error and warning thresholds are 0
invalid_payload = {
"pk": agent.pk,
"check": {
"check_type": "memory",
"error_threshold": 0,
"warning_threshold": 0,
"fails_b4_alert": 9,
},
}
resp = self.client.post(url, invalid_payload, format="json")
self.assertEqual(resp.status_code, 400)
# should fail because error is less than warning
invalid_payload = {
"pk": agent.pk,
"check": {
"check_type": "memory",
"error_threshold": 10,
"warning_threshold": 50,
"fails_b4_alert": 9,
},
}
resp = self.client.post(url, invalid_payload, format="json")
self.assertEqual(resp.status_code, 400)
def test_get_policy_disk_check(self): def test_get_policy_disk_check(self):
# setup data # setup data
policy = baker.make("automation.Policy") policy = baker.make("automation.Policy")
@@ -126,11 +223,37 @@ class TestCheckViews(TacticalTestCase):
"check": { "check": {
"check_type": "diskspace", "check_type": "diskspace",
"disk": "M:", "disk": "M:",
"threshold": 86, "error_threshold": 86,
"warning_threshold": 0,
"fails_b4_alert": 2, "fails_b4_alert": 2,
}, },
} }
# should fail because both error and warning thresholds are 0
invalid_payload = {
"policy": policy.pk,
"check": {
"check_type": "diskspace",
"error_threshold": 0,
"warning_threshold": 0,
"fails_b4_alert": 9,
},
}
resp = self.client.post(url, invalid_payload, format="json")
self.assertEqual(resp.status_code, 400)
# should fail because warning is less than error
invalid_payload = {
"policy": policy.pk,
"check": {
"check_type": "diskspace",
"error_threshold": 80,
"warning_threshold": 50,
"fails_b4_alert": 9,
},
}
resp = self.client.post(url, valid_payload, format="json") resp = self.client.post(url, valid_payload, format="json")
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
@@ -140,7 +263,8 @@ class TestCheckViews(TacticalTestCase):
"check": { "check": {
"check_type": "diskspace", "check_type": "diskspace",
"disk": "M:", "disk": "M:",
"threshold": 34, "error_threshold": 34,
"warning_threshold": 0,
"fails_b4_alert": 9, "fails_b4_alert": 9,
}, },
} }
@@ -180,3 +304,111 @@ class TestCheckViews(TacticalTestCase):
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
self.check_not_authenticated("patch", url_a) self.check_not_authenticated("patch", url_a)
@patch("agents.models.Agent.nats_cmd")
def test_run_checks(self, nats_cmd):
agent = baker.make_recipe("agents.agent", version="1.4.1")
agent_old = baker.make_recipe("agents.agent", version="1.0.2")
agent_b4_141 = baker.make_recipe("agents.agent", version="1.4.0")
url = f"/checks/runchecks/{agent_old.pk}/"
r = self.client.get(url)
self.assertEqual(r.status_code, 400)
self.assertEqual(r.json(), "Requires agent version 1.1.0 or greater")
url = f"/checks/runchecks/{agent_b4_141.pk}/"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
nats_cmd.assert_called_with({"func": "runchecks"}, wait=False)
nats_cmd.reset_mock()
nats_cmd.return_value = "busy"
url = f"/checks/runchecks/{agent.pk}/"
r = self.client.get(url)
self.assertEqual(r.status_code, 400)
nats_cmd.assert_called_with({"func": "runchecks"}, timeout=15)
self.assertEqual(r.json(), f"Checks are already running on {agent.hostname}")
nats_cmd.reset_mock()
nats_cmd.return_value = "ok"
url = f"/checks/runchecks/{agent.pk}/"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
nats_cmd.assert_called_with({"func": "runchecks"}, timeout=15)
self.assertEqual(r.json(), f"Checks will now be re-run on {agent.hostname}")
nats_cmd.reset_mock()
nats_cmd.return_value = "timeout"
url = f"/checks/runchecks/{agent.pk}/"
r = self.client.get(url)
self.assertEqual(r.status_code, 400)
nats_cmd.assert_called_with({"func": "runchecks"}, timeout=15)
self.assertEqual(r.json(), "Unable to contact the agent")
self.check_not_authenticated("get", url)
def test_get_check_history(self):
# 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)
check_history_data = baker.make(
"checks.CheckHistory",
check_history=check,
_quantity=30,
)
# need to manually set the date back 35 days
for check_history in check_history_data:
check_history.x = djangotime.now() - djangotime.timedelta(days=35)
check_history.save()
# test invalid check pk
resp = self.client.patch("/checks/history/500/", format="json")
self.assertEqual(resp.status_code, 404)
url = f"/checks/history/{check.id}/"
# test with timeFilter last 30 days
data = {"timeFilter": 30}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
self.assertEqual(len(resp.data), 30)
# test with timeFilter equal to 0
data = {"timeFilter": 0}
resp = self.client.patch(url, data, format="json")
self.assertEqual(resp.status_code, 200)
self.assertEqual(len(resp.data), 60)
self.check_not_authenticated("patch", url)
class TestCheckTasks(TacticalTestCase):
def setUp(self):
self.setup_coresettings()
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)
check_history_data = baker.make(
"checks.CheckHistory",
check_history=check,
_quantity=30,
)
# need to manually set the date back 35 days
for check_history in check_history_data:
check_history.x = djangotime.now() - djangotime.timedelta(days=35)
check_history.save()
# prune data 30 days old
prune_check_history(30)
self.assertEqual(CheckHistory.objects.count(), 30)
# prune all Check history Data
prune_check_history(0)
self.assertEqual(CheckHistory.objects.count(), 0)

View File

@@ -7,4 +7,5 @@ urlpatterns = [
path("<pk>/loadchecks/", views.load_checks), path("<pk>/loadchecks/", views.load_checks),
path("getalldisks/", views.get_disks_for_policies), path("getalldisks/", views.get_disks_for_policies),
path("runchecks/<pk>/", views.run_checks), path("runchecks/<pk>/", views.run_checks),
path("history/<int:checkpk>/", views.CheckHistory.as_view()),
] ]

View File

@@ -1,6 +1,11 @@
import asyncio import asyncio
from packaging import version as pyver
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.db.models import Q
from django.utils import timezone as djangotime
from datetime import datetime as dt
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.response import Response from rest_framework.response import Response
@@ -13,7 +18,7 @@ from automation.models import Policy
from .models import Check from .models import Check
from scripts.models import Script from scripts.models import Script
from .serializers import CheckSerializer from .serializers import CheckSerializer, CheckHistorySerializer
from automation.tasks import ( from automation.tasks import (
@@ -135,14 +140,46 @@ class GetUpdateDeleteCheck(APIView):
return Response(f"{check.readable_desc} was deleted!") return Response(f"{check.readable_desc} was deleted!")
class CheckHistory(APIView):
def patch(self, request, checkpk):
check = get_object_or_404(Check, pk=checkpk)
timeFilter = Q()
if "timeFilter" in request.data:
if request.data["timeFilter"] != 0:
timeFilter = Q(
x__lte=djangotime.make_aware(dt.today()),
x__gt=djangotime.make_aware(dt.today())
- djangotime.timedelta(days=request.data["timeFilter"]),
)
check_history = check.check_history.filter(timeFilter).order_by("-x")
return Response(
CheckHistorySerializer(
check_history, context={"timezone": check.agent.timezone}, many=True
).data
)
@api_view() @api_view()
def run_checks(request, pk): def run_checks(request, pk):
agent = get_object_or_404(Agent, pk=pk) agent = get_object_or_404(Agent, pk=pk)
if not agent.has_nats: if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater") return notify_error("Requires agent version 1.1.0 or greater")
if pyver.parse(agent.version) >= pyver.parse("1.4.1"):
r = asyncio.run(agent.nats_cmd({"func": "runchecks"}, timeout=15))
if r == "busy":
return notify_error(f"Checks are already running on {agent.hostname}")
elif r == "ok":
return Response(f"Checks will now be re-run on {agent.hostname}")
else:
return notify_error("Unable to contact the agent")
else:
asyncio.run(agent.nats_cmd({"func": "runchecks"}, wait=False)) asyncio.run(agent.nats_cmd({"func": "runchecks"}, wait=False))
return Response(agent.hostname) return Response(f"Checks will now be re-run on {agent.hostname}")
@api_view() @api_view()

View File

@@ -0,0 +1,25 @@
# Generated by Django 3.1.4 on 2021-02-12 14:08
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('alerts', '0004_auto_20210212_1408'),
('clients', '0008_auto_20201103_1430'),
]
operations = [
migrations.AddField(
model_name='client',
name='alert_template',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='clients', to='alerts.alerttemplate'),
),
migrations.AddField(
model_name='site',
name='alert_template',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sites', to='alerts.alerttemplate'),
),
]

View File

@@ -23,6 +23,36 @@ class Client(BaseAuditModel):
blank=True, blank=True,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
) )
alert_template = models.ForeignKey(
"alerts.AlertTemplate",
related_name="clients",
on_delete=models.SET_NULL,
null=True,
blank=True,
)
def save(self, *args, **kw):
from automation.tasks import generate_agent_checks_by_location_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 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,
)
class Meta: class Meta:
ordering = ("name",) ordering = ("name",)
@@ -45,6 +75,7 @@ class Client(BaseAuditModel):
"overdue_text_alert", "overdue_text_alert",
"last_seen", "last_seen",
"overdue_time", "overdue_time",
"offline_time",
) )
.filter(site__client=self) .filter(site__client=self)
.prefetch_related("agentchecks") .prefetch_related("agentchecks")
@@ -87,6 +118,36 @@ class Site(BaseAuditModel):
blank=True, blank=True,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
) )
alert_template = models.ForeignKey(
"alerts.AlertTemplate",
related_name="sites",
on_delete=models.SET_NULL,
null=True,
blank=True,
)
def save(self, *args, **kw):
from automation.tasks import generate_agent_checks_by_location_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 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,
)
class Meta: class Meta:
ordering = ("name",) ordering = ("name",)
@@ -107,6 +168,7 @@ class Site(BaseAuditModel):
"overdue_text_alert", "overdue_text_alert",
"last_seen", "last_seen",
"overdue_time", "overdue_time",
"offline_time",
) )
.filter(site=self) .filter(site=self)
.prefetch_related("agentchecks") .prefetch_related("agentchecks")

View File

@@ -10,7 +10,7 @@ class SiteSerializer(ModelSerializer):
fields = "__all__" fields = "__all__"
def validate(self, val): def validate(self, val):
if "|" in val["name"]: if "name" in val.keys() and "|" in val["name"]:
raise ValidationError("Site name cannot contain the | character") raise ValidationError("Site name cannot contain the | character")
if self.context: if self.context:
@@ -36,7 +36,7 @@ class ClientSerializer(ModelSerializer):
if len(self.context["site"]) > 255: if len(self.context["site"]) > 255:
raise ValidationError("Site name too long") raise ValidationError("Site name too long")
if "|" in val["name"]: if "name" in val.keys() and "|" in val["name"]:
raise ValidationError("Client name cannot contain the | character") raise ValidationError("Client name cannot contain the | character")
return val return val

View File

@@ -61,7 +61,8 @@ class GetAddClients(APIView):
class GetUpdateDeleteClient(APIView): class GetUpdateDeleteClient(APIView):
def put(self, request, pk): def put(self, request, pk):
client = get_object_or_404(Client, pk=pk) client = get_object_or_404(Client, pk=pk)
serializer = ClientSerializer(data=request.data, instance=client)
serializer = ClientSerializer(data=request.data, instance=client, partial=True)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
serializer.save() serializer.save()
@@ -106,7 +107,7 @@ class GetUpdateDeleteSite(APIView):
def put(self, request, pk): def put(self, request, pk):
site = get_object_or_404(Site, pk=pk) site = get_object_or_404(Site, pk=pk)
serializer = SiteSerializer(instance=site, data=request.data) serializer = SiteSerializer(instance=site, data=request.data, partial=True)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
serializer.save() serializer.save()
@@ -192,7 +193,7 @@ class GenerateAgent(APIView):
if not os.path.exists(go_bin): if not os.path.exists(go_bin):
return notify_error("Missing golang") return notify_error("Missing golang")
api = f"{request.scheme}://{request.get_host()}" api = f"https://{request.get_host()}"
inno = ( inno = (
f"winagent-v{settings.LATEST_AGENT_VER}.exe" f"winagent-v{settings.LATEST_AGENT_VER}.exe"
if d.arch == "64" if d.arch == "64"
@@ -223,7 +224,7 @@ class GenerateAgent(APIView):
f"GOARCH={goarch}", f"GOARCH={goarch}",
go_bin, go_bin,
"build", "build",
f"-ldflags=\"-X 'main.Inno={inno}'", f"-ldflags=\"-s -w -X 'main.Inno={inno}'",
f"-X 'main.Api={api}'", f"-X 'main.Api={api}'",
f"-X 'main.Client={d.client.pk}'", f"-X 'main.Client={d.client.pk}'",
f"-X 'main.Site={d.site.pk}'", f"-X 'main.Site={d.site.pk}'",

View File

@@ -57,7 +57,6 @@ func main() {
debugLog := flag.String("log", "", "Verbose output") debugLog := flag.String("log", "", "Verbose output")
localMesh := flag.String("local-mesh", "", "Use local mesh agent") localMesh := flag.String("local-mesh", "", "Use local mesh agent")
noSalt := flag.Bool("nosalt", false, "Does not install salt")
silent := flag.Bool("silent", false, "Do not popup any message boxes during installation") silent := flag.Bool("silent", false, "Do not popup any message boxes during installation")
cert := flag.String("cert", "", "Path to ca.pem") cert := flag.String("cert", "", "Path to ca.pem")
timeout := flag.String("timeout", "", "Timeout for subprocess calls") timeout := flag.String("timeout", "", "Timeout for subprocess calls")
@@ -86,10 +85,6 @@ func main() {
cmdArgs = append(cmdArgs, "-silent") cmdArgs = append(cmdArgs, "-silent")
} }
if *noSalt {
cmdArgs = append(cmdArgs, "-nosalt")
}
if len(strings.TrimSpace(*localMesh)) != 0 { if len(strings.TrimSpace(*localMesh)) != 0 {
cmdArgs = append(cmdArgs, "-local-mesh", *localMesh) cmdArgs = append(cmdArgs, "-local-mesh", *localMesh)
} }
@@ -138,7 +133,7 @@ func main() {
os.Exit(1) os.Exit(1)
} }
time.Sleep(10 * time.Second) time.Sleep(5 * time.Second)
fmt.Println("Installation starting.") fmt.Println("Installation starting.")
cmd := exec.Command(tacrmm, cmdArgs...) cmd := exec.Command(tacrmm, cmdArgs...)

View File

@@ -36,7 +36,7 @@ If (Get-Service $serviceName -ErrorAction SilentlyContinue) {
Invoke-WebRequest -Uri $downloadlink -OutFile $OutPath\$output Invoke-WebRequest -Uri $downloadlink -OutFile $OutPath\$output
Start-Process -FilePath $OutPath\$output -ArgumentList ('/VERYSILENT /SUPPRESSMSGBOXES') -Wait Start-Process -FilePath $OutPath\$output -ArgumentList ('/VERYSILENT /SUPPRESSMSGBOXES') -Wait
write-host ('Extracting...') write-host ('Extracting...')
Start-Sleep -s 10 Start-Sleep -s 5
Start-Process -FilePath "C:\Program Files\TacticalAgent\tacticalrmm.exe" -ArgumentList $installArgs -Wait Start-Process -FilePath "C:\Program Files\TacticalAgent\tacticalrmm.exe" -ArgumentList $installArgs -Wait
exit 0 exit 0
} }

View File

@@ -16,7 +16,7 @@ class Command(BaseCommand):
# 10-16-2020 changed the type of the agent's 'disks' model field # 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 # 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 # the following will convert dicts to lists for agent's still on the python agent
agents = Agent.objects.all() agents = Agent.objects.only("pk", "disks")
for agent in agents: for agent in agents:
if agent.disks is not None and isinstance(agent.disks, dict): if agent.disks is not None and isinstance(agent.disks, dict):
new = [] new = []

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.4 on 2021-01-10 18:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0011_auto_20201026_0719"),
]
operations = [
migrations.AddField(
model_name="coresettings",
name="check_history_prune_days",
field=models.PositiveIntegerField(default=30),
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 3.1.4 on 2021-02-12 14:08
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('alerts', '0004_auto_20210212_1408'),
('core', '0012_coresettings_check_history_prune_days'),
]
operations = [
migrations.AddField(
model_name='coresettings',
name='alert_template',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_alert_template', to='alerts.alerttemplate'),
),
]

View File

@@ -49,6 +49,8 @@ class CoreSettings(BaseAuditModel):
default_time_zone = models.CharField( default_time_zone = models.CharField(
max_length=255, choices=TZ_CHOICES, default="America/Los_Angeles" max_length=255, choices=TZ_CHOICES, default="America/Los_Angeles"
) )
# removes check history older than days
check_history_prune_days = models.PositiveIntegerField(default=30)
mesh_token = models.CharField(max_length=255, null=True, blank=True, default="") 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_username = models.CharField(max_length=255, null=True, blank=True, default="")
mesh_site = models.CharField(max_length=255, null=True, blank=True, default="") mesh_site = models.CharField(max_length=255, null=True, blank=True, default="")
@@ -67,8 +69,17 @@ class CoreSettings(BaseAuditModel):
blank=True, blank=True,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
) )
alert_template = models.ForeignKey(
"alerts.AlertTemplate",
related_name="default_alert_template",
on_delete=models.SET_NULL,
null=True,
blank=True,
)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
from automation.tasks import generate_all_agent_checks_task
if not self.pk and CoreSettings.objects.exists(): if not self.pk and CoreSettings.objects.exists():
raise ValidationError("There can only be one CoreSettings instance") raise ValidationError("There can only be one CoreSettings instance")
@@ -81,7 +92,18 @@ class CoreSettings(BaseAuditModel):
except: except:
pass pass
return super(CoreSettings, self).save(*args, **kwargs) old_settings = type(self).objects.get(pk=self.pk) if self.pk else None
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
)
def __str__(self): def __str__(self):
return "Global Site Settings" return "Global Site Settings"
@@ -122,18 +144,30 @@ class CoreSettings(BaseAuditModel):
return False return False
def send_mail(self, subject, body, test=False): def send_mail(self, subject, body, alert_template=None, test=False):
if not self.email_is_configured: if not alert_template and not self.email_is_configured:
if test: if test:
return "Missing required fields (need at least 1 recipient)" return "Missing required fields (need at least 1 recipient)"
return False return False
# override email from if alert_template is passed and is set
if alert_template and alert_template.email_from:
from_address = alert_template.email_from
else:
from_address = self.smtp_from_email
# override email recipients if alert_template is passed and is set
if alert_template and alert_template.email_recipients:
email_recipients = ", ".join(alert_template.email_recipients)
else:
email_recipients = ", ".join(self.email_alert_recipients)
try: try:
msg = EmailMessage() msg = EmailMessage()
msg["Subject"] = subject msg["Subject"] = subject
msg["From"] = self.smtp_from_email msg["From"] = from_address
msg["To"] = ", ".join(self.email_alert_recipients) msg["To"] = email_recipients
msg.set_content(body) msg.set_content(body)
with smtplib.SMTP(self.smtp_host, self.smtp_port, timeout=20) as server: with smtplib.SMTP(self.smtp_host, self.smtp_port, timeout=20) as server:
@@ -155,12 +189,18 @@ class CoreSettings(BaseAuditModel):
else: else:
return True return True
def send_sms(self, body): def send_sms(self, body, alert_template=None):
if not self.sms_is_configured: if not alert_template and not self.sms_is_configured:
return return
# override email recipients if alert_template is passed and is set
if alert_template and alert_template.text_recipients:
text_recipients = alert_template.email_recipients
else:
text_recipients = self.sms_alert_recipients
tw_client = TwClient(self.twilio_account_sid, self.twilio_auth_token) tw_client = TwClient(self.twilio_account_sid, self.twilio_auth_token)
for num in self.sms_alert_recipients: for num in text_recipients:
try: try:
tw_client.messages.create(body=body, to=num, from_=self.twilio_number) tw_client.messages.create(body=body, to=num, from_=self.twilio_number)
except Exception as e: except Exception as e:

View File

@@ -4,8 +4,10 @@ from loguru import logger
from django.conf import settings from django.conf import settings
from django.utils import timezone as djangotime from django.utils import timezone as djangotime
from tacticalrmm.celery import app from tacticalrmm.celery import app
from core.models import CoreSettings
from autotasks.models import AutomatedTask from autotasks.models import AutomatedTask
from autotasks.tasks import delete_win_task_schedule from autotasks.tasks import delete_win_task_schedule
from checks.tasks import prune_check_history
logger.configure(**settings.LOG_CONFIG) logger.configure(**settings.LOG_CONFIG)
@@ -25,3 +27,7 @@ def core_maintenance_tasks():
if now > task_time_utc: if now > task_time_utc:
delete_win_task_schedule.delay(task.pk) delete_win_task_schedule.delay(task.pk)
# remove old CheckHistory data
older_than = CoreSettings.objects.first().check_history_prune_days
prune_check_history.delay(older_than)

View File

@@ -83,8 +83,9 @@ class TestCoreTasks(TacticalTestCase):
self.check_not_authenticated("patch", url) self.check_not_authenticated("patch", url)
@patch("tacticalrmm.utils.reload_nats")
@patch("autotasks.tasks.remove_orphaned_win_tasks.delay") @patch("autotasks.tasks.remove_orphaned_win_tasks.delay")
def test_ui_maintenance_actions(self, remove_orphaned_win_tasks): def test_ui_maintenance_actions(self, remove_orphaned_win_tasks, reload_nats):
url = "/core/servermaintenance/" url = "/core/servermaintenance/"
agents = baker.make_recipe("agents.online_agent", _quantity=3) agents = baker.make_recipe("agents.online_agent", _quantity=3)
@@ -103,6 +104,7 @@ class TestCoreTasks(TacticalTestCase):
data = {"action": "reload_nats"} data = {"action": "reload_nats"}
r = self.client.post(url, data) r = self.client.post(url, data)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
reload_nats.assert_called_once()
# test prune db with no tables # test prune db with no tables
data = {"action": "prune_db"} data = {"action": "prune_db"}

View File

@@ -43,22 +43,9 @@ def get_core_settings(request):
@api_view(["PATCH"]) @api_view(["PATCH"])
def edit_settings(request): def edit_settings(request):
coresettings = CoreSettings.objects.first() coresettings = CoreSettings.objects.first()
old_server_policy = coresettings.server_policy
old_workstation_policy = coresettings.workstation_policy
serializer = CoreSettingsSerializer(instance=coresettings, data=request.data) serializer = CoreSettingsSerializer(instance=coresettings, data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
new_settings = serializer.save() serializer.save()
# check if default policies changed
if old_server_policy != new_settings.server_policy:
generate_all_agent_checks_task.delay(
mon_type="server", clear=True, create_tasks=True
)
if old_workstation_policy != new_settings.workstation_policy:
generate_all_agent_checks_task.delay(
mon_type="workstation", clear=True, create_tasks=True
)
return Response("ok") return Response("ok")
@@ -75,6 +62,8 @@ def dashboard_info(request):
"trmm_version": settings.TRMM_VERSION, "trmm_version": settings.TRMM_VERSION,
"dark_mode": request.user.dark_mode, "dark_mode": request.user.dark_mode,
"show_community_scripts": request.user.show_community_scripts, "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,
} }
) )
@@ -107,7 +96,7 @@ def server_maintenance(request):
from agents.models import Agent from agents.models import Agent
from autotasks.tasks import remove_orphaned_win_tasks from autotasks.tasks import remove_orphaned_win_tasks
agents = Agent.objects.all() agents = Agent.objects.only("pk", "last_seen", "overdue_time", "offline_time")
online = [i for i in agents if i.status == "online"] online = [i for i in agents if i.status == "online"]
for agent in online: for agent in online:
remove_orphaned_win_tasks.delay(agent.pk) remove_orphaned_win_tasks.delay(agent.pk)
@@ -117,7 +106,6 @@ def server_maintenance(request):
) )
if request.data["action"] == "prune_db": if request.data["action"] == "prune_db":
from agents.models import AgentOutage
from logs.models import AuditLog, PendingAction from logs.models import AuditLog, PendingAction
if "prune_tables" not in request.data: if "prune_tables" not in request.data:
@@ -125,11 +113,6 @@ def server_maintenance(request):
tables = request.data["prune_tables"] tables = request.data["prune_tables"]
records_count = 0 records_count = 0
if "agent_outages" in tables:
agentoutages = AgentOutage.objects.exclude(recovery_time=None)
records_count += agentoutages.count()
agentoutages.delete()
if "audit_logs" in tables: if "audit_logs" in tables:
auditlogs = AuditLog.objects.filter(action="check_run") auditlogs = AuditLog.objects.filter(action="check_run")
records_count += auditlogs.count() records_count += auditlogs.count()

View File

@@ -1,4 +1,3 @@
from logs.models import AuditLog
from model_bakery.recipe import Recipe from model_bakery.recipe import Recipe
from itertools import cycle from itertools import cycle
@@ -19,10 +18,10 @@ object_actions = ["add", "modify", "view", "delete"]
agent_actions = ["remote_session", "execute_script", "execute_command"] agent_actions = ["remote_session", "execute_script", "execute_command"]
login_actions = ["failed_login", "login"] login_actions = ["failed_login", "login"]
agent_logs = Recipe(AuditLog, action=cycle(agent_actions), object_type="agent") agent_logs = Recipe("logs.AuditLog", action=cycle(agent_actions), object_type="agent")
object_logs = Recipe( object_logs = Recipe(
AuditLog, action=cycle(object_actions), object_type=cycle(object_types) "logs.AuditLog", action=cycle(object_actions), object_type=cycle(object_types)
) )
login_logs = Recipe(AuditLog, action=cycle(login_actions), object_type="user") login_logs = Recipe("logs.AuditLog", action=cycle(login_actions), object_type="user")

View File

@@ -147,8 +147,8 @@ class TestAuditViews(TacticalTestCase):
def test_options_filter(self): def test_options_filter(self):
url = "/logs/auditlogs/optionsfilter/" url = "/logs/auditlogs/optionsfilter/"
baker.make("agents.Agent", hostname=seq("AgentHostname"), _quantity=5) baker.make_recipe("agents.agent", hostname=seq("AgentHostname"), _quantity=5)
baker.make("agents.Agent", hostname=seq("Server"), _quantity=3) baker.make_recipe("agents.agent", hostname=seq("Server"), _quantity=3)
baker.make("accounts.User", username=seq("Username"), _quantity=7) baker.make("accounts.User", username=seq("Username"), _quantity=7)
baker.make("accounts.User", username=seq("soemthing"), _quantity=3) baker.make("accounts.User", username=seq("soemthing"), _quantity=3)
@@ -194,7 +194,8 @@ class TestAuditViews(TacticalTestCase):
def test_all_pending_actions(self): def test_all_pending_actions(self):
url = "/logs/allpendingactions/" url = "/logs/allpendingactions/"
pending_actions = baker.make("logs.PendingAction", _quantity=6) agent = baker.make_recipe("agents.agent")
pending_actions = baker.make("logs.PendingAction", agent=agent, _quantity=6)
resp = self.client.get(url, format="json") resp = self.client.get(url, format="json")
serializer = PendingActionSerializer(pending_actions, many=True) serializer = PendingActionSerializer(pending_actions, many=True)

View File

@@ -140,7 +140,7 @@ def cancel_pending_action(request):
def debug_log(request, mode, hostname, order): def debug_log(request, mode, hostname, order):
log_file = settings.LOG_CONFIG["handlers"][0]["sink"] log_file = settings.LOG_CONFIG["handlers"][0]["sink"]
agents = Agent.objects.all() agents = Agent.objects.prefetch_related("site").only("pk", "hostname")
agent_hostnames = AgentHostnameSerializer(agents, many=True) agent_hostnames = AgentHostnameSerializer(agents, many=True)
switch_mode = { switch_mode = {

View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class NatsapiConfig(AppConfig):
name = "natsapi"

View File

@@ -0,0 +1,59 @@
from model_bakery import baker
from tacticalrmm.test import TacticalTestCase
from django.conf import settings
class TestNatsAPIViews(TacticalTestCase):
def setUp(self):
self.authenticate()
self.setup_coresettings()
def test_nats_wmi(self):
url = "/natsapi/wmi/"
baker.make_recipe("agents.online_agent", version="1.2.0", _quantity=14)
baker.make_recipe(
"agents.online_agent", version=settings.LATEST_AGENT_VER, _quantity=3
)
baker.make_recipe(
"agents.overdue_agent", version=settings.LATEST_AGENT_VER, _quantity=5
)
baker.make_recipe("agents.online_agent", version="1.1.12", _quantity=7)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(len(r.json()["agent_ids"]), 17)
def test_natscheckin_patch(self):
from logs.models import PendingAction
url = "/natsapi/checkin/"
agent_updated = baker.make_recipe("agents.agent", version="1.3.0")
PendingAction.objects.create(
agent=agent_updated,
action_type="agentupdate",
details={
"url": agent_updated.winagent_dl,
"version": agent_updated.version,
"inno": agent_updated.win_inno_exe,
},
)
action = agent_updated.pendingactions.filter(action_type="agentupdate").first()
self.assertEqual(action.status, "pending")
# test agent failed to update and still on same version
payload = {
"func": "hello",
"agent_id": agent_updated.agent_id,
"version": "1.3.0",
}
r = self.client.patch(url, payload, format="json")
self.assertEqual(r.status_code, 200)
action = agent_updated.pendingactions.filter(action_type="agentupdate").first()
self.assertEqual(action.status, "pending")
# test agent successful update
payload["version"] = settings.LATEST_AGENT_VER
r = self.client.patch(url, payload, format="json")
self.assertEqual(r.status_code, 200)
action = agent_updated.pendingactions.filter(action_type="agentupdate").first()
self.assertEqual(action.status, "completed")
action.delete()

View File

@@ -0,0 +1,14 @@
from django.urls import path
from . import views
urlpatterns = [
path("natsinfo/", views.nats_info),
path("checkin/", views.NatsCheckIn.as_view()),
path("syncmesh/", views.SyncMeshNodeID.as_view()),
path("winupdates/", views.NatsWinUpdates.as_view()),
path("choco/", views.NatsChoco.as_view()),
path("wmi/", views.NatsWMI.as_view()),
path("offline/", views.OfflineAgents.as_view()),
path("logcrash/", views.LogCrash.as_view()),
path("superseded/", views.SupersededWinUpdate.as_view()),
]

Some files were not shown because too many files have changed in this diff Show More