Compare commits

...

1054 Commits

Author SHA1 Message Date
wh1te909
41aa84f403 back to dev [skip ci] 2025-02-04 18:02:57 +00:00
wh1te909
0669a126ed bump version 2025-02-04 17:52:01 +00:00
wh1te909
d5fc77e70a update jinja 2025-02-03 21:59:30 +00:00
wh1te909
079c987c44 bump webver 2025-02-03 21:58:02 +00:00
wh1te909
e4fb4ac28a update reqs 2025-02-03 05:14:39 +00:00
wh1te909
10fd07577f fix for celery workers stop consuming tasks when connection to redis lost 2025-02-01 20:51:59 +00:00
wh1te909
83b4d8c686 fix tests 2025-01-21 21:02:59 +00:00
wh1te909
0a2547d65c fix cors error 2025-01-21 18:52:35 +00:00
wh1te909
5ee2a3cb54 update readme 2024-11-28 19:56:19 +00:00
wh1te909
e505d0768c add server maintenance mode mgmt command 2024-11-22 19:14:55 +00:00
wh1te909
6d4fe84ddc back to dev [skip ci] 2024-11-22 00:41:46 +00:00
wh1te909
c6b667f8b3 bump version [skip ci] 2024-11-21 16:57:37 +00:00
wh1te909
ad4cddb4f3 bump web ver [skip ci] 2024-11-20 19:50:21 +00:00
wh1te909
ddba83b993 Merge pull request #2001 from sadnub/sso
feat: single sign-on #508
2024-11-20 11:17:52 -08:00
wh1te909
91c33b0431 add setting override to disable sso 2024-11-16 19:28:28 +00:00
wh1te909
d1df40633a call sync mesh after sso user created 2024-11-16 19:10:17 +00:00
wh1te909
7f9fc484e8 revert as these haven't changed [skip ci] 2024-11-15 20:40:16 +00:00
wh1te909
ecf564648e update reqs 2024-11-15 20:25:54 +00:00
wh1te909
150e3190bc refurb 2024-11-15 20:19:00 +00:00
wh1te909
63947346e9 remove deprecated login endpoints 2024-11-15 20:18:41 +00:00
wh1te909
86816ce357 move name stuff to the correct view and add email fallback 2024-11-10 20:59:27 +00:00
wh1te909
0d34831df4 also check if first name only and display 2024-11-06 20:32:28 +00:00
wh1te909
c35da67401 update reqs 2024-11-05 20:26:50 +00:00
wh1te909
fb47022380 redo migrations 2024-11-04 23:40:59 +00:00
wh1te909
46c5128418 move callback url info to the backend 2024-11-04 21:58:37 +00:00
wh1te909
4a5bfee616 fix failsafe to ensure no lockouts and add self-reset sso perms 2024-11-04 20:28:01 +00:00
wh1te909
f8314e0f8e fix pop 2024-11-04 18:57:09 +00:00
wh1te909
9624af4e67 fix tests 2024-11-03 08:47:40 +00:00
wh1te909
5bec4768e7 forgot frontend 2024-11-03 06:22:33 +00:00
wh1te909
3851b0943a modify settings instead of local_settings 2024-11-03 06:17:04 +00:00
wh1te909
cc1f640a50 set icon based on provider 2024-11-01 17:42:51 +00:00
wh1te909
ec0a2dc053 handle deployment config updates 2024-10-31 19:06:39 +00:00
wh1te909
a6166a1ad7 add random otp to social accounts 2024-10-31 01:25:20 +00:00
wh1te909
41e3d1f490 move check to signup 2024-10-30 23:07:14 +00:00
wh1te909
2cbecaa552 don't show providers list on login screen if sso is disabled globally 2024-10-30 05:13:35 +00:00
wh1te909
8d543dcc7d move inside if block 2024-10-29 20:03:10 +00:00
sadnub
18b1afe34f formatting 2024-10-29 11:40:05 -04:00
sadnub
0f86bbfad8 disable password/mfa reset views if block_local_logon is enabled 2024-10-29 11:29:04 -04:00
wh1te909
0d021a800a use exists 2024-10-29 11:29:04 -04:00
wh1te909
038304384a move sso settings 2024-10-29 11:29:04 -04:00
wh1te909
2c09ad6b91 update headers 2024-10-29 11:29:04 -04:00
wh1te909
0bd09d03c1 fix tests 2024-10-29 11:29:04 -04:00
wh1te909
faa0e6c289 handle orphaned sso providers 2024-10-29 11:29:04 -04:00
wh1te909
c28d800d7f blacked 2024-10-29 11:29:04 -04:00
wh1te909
4fd772ecd8 update reqs 2024-10-29 11:29:04 -04:00
sadnub
5520a84062 fix client ip not showing in audit log for sso logon and disable some unused urls and settings 2024-10-29 11:29:04 -04:00
sadnub
66c7123f7c allow displaying full name in UI if present 2024-10-29 11:29:03 -04:00
sadnub
bacf4154fd fix some 500 errors 2024-10-29 11:29:03 -04:00
wh1te909
61790d2261 blacked 2024-10-29 11:29:03 -04:00
wh1te909
899111a310 remove unused imports 2024-10-29 11:29:03 -04:00
wh1te909
3bfa35e1c7 move settings before local import 2024-10-29 11:29:03 -04:00
wh1te909
ebefcb7fc1 block local should be disabled by default 2024-10-29 11:29:03 -04:00
sadnub
ce11685371 secure sso token a little more and allow for disabling sso feature. 2024-10-29 11:29:03 -04:00
sadnub
9edb848947 implement default role for sso signups and log ip for sso logins 2024-10-29 11:29:03 -04:00
wh1te909
f326096fad isort 2024-10-29 11:29:03 -04:00
wh1te909
46f0b23f4f rename to avoid conflict with django settings 2024-10-29 11:29:03 -04:00
wh1te909
1c1d3bd619 frontend needs to come first 2024-10-29 11:29:03 -04:00
wh1te909
d894f92d5e format 2024-10-29 11:29:03 -04:00
wh1te909
6c44191fe4 blacked 2024-10-29 11:29:03 -04:00
wh1te909
0deb78a9af fix settings 2024-10-29 11:29:03 -04:00
sadnub
9c15f4ba88 implemented user session tracking, social account tracking, and blocking local user logon 2024-10-29 11:29:03 -04:00
sadnub
4ba27ec1d6 add auditing and session key checking to the sso auth token view 2024-10-29 11:29:03 -04:00
sadnub
c8dd80530a fix session auth and restrict it only to access_token view 2024-10-29 11:29:03 -04:00
sadnub
eda5ea7d1a sso init 2024-10-29 11:29:03 -04:00
wh1te909
77a916e1a8 don't force env vars fixes #2048 2024-10-29 04:31:24 +00:00
wh1te909
7ba2a4b27b update chocos 2024-10-24 09:23:16 +00:00
wh1te909
d33f69720a back to dev [skip ci] 2024-10-23 17:31:55 +00:00
wh1te909
e5c355e8f9 bump version 2024-10-23 17:23:00 +00:00
wh1te909
d36fadf3ca update wording 2024-10-23 17:22:48 +00:00
wh1te909
b618cbdf7c update reqs 2024-10-23 00:56:57 +00:00
wh1te909
15ec7173aa bump web vers 2024-10-23 00:56:46 +00:00
wh1te909
4166e92754 don't trim script whitespace 2024-10-18 06:20:13 +00:00
wh1te909
85166b6e8b add run on server option to run script endpoint #1923 2024-10-17 20:27:15 +00:00
wh1te909
5278599675 update nats 2024-10-17 20:26:04 +00:00
wh1te909
18cac8ba5d show more detail in checks tab #2014 2024-10-15 08:31:06 +00:00
wh1te909
dfccbceea6 bump mesh 2024-10-15 08:28:38 +00:00
wh1te909
fc4b651e46 change to match standard install 2024-10-15 08:25:22 +00:00
wh1te909
fb89922ecf format 2024-10-15 08:24:07 +00:00
wh1te909
8ab23c8cd9 update reqs 2024-10-13 19:51:43 +00:00
wh1te909
787a2c5071 add separate perms for global keystore #1984 2024-10-06 05:58:15 +00:00
wh1te909
da76a20345 forgot to add migration 2024-10-06 03:06:31 +00:00
wh1te909
9688dbdb36 add saving output of bulk script to custom field and agent note closes #1845 2024-10-06 01:49:27 +00:00
wh1te909
6fa16e1a5e update req 2024-10-05 20:25:40 +00:00
wh1te909
71a2e3cfca remove extra mgmt cmd 2024-09-30 19:27:14 +00:00
wh1te909
e9c0f7e200 update reqs 2024-09-30 08:20:22 +00:00
wh1te909
25154a4331 update nats 2024-09-30 07:21:32 +00:00
wh1te909
22c152f600 update reqs 2024-09-04 09:32:37 +00:00
Dan
3eab61cbc3 Merge pull request #1980 from cdp1337/community-scripts-245
Proposed work for amidaware/community-scripts#245
2024-08-20 12:49:17 -07:00
wh1te909
a029c1d0db set alert template when moving site to another client fixes #1975 2024-08-15 18:40:19 +00:00
Charlie Powell
706757d215 Black didn't like the format of that line
whatever, quick fix.
2024-08-15 00:57:04 -04:00
Charlie Powell
9054c233f4 Proposed work for amidaware/community-scripts#245
Modify the load_community_scripts logic to add
env and run_as_user keys.
2024-08-15 00:41:04 -04:00
wh1te909
751b0ef716 bump versions 2024-08-05 17:49:11 +00:00
wh1te909
716450b97e add check for turnkey 2024-08-04 00:30:29 +00:00
wh1te909
2c289a4d8f fix regex 2024-08-01 05:38:16 +00:00
wh1te909
a4ad4c033f also remove control chars 2024-07-30 21:24:43 +00:00
wh1te909
511bca9d66 preserve newlines and tabs 2024-07-30 21:17:07 +00:00
wh1te909
ac3fb03b2d add client and site name to script email closes #1945 2024-07-30 09:10:48 +00:00
wh1te909
282087d0f3 fix custom field view perms fixes #1941 2024-07-30 09:03:45 +00:00
wh1te909
781282599c more webhook json fixes 2024-07-29 22:08:39 +00:00
wh1te909
d611ab0ee2 log body and headers 2024-07-28 22:54:22 +00:00
Dan
411cbdffee Merge pull request #1940 from bc24fl/develop
Allow docker installs the ability to disable web terminal or server side scripts via .env file
2024-07-27 12:39:36 -07:00
bc24fl
cfd19e02a7 Update .env.example 2024-07-27 12:33:20 -04:00
bc24fl
717eeb3903 Update docker-compose.yml 2024-07-27 12:29:50 -04:00
bc24fl
a394fb8757 Update .env.example 2024-07-27 12:29:08 -04:00
bc24fl
2125a7ffdb Update entrypoint.sh 2024-07-27 11:21:45 -04:00
bc24fl
00c0a6ec60 Enable docker installs to disable web terminal and/or server scripts 2024-07-26 19:08:40 -04:00
wh1te909
090bcf89ac potential fix for webhook failures 2024-07-26 19:14:53 +00:00
wh1te909
c8d72ddd3b bump version [skip ci] 2024-07-22 19:40:35 +00:00
wh1te909
5cf618695f fix lint 2024-07-22 19:31:21 +00:00
wh1te909
8a1f497265 fix alert actions not honoring 'run only on' settings and fix availability webhook invalid escape 2024-07-22 19:19:58 +00:00
wh1te909
acdf20f800 add webhook to readme [skip ci] 2024-07-18 18:26:43 +00:00
wh1te909
dbd1003002 back to dev 2024-07-18 18:26:09 +00:00
wh1te909
41ccd14f25 bump version [skip ci] 2024-07-18 05:59:09 +00:00
wh1te909
60800df798 fix resolved emails not being sent 2024-07-18 00:52:34 +00:00
wh1te909
9c36f2cbc5 trigger policy refresh on more fields 2024-07-17 21:19:28 +00:00
wh1te909
0b4fff907a back to dev [skip ci] 2024-07-13 00:28:27 +00:00
wh1te909
50af28b2aa bump versions 2024-07-12 18:53:05 +00:00
wh1te909
28ad74a68e fix lint 2024-07-09 22:58:58 +00:00
wh1te909
13cdbae38f disable unused websocket endpoint 2024-07-09 22:54:58 +00:00
wh1te909
55c77df5ae update reqs 2024-07-09 18:22:07 +00:00
wh1te909
9b1d2fd985 bump web ver [skip ci] 2024-07-08 21:10:16 +00:00
wh1te909
91b7ea0367 more reqs updates 2024-07-08 20:35:38 +00:00
wh1te909
96d3926d09 update bins 2024-07-08 20:34:57 +00:00
wh1te909
c709b5a7eb update nats-api reqs 2024-07-08 20:33:55 +00:00
wh1te909
df82914005 make sure server scripts start with shebang 2024-07-08 19:00:44 +00:00
wh1te909
b1bdc38283 update reqs 2024-07-08 19:00:23 +00:00
sadnub
beb1215329 stop sending resolved message if the alert severity isn't configured to do so 2024-07-08 00:21:12 -04:00
wh1te909
51784388b9 add global option for handling info/warning notifications closes #1834 2024-07-05 21:18:14 +00:00
wh1te909
dbbbd53a4d wording 2024-07-05 21:16:27 +00:00
wh1te909
f9d992c969 add error handling 2024-07-02 16:52:05 +00:00
wh1te909
29a4d61e90 fix auditing/perms for webhook testing 2024-07-02 00:17:32 +00:00
wh1te909
2667cdb26c lower workers on smaller instances 2024-07-01 19:05:55 +00:00
wh1te909
a1669a5104 disabled in hosted 2024-07-01 18:45:54 +00:00
wh1te909
059f1bd63d bump test vers [skip ci] 2024-06-28 23:09:44 +00:00
wh1te909
82ae5e442c use homedir as cwd 2024-06-28 21:50:55 +00:00
Dan
b10114cd7c Merge pull request #1823 from sadnub/urlaction-rework
Serverside actions and cli
2024-06-28 13:30:08 -07:00
wh1te909
33f730aac4 redo migrations 2024-06-28 20:23:24 +00:00
wh1te909
92fdfdb05c delete migrations 2024-06-28 20:21:13 +00:00
wh1te909
fbaf3f3623 update reqs 2024-06-28 18:57:31 +00:00
wh1te909
5f400bc513 fix auditing 2024-06-28 17:13:12 +00:00
wh1te909
0fc59645fc alerts should still be created even if no notifications are selected 2024-06-28 17:13:12 +00:00
wh1te909
e2dee272b8 update pylance settings 2024-06-28 17:13:12 +00:00
wh1te909
364cf362f4 blacked 2024-06-28 17:13:12 +00:00
sadnub
8394a263c4 add auditing to new views 2024-06-28 17:13:12 +00:00
wh1te909
0e9aa26cfc enforce server script perms when handling alert templates 2024-06-28 17:13:12 +00:00
wh1te909
6a23d63266 use constant 2024-06-28 17:13:12 +00:00
wh1te909
af2fc15964 update reqs 2024-06-28 17:13:12 +00:00
wh1te909
5919037a4a fix deprecation warning 2024-06-28 17:13:12 +00:00
wh1te909
a761dab229 simplify query and add logging 2024-06-28 17:13:12 +00:00
wh1te909
fa656e1f56 add missing returns 2024-06-28 17:13:12 +00:00
wh1te909
77e141e84a return error if disabled 2024-06-28 17:13:12 +00:00
wh1te909
2439965fa8 disable web terminal by default 2024-06-28 17:13:12 +00:00
wh1te909
f66afbee90 make default method post and move imports 2024-06-28 17:13:12 +00:00
wh1te909
5a89d23a67 make description textfield 2024-06-28 17:13:12 +00:00
wh1te909
07c8dad1c3 redo migrations 2024-06-28 17:13:12 +00:00
wh1te909
beb8b18e98 remove migrations 2024-06-28 17:13:12 +00:00
wh1te909
887bb5d7cc rename model fields 2024-06-28 17:13:12 +00:00
wh1te909
4a9542d970 still need the old login views for frontend transition 2024-06-28 17:13:12 +00:00
wh1te909
c049d9d5ff alerts should not be created if agent in maintenance mode fixes #1849 2024-06-28 17:13:12 +00:00
wh1te909
c2cc4389a0 add test 2024-06-28 17:13:12 +00:00
wh1te909
12b5011266 fix tests 2024-06-28 17:13:12 +00:00
wh1te909
6e3cad454c add error handling for server script 2024-06-28 17:13:12 +00:00
sadnub
8251bd028c add error handling to webhook test function 2024-06-28 17:13:12 +00:00
sadnub
da87d452c2 fix tests 2024-06-28 17:13:12 +00:00
wh1te909
9bca0dfb3c fix action/resolved name if webhook 2024-06-28 17:13:12 +00:00
wh1te909
57904c4a97 also disable in demo 2024-06-28 17:13:12 +00:00
wh1te909
4e74d851e9 add test server script and start making server scripts/webterm optional 2024-06-28 17:13:12 +00:00
wh1te909
e5c1f69b02 use sigkill instead of sigterm 2024-06-28 17:13:12 +00:00
sadnub
9d390d064c flake 2024-06-28 17:13:12 +00:00
sadnub
4994d7892c black 2024-06-28 17:13:12 +00:00
sadnub
1ea06e3c42 fixes some tests for auth, fixes the recursive property lookup, fixes the replacement of alert variables 2024-06-28 17:13:12 +00:00
sadnub
a4b7a6dfc7 code formatting 2024-06-28 17:13:12 +00:00
sadnub
7fe1cce606 remove some unused imports 2024-06-28 17:13:12 +00:00
sadnub
7e5abe32e0 remove more server task stuff 2024-06-28 17:13:12 +00:00
wh1te909
47caf7c142 blacked 2024-06-28 17:13:12 +00:00
sadnub
cf4d777344 remove run_server_task command 2024-06-28 17:13:12 +00:00
sadnub
255927c346 remove autotasks rework 2024-06-28 17:13:12 +00:00
sadnub
e8c5fc79a6 added check to make sure instance_type == 'none' doesn't trigger a Model lookup and added json.dumps on body 2024-06-28 17:13:12 +00:00
sadnub
b309b24d0b Fix string replacement function and fix flaw in regex to match {{model.prop}} tags 2024-06-28 17:13:12 +00:00
sadnub
13f4cca9d5 allow strings in instance id for Agent hostname 2024-06-28 17:13:12 +00:00
sadnub
b3c0273e0c cleanup model resolution and potential fix for nested object and array properties in requets body 2024-06-28 17:13:12 +00:00
wh1te909
1df7fdf703 fix request body and url 2024-06-28 17:13:12 +00:00
wh1te909
cbf38309e2 blacked 2024-06-28 17:13:12 +00:00
sadnub
2ec7257dd7 add view for web hook test and add recursion to the dictionary data replacer 2024-06-28 17:13:12 +00:00
wh1te909
531aac6923 harden connect method 2024-06-28 17:13:12 +00:00
wh1te909
59b4604c77 wrong role name 2024-06-28 17:13:12 +00:00
sadnub
52aa269af9 modify totp setup view 2024-06-28 17:13:12 +00:00
wh1te909
8a03d9c498 set term 2024-06-28 17:13:12 +00:00
wh1te909
a36fc7ecfd fix webhooks 2024-06-28 17:13:12 +00:00
sadnub
7b0c269bce fix flake 2024-06-28 17:13:12 +00:00
sadnub
c10bf9b357 black 2024-06-28 17:13:12 +00:00
sadnub
0606642953 fix failure action not saving correctly if a server script 2024-06-28 17:13:12 +00:00
sadnub
d1b2cae201 add migrations 2024-06-28 17:13:12 +00:00
sadnub
097e567122 init 2024-06-28 17:13:12 +00:00
wh1te909
d22e1d6a24 update nats-server 2024-06-28 17:12:11 +00:00
wh1te909
2827069bd9 handle expired nginx signing key 2024-06-25 16:06:26 +00:00
Dan
614e3bd2a0 Merge pull request #1903 from silversword411/develop
troubleshoot_server.sh - Checking for resolvconf and giving helper text
2024-06-24 13:48:01 -07:00
silversword411
ff756a01d2 Added version tracking header info 2024-06-24 16:31:56 -04:00
silversword411
db14606dbe troubleshoot_server: Add helper for resolvconf error 2024-06-24 16:24:14 -04:00
wh1te909
5bf5065d9a replace expired nginx key 2024-06-24 05:51:36 +00:00
wh1te909
0235dadbf7 fix alert template not assigned on new agent fixes #1896 2024-06-19 04:23:44 +00:00
wh1te909
203a15b447 cleanup pid file on start 2024-06-11 00:31:53 +00:00
wh1te909
fe4dfe2194 update reqs 2024-06-08 08:33:21 +00:00
wh1te909
c2eb93abe0 switch to localhost to download mesh exe 2024-06-08 08:07:05 +00:00
wh1te909
d32b834ae7 fix snippet bug fixes #1702 2024-05-29 06:29:00 +00:00
Dan
cecf45a698 Merge pull request #1824 from dinger1986/dinger1986-added-passwordless-sudo-verify
added passwordless sudo verify for backup scheduling
2024-05-18 22:28:22 -07:00
Dan
69cd348cc3 Merge branch 'develop' into dinger1986-added-passwordless-sudo-verify 2024-05-18 22:23:59 -07:00
wh1te909
868025ffa3 update reqs 2024-05-16 19:40:22 +00:00
wh1te909
60126a8cc5 update reqs 2024-05-07 02:30:06 +00:00
wh1te909
8cfba49559 add noninteractive 2024-04-25 22:03:22 +00:00
wh1te909
168f053c6f revert, already fixed in #1823 2024-04-22 18:12:02 +00:00
wh1te909
897e1d4539 fix script name fixes #1852 2024-04-22 17:32:05 +00:00
wh1te909
5ef6a0f4ea update reqs 2024-04-19 21:21:13 +00:00
wh1te909
eb80e32812 no-owner for pg_dump 2024-04-19 20:36:11 +00:00
wh1te909
620dadafe4 back to dev [skip ci] 2024-04-09 03:14:10 +00:00
wh1te909
376b421eb9 bump versions 2024-04-09 00:37:07 +00:00
wh1te909
e1643aca80 revert DRF for now until we do more testing 2024-04-08 23:35:48 +00:00
wh1te909
4e97c0c5c9 add note about where to find bulk output results 2024-04-08 23:27:30 +00:00
dinger1986
2d51b122af Update backup.sh 2024-04-02 16:59:48 +01:00
wh1te909
05b88a3c73 fix for usernames with spaces in them fixes #1820 2024-03-30 22:08:15 +00:00
wh1te909
3c087d49e9 update reqs 2024-03-30 06:32:59 +00:00
wh1te909
d81fcccf10 add guest sharing perm 2024-03-30 05:56:24 +00:00
wh1te909
ee3a7bbbfc fix run urlactions perms fixes #1819 2024-03-30 05:52:09 +00:00
wh1te909
82d9e2fb16 back to dev 2024-03-30 05:49:56 +00:00
wh1te909
4aa413e697 bump version 2024-03-29 21:07:33 +00:00
wh1te909
04b3fc54b0 add nonalpha chars to mesh password #1814 2024-03-29 20:10:35 +00:00
wh1te909
e4c5a4e886 fix rights 2024-03-29 08:39:18 +00:00
wh1te909
a0ee7a59eb remove old funcs 2024-03-29 08:36:03 +00:00
wh1te909
b4a05160df skip if no mesh node id #1814 2024-03-28 23:43:18 +00:00
Dan
1a437b3961 Merge pull request #1815 from silversword411/develop
Tweaking bug report template
2024-03-28 14:10:14 -07:00
wh1te909
bda8555190 remove lambda 2024-03-28 07:32:54 +00:00
silversword411
10ca38f91d Tweaking bug report template 2024-03-28 02:24:18 -04:00
wh1te909
a468faad20 fix lint 2024-03-28 04:30:31 +00:00
wh1te909
7a20be4aff fix for mesh sync if trmm username is an email 2024-03-28 04:18:25 +00:00
wh1te909
06b974c8a4 back to dev 2024-03-28 04:18:01 +00:00
wh1te909
515394049a bump version 2024-03-27 18:09:17 +00:00
wh1te909
35c8b4f535 add mgmt command to get mesh login url 2024-03-27 17:28:32 +00:00
wh1te909
1a325a66b4 bump versions 2024-03-25 17:35:44 +00:00
wh1te909
7d82116fb9 add home endpoint 2024-03-25 17:29:43 +00:00
wh1te909
8a7bd4f21b update bins 2024-03-24 19:31:12 +00:00
wh1te909
2e5a2ef12d update nats 2024-03-24 19:29:18 +00:00
wh1te909
89aceda65a update reqs 2024-03-21 18:28:59 +00:00
Dan
39fd83aa16 Merge pull request #1810 from dinger1986/dinger1986-add-mesh-coname-to-initial
Update views.py
2024-03-20 17:03:07 -07:00
dinger1986
a23d811fe8 Update tests.py 2024-03-20 23:29:33 +00:00
dinger1986
a238779724 Update tests.py 2024-03-20 23:24:51 +00:00
dinger1986
3a848bc037 Update views.py 2024-03-20 20:52:31 +00:00
wh1te909
0528ecb454 fix iter logic 2024-03-18 09:12:18 +00:00
wh1te909
141835593c ensure email always verified 2024-03-16 09:03:20 +00:00
wh1te909
3d06200368 update deno 2024-03-16 09:02:54 +00:00
wh1te909
729bef9a77 update reqs 2024-03-15 07:53:28 +00:00
wh1te909
94f33bd642 force sync in hosted 2024-03-13 02:00:54 +00:00
wh1te909
7e010cdbca nodesource added their installation scripts back 2024-03-13 01:06:07 +00:00
wh1te909
8887bcd941 disable auto login no longer needed with mesh sync 2024-03-12 05:26:40 +00:00
wh1te909
56aeeee04c add stdout 2024-03-12 05:22:32 +00:00
wh1te909
98eb3c7287 fix mgmt commands 2024-03-11 20:27:12 +00:00
wh1te909
6819c1989b move to mgmt commands 2024-03-11 19:05:20 +00:00
wh1te909
7e01dd3e97 change to run ever 2 hours 2024-03-11 16:49:02 +00:00
wh1te909
ea4f2c3de8 break sync into chunks 2024-03-10 22:29:00 +00:00
wh1te909
b2f63b8761 should have been 10mb default 2024-03-10 21:26:12 +00:00
wh1te909
65865101ce handle large requests 2024-03-10 02:05:38 +00:00
wh1te909
c3637afe69 max websocket max size customizable 2024-03-10 00:14:04 +00:00
wh1te909
ab543ddf0c add option to use own cert during install 2024-03-09 19:21:21 +00:00
wh1te909
80595e76e7 cleanup orphaned checkhistory results fixes #1789 2024-03-09 08:31:25 +00:00
wh1te909
d49e68737a update reqs 2024-03-09 08:30:53 +00:00
wh1te909
712e15ba80 just try returning str for all 2024-03-05 20:45:34 +00:00
wh1te909
986160e667 also allow accessing floats 2024-03-05 20:27:32 +00:00
wh1te909
1ae4e23db1 more sync mesh fixes 2024-03-04 10:05:45 +00:00
wh1te909
bad646141c rework mesh sync #182 2024-03-03 11:37:24 +00:00
wh1te909
7911235b68 fix serializer/tests 2024-02-29 07:53:05 +00:00
wh1te909
12dee4d14d py 3.11.8 and update reqs 2024-02-29 02:09:33 +00:00
wh1te909
cba841beb8 don't show in hosted 2024-02-29 02:07:53 +00:00
wh1te909
4e3ebf7078 remove from local settings 2024-02-29 01:56:19 +00:00
wh1te909
1c34969f64 fix redis 2024-02-25 23:42:08 +00:00
wh1te909
dc26cabacd make sure to cleanup if sync is toggled off 2024-02-25 07:17:54 +00:00
wh1te909
a7bffcd471 install by default 2024-02-25 06:41:40 +00:00
wh1te909
6ae56ac2cc increase max ws response size for instances with large agent counts 2024-02-25 02:18:40 +00:00
wh1te909
03c087020c exclude inactive users from the sync 2024-02-25 02:17:07 +00:00
wh1te909
857a1ab9c4 handle old node and add mgmt command for sync mesh 2024-02-24 23:19:03 +00:00
wh1te909
64d9530e13 fixes to sync mesh #182 2024-02-24 07:53:05 +00:00
wh1te909
5dac1efc30 sync mesh users/perms with trmm #182 2024-02-23 21:17:24 +00:00
wh1te909
18bc74bc96 match more flags 2024-02-23 18:56:23 +00:00
wh1te909
f64efc63f8 allow access to jsonfields in script vars 2024-02-23 02:48:32 +00:00
Dan
e84b897991 Merge pull request #1766 from conlan0/develop
Add agent shutdown endpoint and nats
2024-02-22 13:48:03 -08:00
wh1te909
519647ef93 exit on install if existing 2024-02-22 21:25:45 +00:00
wh1te909
f694fe00e4 allow getting pk/id 2024-02-22 21:18:50 +00:00
wh1te909
0b951f27b6 add defaults 2024-02-22 21:18:06 +00:00
wh1te909
8aa082c9df exit restore if existing install 2024-02-22 21:17:05 +00:00
wh1te909
f2c5d47bd8 add migration 2024-02-22 04:52:05 +00:00
Dan
ac7642cc15 Merge pull request #1676 from NiceGuyIT/feature/cross-platform-scripting
[Feature] Add cross site scripting
2024-02-21 20:48:24 -08:00
conlan0
8f34865dab Add shutdown url 2024-02-21 21:29:53 -05:00
conlan0
c762d12a40 Add shutdown class 2024-02-21 21:29:29 -05:00
wh1te909
fe1e71dc07 update vscode settings 2024-02-21 17:34:44 +00:00
wh1te909
85b0350ed4 update reqs 2024-02-21 17:34:32 +00:00
wh1te909
a980491455 update reqs 2024-02-20 22:28:10 +00:00
wh1te909
5798c0ccaa wrong branch 2024-02-20 22:22:16 +00:00
wh1te909
742f49ca1f update reqs 2024-02-19 06:06:57 +00:00
wh1te909
5560fc805b switch to bigint for pk 2024-02-19 06:01:01 +00:00
wh1te909
9d4f8a4e8c update reqs 2024-02-09 17:39:55 +00:00
wh1te909
b4d25d6285 revert, prevent recursion 2024-02-09 17:31:59 +00:00
wh1te909
a504a376bd avoid db call and add test 2024-02-09 16:59:50 +00:00
wh1te909
f61ea6e90a fix super calls 2024-02-09 16:58:59 +00:00
wh1te909
b2651df36f wrong model, and don't need to pass class 2024-02-09 16:49:52 +00:00
wh1te909
b56c086841 back to dev [skip ci] 2024-02-06 06:46:32 +00:00
wh1te909
4343478c7b bump version 2024-02-06 06:41:51 +00:00
wh1te909
94649cbfc7 handle localhost bind issues on some instances 2024-02-06 06:19:57 +00:00
wh1te909
fb83f84d84 back to dev [skip ci] 2024-02-06 04:20:28 +00:00
wh1te909
84c2632d40 bump versions 2024-02-05 09:06:15 +00:00
wh1te909
3417ee25eb update reqs 2024-02-03 06:15:44 +00:00
wh1te909
6ada30102c bump web ver [skip ci] 2024-02-02 01:15:27 +00:00
wh1te909
ac86ca7266 forgot to add year 2024-02-01 17:15:47 +00:00
wh1te909
bb1d3edf71 make workers consistent with standard install [skip ci] 2024-01-30 19:05:17 +00:00
wh1te909
97b9253017 handle alert template when montype/site changes fixes #1733 2024-01-30 08:56:44 +00:00
wh1te909
971c2180c9 update mesh [skip ci] 2024-01-28 03:54:45 +00:00
wh1te909
f96dc6991e feat: hide custom fields in summary tab only closes #1745 2024-01-28 03:24:47 +00:00
wh1te909
6855493b2f feat: add serial number to linux/mac #1683 2024-01-27 02:54:26 +00:00
wh1te909
ff0d1f7c42 feat: show cpu cores/threads in summary tab closes #1715 2024-01-27 01:32:09 +00:00
wh1te909
3ae5824761 internal only now 2024-01-26 20:55:32 +00:00
wh1te909
702e865715 format 2024-01-26 20:55:08 +00:00
wh1te909
6bcf64c83f fix func 2024-01-26 19:35:33 +00:00
wh1te909
18b270c9d0 fixes to nats rework and add tests 2024-01-26 19:19:38 +00:00
wh1te909
783376acb0 node 20 2024-01-26 18:35:39 +00:00
wh1te909
81dab470d2 blacked 2024-01-26 07:38:52 +00:00
wh1te909
a12f0feb66 rework nats 2024-01-26 07:26:50 +00:00
wh1te909
d3c99d9c1c update bins 2024-01-26 07:09:00 +00:00
wh1te909
3eb3586c0f ioutil is deprecated 2024-01-26 07:08:18 +00:00
wh1te909
fdde16cf56 feat: add from name to email closes #1726 2024-01-26 00:39:45 +00:00
wh1te909
b8bc5596fd feat: add time and ret code to script test #1713 2024-01-26 00:03:11 +00:00
wh1te909
47842a79c7 update reqs 2024-01-26 00:02:08 +00:00
wh1te909
391d5bc386 update nats-api 2024-01-21 03:42:02 +00:00
wh1te909
ba8561e357 update reqs 2024-01-21 03:17:13 +00:00
wh1te909
6aa1170cef fix for redis 5 2024-01-16 03:18:10 +00:00
wh1te909
6d4363e685 prep for celery 6 2024-01-16 02:53:45 +00:00
wh1te909
6b02b1e1e8 update reqs 2024-01-15 03:16:58 +00:00
wh1te909
58a5550989 debian repo issue #1721 2023-12-30 01:20:40 +00:00
wh1te909
f225c5cf9a nodesource no longer installs npm on node 18 2023-12-29 05:24:43 +00:00
Dan
5c62c7992c Merge pull request #1717 from alexcmatm/patch-1
Add gmail relay handling for emails
2023-12-27 17:03:30 -08:00
wh1te909
70b8f09ccb fix logic 2023-12-28 00:50:24 +00:00
Alexandra Stone
abfeafa026 Add gmail relay handling for emails
This change adds ehlo and starttls when the server hostname is smtp-relay.gmail.com and authentication is disabled.
Just sending the message and quitting isn't enough for gmail specifically.
2023-12-27 14:07:57 -07:00
wh1te909
aa029b005f back to dev [skip ci] 2023-12-24 01:36:38 +00:00
wh1te909
b753d2ca1e bump agent version 2023-12-24 01:10:59 +00:00
wh1te909
1e50329c9e bump version 2023-12-22 17:40:36 +00:00
wh1te909
4942811694 update reqs 2023-12-22 17:38:16 +00:00
wh1te909
59e37e0ccb also make sudo changes to restore 2023-12-22 17:38:04 +00:00
Dan
20aa86d8a9 Merge pull request #1712 from Tenebor/ubuntu-psql-fix
Ubuntu psql fix and cert folder chown
2023-12-21 12:44:51 -08:00
Tenebor
64c5ab7042 fix: chown on ssl cert
Exec chown on /etc/letsencrypt only in case of secure installation.
2023-12-21 20:51:16 +01:00
Tenebor
d210f5171a fix: use interactive shell to run psql
Using ubuntu "sudo -u postgres psql" returns a permission error
2023-12-21 16:47:39 +01:00
wh1te909
c7eee0f14d update reqs 2023-12-11 19:27:28 +00:00
wh1te909
221753b62e update hash_bucket_size 2023-12-11 18:36:08 +00:00
wh1te909
d213e4d37f vscode 2023-12-11 18:35:21 +00:00
wh1te909
f8695f21d3 back to dev 2023-12-11 18:34:15 +00:00
David Randall
4ac1030289 Fix: Unused import
Signed-off-by: David Randall <David@NiceGuyIT.biz>
2023-12-10 18:41:30 -05:00
David Randall
93c7117319 Fix: Whitespace formatting 2023-12-10 18:06:09 -05:00
David Randall
974afd92ce Merge remote-tracking branch 'upstream/develop' into feature/cross-platform-scripting 2023-12-05 19:32:55 -05:00
wh1te909
be847baaed bump versions 2023-12-04 21:37:56 +00:00
wh1te909
2b819e6751 clarify wording 2023-12-04 19:55:47 +00:00
wh1te909
66247cc005 add version check for onboarding tasks 2023-12-04 18:41:19 +00:00
David Randall
eafd38d3f2 Merge branch 'feature/cross-platform-scripting' of github.com:NiceGuyIT/tacticalrmm into feature/cross-platform-scripting 2023-12-03 23:20:00 -05:00
David Randall
c4e590e7a0 Add: Server variables are opt-out by default
- Pull the Nushell and Deno versions from the server.
- Support downloading Nushell and Deno from a url (not GitHUb).
- Add support for nu config.nu and env.nu files.
- Add support for default Deno permissions.
2023-12-03 23:19:43 -05:00
wh1te909
b92a594114 doesn't support go 1.21 yet, removing for now 2023-12-01 20:26:02 +00:00
wh1te909
9dfb16f6b8 update ci 2023-12-01 20:07:04 +00:00
wh1te909
4b74866d85 update bins 2023-12-01 19:50:07 +00:00
wh1te909
f532c85247 update natsapi 2023-12-01 19:49:10 +00:00
wh1te909
b1cc00c1bc dynamically import custom filters 2023-12-01 19:25:36 +00:00
wh1te909
5696aa49d5 update reqs 2023-12-01 19:24:54 +00:00
wh1te909
e12dc936fd fix tests 2023-11-27 21:58:27 +00:00
sadnub
6d39a7fb75 add onboarding task and revert runonce 2023-11-22 23:42:45 -05:00
sadnub
c87c312349 set insecure nats mode for docker dev 2023-11-22 23:42:45 -05:00
wh1te909
e9c1886cdd bump webver [skip ci] 2023-11-23 00:52:15 +00:00
wh1te909
13e4b1a781 increase timeout and change logger 2023-11-22 23:14:31 +00:00
wh1te909
3766fb14ef increase timeout 2023-11-22 22:55:35 +00:00
sadnub
29ee50e38b fix flake8 2023-11-22 16:52:48 -05:00
sadnub
d1ab69dc31 change nats task payload for run once task change 2023-11-22 16:47:22 -05:00
wh1te909
e3c4a54193 small fixes 2023-11-22 10:23:54 +00:00
wh1te909
2abbd2e3cf switch logger 2023-11-22 10:13:43 +00:00
wh1te909
f9387a5851 update reqs 2023-11-21 19:50:22 +00:00
wh1te909
7a9fb74b54 remove log 2023-11-21 19:49:27 +00:00
David Randall
d754f3dd4c Merge branch 'develop' into feature/cross-platform-scripting 2023-11-18 20:08:49 -05:00
David Randall
f54fc9e990 Fix: Linux uninstall script
Signed-off-by: David Randall <David@NiceGuyIT.biz>
2023-11-18 19:51:59 -05:00
wh1te909
8952095da5 fix payload and skip posix 2023-11-15 07:56:10 +00:00
wh1te909
597240d501 fixes to async rework 2023-11-15 02:53:10 +00:00
wh1te909
7377906d02 update reqs 2023-11-14 23:46:46 +00:00
wh1te909
ce6da1bce3 async rework of sync scheduled tasks 2023-11-13 02:44:37 +00:00
David Randall
1bf8ff73f8 [Feature] Add cross site scripting 2023-11-12 15:10:18 -05:00
wh1te909
564aaaf3df rework agent uninstall perms fixes #1673 2023-11-09 20:01:42 +00:00
wh1te909
64ba69b2d0 fix sorted migrations 2023-11-09 19:59:49 +00:00
wh1te909
ce5ada42af django-ipware is deprecated, switch to python-ipware 2023-11-08 21:34:24 +00:00
wh1te909
1ce5973713 call task directly and remove note about debug log 2023-11-08 21:21:35 +00:00
wh1te909
b035b53092 remove pytz 2023-11-08 08:17:23 +00:00
sadnub
7d0e02358c fix json output with custom fields 2023-11-07 17:34:20 -05:00
sadnub
374ff0aeb5 increase max text for report template and base template name field 2023-11-07 17:34:20 -05:00
wh1te909
947a43111e back to dev [skip ci] 2023-11-07 18:56:34 +00:00
wh1te909
5fed81c27b bump versions 2023-11-07 17:19:35 +00:00
wh1te909
dce4f1a5ae update reqs 2023-11-07 17:19:23 +00:00
wh1te909
7e1fc32a1c forgot to bump version of backup script last update 2023-11-07 17:17:37 +00:00
sadnub
a69f14f504 add loop controls and expression extensions to jinja 2023-11-03 15:49:10 -04:00
wh1te909
931069458d add custom filter for local_ips and rework imports 2023-11-03 17:17:54 +00:00
wh1te909
a5259baab0 start adding support for custom jinja filters 2023-11-03 16:58:43 +00:00
wh1te909
8aaa27350d expose ZoneInfo to template 2023-11-03 16:22:11 +00:00
wh1te909
ac74d2b7c2 remove debug stuff 2023-11-02 21:25:24 +00:00
sadnub
2b316aeae9 expose datetime and re modules to template 2023-11-02 16:37:23 -04:00
wh1te909
aff96a45c6 back to dev [skip ci] 2023-11-01 23:31:26 +00:00
wh1te909
e2f524ce7a this is a major version duh [skip ci] 2023-11-01 19:30:25 +00:00
wh1te909
a58b054292 bump version 2023-11-01 19:00:53 +00:00
wh1te909
ea9e5be1fc bump script versions [skip ci] 2023-10-31 18:32:01 +00:00
wh1te909
760ea4727c update reqs 2023-10-31 18:14:51 +00:00
wh1te909
f57f2e53a0 better scaling 2023-10-30 00:09:14 +00:00
Dan
136a393a17 Merge pull request #1663 from lcsnetworks/docker_allow_custom_uwsgi_configs
Add option to skip uWSGI config in Docker environments
2023-10-29 16:22:41 -07:00
wh1te909
8bbaab78b7 update markdown 2023-10-29 22:11:13 +00:00
wh1te909
067cd59637 daphne needed for tests 2023-10-29 21:46:21 +00:00
wh1te909
ce6ac7bf53 replace daphne with uvicorn 2023-10-29 21:38:33 +00:00
wh1te909
99271c4477 comment flaky test for now 2023-10-29 21:38:06 +00:00
wh1te909
156142ed58 bump web ver [skip ci] 2023-10-29 19:34:47 +00:00
wh1te909
4b5516c0eb update reqs 2023-10-29 19:01:55 +00:00
wh1te909
c3d8d2d240 change datetime 2023-10-29 18:54:13 +00:00
wh1te909
c29cf70025 back to uwsgi 2023-10-28 20:07:50 +00:00
wh1te909
6ebce55be3 update for weasyprint 2023-10-28 01:41:50 +00:00
sadnub
01c4a85bc0 move from uwsgi to gunicorn in docker. fix pulling dynamic web tar 2023-10-27 09:37:26 -04:00
sadnub
12d4206d84 update dockerfile image versions 2023-10-27 09:36:15 -04:00
wh1te909
946de18bea move import 2023-10-27 06:33:10 +00:00
wh1te909
904eb3538c fix grep 2023-10-27 06:32:14 +00:00
wh1te909
c851ca9328 switch to gunicorn due to issues with uwsgi and reporting 2023-10-27 02:22:16 +00:00
wh1te909
0ac415ad83 lower max requests per worker 2023-10-26 06:11:14 +00:00
sadnub
b3ba34d980 update docker to support reporting 2023-10-25 23:30:20 -04:00
wh1te909
52740271d9 nginx updates and python 3.11.6 2023-10-26 01:08:52 +00:00
wh1te909
c2e444249a add helper 2023-10-25 20:21:06 +00:00
wh1te909
97310b091e update reqs 2023-10-25 15:56:10 +00:00
Dan
4dda9cc3a1 Merge pull request #1086 from sadnub/feat-reports
Reporting Feature
2023-10-24 18:24:24 -07:00
wh1te909
a0538b57e2 more refurb 2023-10-25 01:10:56 +00:00
wh1te909
d7f394eeb6 refurb 2023-10-25 00:38:07 +00:00
wh1te909
1bc4571d42 isort 2023-10-25 00:18:54 +00:00
wh1te909
22e878502a return error 2023-10-25 00:12:18 +00:00
wh1te909
03c1b6e30c update repo 2023-10-24 22:29:10 +00:00
Joel DeTeves
374a434d98 Add option to skip uWSGI config in Docker environments 2023-10-24 14:35:16 -07:00
wh1te909
f1e85ff0e9 update license 2023-10-24 05:34:00 +00:00
wh1te909
6b010f76ea add download 2023-10-24 05:18:47 +00:00
wh1te909
0c3e9f7824 update reqs 2023-10-23 23:32:17 +00:00
wh1te909
ccca578622 test with superuser 2023-10-20 22:47:26 +00:00
wh1te909
56f7c18550 add reporting perms 2023-10-20 22:24:07 +00:00
wh1te909
d438f71bbb add assets 2023-10-20 20:25:43 +00:00
wh1te909
ca5df24b6d add pending actions to reporting 2023-10-18 22:38:22 +00:00
sadnub
4a6c2d106f fix and add some tests for csv data queries 2023-10-15 18:26:02 -04:00
sadnub
cd25a9568b remove reporting user and configuration 2023-10-15 12:24:50 -04:00
sadnub
f78a787adb initial wip shared report templates 2023-10-14 23:08:59 -04:00
sadnub
dc520fa77c allow overwriting templates on name conflicts. Remove 'make_dataqueries_inline' 2023-10-14 20:51:36 -04:00
sadnub
8f06d4dd9d add csv option to data source 2023-10-14 19:49:32 -04:00
sadnub
a7047183e1 use django timezone to get current time 2023-10-13 18:07:01 -04:00
sadnub
c0b145da24 add yaml extension to get the current date and also subtract/add time 2023-10-13 17:43:00 -04:00
sadnub
52e7fd6f72 add plain text template type 2023-10-05 12:59:42 -04:00
wh1te909
4bbe22b1c7 small fixes 2023-10-04 16:51:26 +00:00
sadnub
4747ffc08b fix dockerfile in dev and remove chart rendering if data query is empty 2023-10-04 11:01:28 -04:00
wh1te909
9d07131fd6 remove duplicate entry 2023-10-03 22:51:47 +00:00
wh1te909
721126d3db function renamed 2023-10-03 20:15:57 +00:00
wh1te909
2b65f5e3dc update reqs 2023-10-03 20:13:56 +00:00
Dan
57f10cf387 using psycopg3 now 2023-10-03 12:45:11 -07:00
wh1te909
f60c8a173b add redis ping to monitoring endpoint 2023-10-02 17:14:13 +00:00
Dan
857cd690be Merge pull request #1643 from bc24fl/develop
Added optional web port override settings
2023-10-02 10:05:31 -07:00
sadnub
a407b60152 fix report preview 2023-10-02 12:32:02 -04:00
sadnub
2c3c55adc0 fix test 2023-10-02 12:32:02 -04:00
sadnub
f586b4da17 fix flake8 errors 2023-10-02 12:32:02 -04:00
sadnub
0b7eb41049 finish up tests and some code rework 2023-10-02 12:32:02 -04:00
sadnub
bd19c4e2bd add json support for data sources 2023-10-02 12:32:02 -04:00
sadnub
e8a73087d6 fix custom fields 2023-10-02 12:32:02 -04:00
sadnub
dde4fd82f4 update json schema and add custom fields to data sources 2023-10-02 12:32:02 -04:00
wh1te909
0420c393f3 fix grep 2023-10-02 12:32:02 -04:00
wh1te909
c88dac6437 fix mkdir 2023-10-02 12:32:02 -04:00
wh1te909
cd450f55e2 fix command 2023-10-02 12:32:02 -04:00
wh1te909
190ee7f9fb add query schema view 2023-10-02 12:32:02 -04:00
wh1te909
fd057300cc black and isort 2023-10-02 12:32:02 -04:00
wh1te909
56791089c1 generate must come before collectstatic 2023-10-02 12:32:02 -04:00
wh1te909
e91cb32ca3 redo migrations and fix hardcoded url 2023-10-02 12:32:02 -04:00
wh1te909
9ab20df8d2 update pandas 2023-10-02 12:32:02 -04:00
sadnub
050350501c fix some issues and improve report import/export 2023-10-02 12:32:02 -04:00
sadnub
d078acdf73 fix error messages and resolve data frames in charts 2023-10-02 12:32:02 -04:00
sadnub
b786a688b5 fix up json schema with new options 2023-10-02 12:32:02 -04:00
sadnub
6b7fe40dd2 limited any variable analysis queries to 1 result 2023-10-02 12:32:02 -04:00
sadnub
6f6c422246 add variables length to sidebar 2023-10-02 12:32:02 -04:00
sadnub
d371ff4f60 variables introspection 2023-10-02 12:32:02 -04:00
sadnub
d1a8348912 fix report preview without debug 2023-10-02 12:32:02 -04:00
sadnub
be956d3cb6 allow traversing relations in debug view 2023-10-02 12:32:02 -04:00
sadnub
ba5beb81b7 some fixes 2023-10-02 12:32:02 -04:00
sadnub
106bbe5244 add debug mode for preview. add template import/export. other fixes 2023-10-02 12:32:02 -04:00
sadnub
f39d0e7ba2 send template errors to frontend 2023-10-02 12:32:02 -04:00
sadnub
de7a1fd8ff more improvements 2023-10-02 12:32:02 -04:00
sadnub
1ac2b25876 send error messages to UI when generating reports 2023-10-02 12:32:02 -04:00
sadnub
9e014d1371 put yaml data source in variables to support variables 2023-10-02 12:32:02 -04:00
sadnub
93b274a113 fix the variable replacement in variables 2023-10-02 12:32:02 -04:00
sadnub
474c7ae873 Update config.py 2023-10-02 12:32:02 -04:00
sadnub
31690d4cad charts 2023-10-02 12:32:02 -04:00
sadnub
bbfc7e7e49 create DB user in mgmt command for docker build 2023-10-02 12:32:02 -04:00
sadnub
1c0aa55e7a more improvements 2023-10-02 12:32:02 -04:00
sadnub
29778ca19e fix report assets over https and add an endpoint for asset selection 2023-10-02 12:32:02 -04:00
sadnub
9e87318cc5 get jinja templates 100% compatible with reporting 2023-10-02 12:32:02 -04:00
sadnub
c645be6b70 fix data lookups 2023-10-02 12:32:02 -04:00
sadnub
57fc5ac088 docker and install script fixes 2023-10-02 12:32:02 -04:00
sadnub
924774f52a fix report asset path 2023-10-02 12:32:02 -04:00
sadnub
446a7a0844 fix url 2023-10-02 12:32:02 -04:00
sadnub
5cfeed76d0 fix 2023-10-02 12:32:02 -04:00
sadnub
de419319d8 fix branch 2023-10-02 12:32:02 -04:00
sadnub
7a3d36899b fix permissions 2023-10-02 12:32:02 -04:00
sadnub
f5dbb363f4 install script fixes 2023-10-02 12:32:00 -04:00
sadnub
2bbc59a212 fix install/update script 2023-10-02 12:31:31 -04:00
sadnub
3403d76aae reporting wip 2023-10-02 12:31:29 -04:00
bc24fl
58399cedb6 Update docker-compose.yml
Added optional web port override settings for those who prefer to use tactical behind a proxy.
2023-10-02 00:46:18 -04:00
bc24fl
9bca7e9e11 Update .env.example
Added optional web port override settings for those who prefer to use tactical behind a proxy.
2023-10-02 00:43:25 -04:00
wh1te909
3a61430e44 back to dev [skip ci] 2023-10-02 01:58:49 +00:00
wh1te909
a2e996b550 bump version 2023-10-02 01:49:57 +00:00
wh1te909
cfc1c31050 rename setting 2023-10-02 00:12:39 +00:00
wh1te909
45106bf6f9 remove apt-key [skip ci] 2023-10-01 15:59:14 +00:00
wh1te909
6e3cfe491b update chocos fixes #1538 2023-09-30 23:33:52 +00:00
wh1te909
12f2158afd feat: make env vars expand custom fields closes #1609 2023-09-30 22:05:52 +00:00
wh1te909
6d78773c55 bump web ver 2023-09-30 22:02:49 +00:00
wh1te909
43a62d4eb6 update reqs 2023-09-30 20:53:08 +00:00
wh1te909
cc08dfda96 make beta api optional 2023-09-30 19:33:22 +00:00
Dan
622e33588e Merge pull request #1636 from redanthrax/beta-api
beta api clients, agents, sites with paging
2023-09-30 12:16:54 -07:00
wh1te909
67980b58a0 fix docker 2023-09-29 08:29:59 +00:00
redanthrax
027e444955 beta api clients, agents, sites with paging
formatted with black

django filter requirement

updated beta api, restricted to get and put
2023-09-27 14:36:14 -07:00
wh1te909
d838750389 update reqs 2023-09-27 17:25:56 +00:00
wh1te909
71d8bd5266 update reqs 2023-09-20 03:24:40 +00:00
wh1te909
ec4ae24bbd add note about x forwarding 2023-09-20 03:21:15 +00:00
wh1te909
1128149359 fix docker mesh npm install 2023-09-20 03:20:15 +00:00
wh1te909
bdfc6634ec fix tempdir cleanup [skip ci] 2023-09-13 20:29:33 +00:00
wh1te909
ca4d19667b update reqs 2023-09-11 02:43:50 +00:00
wh1te909
c71aa7baa7 back to dev 2023-09-11 02:40:28 +00:00
wh1te909
9dc0b24399 bump versions 2023-09-01 23:48:31 +00:00
wh1te909
747954e6fb wording 2023-09-01 22:03:51 +00:00
wh1te909
274f4f227e node install script is deprecated [skip ci] 2023-09-01 21:12:45 +00:00
wh1te909
92197d8d49 change to localhost 2023-09-01 18:56:09 +00:00
wh1te909
aee06920eb more self signed stuff 2023-09-01 18:55:34 +00:00
wh1te909
5111b17d3c bump web ver [skip ci] 2023-08-30 04:29:36 +00:00
wh1te909
2849d8f45d update scripts for self signed 2023-08-29 23:53:19 +00:00
wh1te909
bac60d9bd4 feat: reset all checks status closes amidaware/tacticalrmm#1615 2023-08-29 20:36:20 +00:00
wh1te909
9c797162f4 only Manual is supported in insecure mode 2023-08-29 20:33:58 +00:00
wh1te909
09d184e2f8 update installers 2023-08-25 18:25:09 +00:00
wh1te909
7bca618906 allow self-signed certs 2023-08-24 21:40:51 +00:00
wh1te909
67607103e9 back to dev [skip ci] 2023-08-24 21:05:50 +00:00
wh1te909
b42f2ffe33 bump version [skip ci] 2023-08-18 04:29:41 +00:00
wh1te909
30a3f185ef fix npm #1604 [skip ci] 2023-08-18 04:28:58 +00:00
wh1te909
83b9d13ec9 bump version [skip ci] 2023-08-14 20:57:14 +00:00
wh1te909
cee7896c37 back to dev [skip ci] 2023-08-14 17:06:40 +00:00
wh1te909
b472f3644e bump versions 2023-08-14 16:42:32 +00:00
wh1te909
5d8ea837c8 fix posix restart 2023-08-12 00:28:20 +00:00
wh1te909
82de6bc849 syntax fix [skip ci] 2023-08-11 23:05:14 +00:00
wh1te909
cb4bc68c48 fix syntax [skip ci] 2023-08-11 22:07:59 +00:00
wh1te909
3ce6b38247 bump dev vers [skip ci] 2023-08-11 07:08:48 +00:00
wh1te909
716c0fe979 handle cloud init hosts file 2023-08-11 05:14:43 +00:00
wh1te909
c993790b7a bump web ver [skip ci] 2023-08-06 23:08:35 +00:00
wh1te909
aa32286531 update reqs 2023-08-06 22:31:40 +00:00
wh1te909
6f94abde00 fix alert filtering fixes #1572 2023-07-30 20:05:34 +00:00
wh1te909
fa19538c9d handle custom certs in backup/restore 2023-07-30 07:14:44 +00:00
wh1te909
84c858b878 fix issuer name 2023-07-30 07:12:51 +00:00
wh1te909
865de142d4 update reqs 2023-07-30 07:11:33 +00:00
wh1te909
9118162553 fix duplicate agent customfields 2023-07-18 22:23:34 +00:00
wh1te909
f4fc6ee9b4 update nats 2023-07-18 22:22:19 +00:00
wh1te909
108c38d57b update reqs 2023-07-18 22:20:38 +00:00
wh1te909
a1d73eb830 noexec 2023-07-18 22:10:29 +00:00
wh1te909
997906a610 formatting 2023-07-18 22:06:36 +00:00
wh1te909
b6e5d120d3 mongo check [skip ci] 2023-07-11 01:06:45 +00:00
wh1te909
d469d0b435 format [skip ci] 2023-07-11 01:05:10 +00:00
Dan
e9f823e000 Merge pull request #1560 from dinger1986/develop
change script to work with debian 12
2023-07-10 17:58:02 -07:00
dinger1986
d7fb76ba74 Update troubleshoot_server.sh 2023-07-10 23:35:57 +01:00
dinger1986
b7dde1a0d9 Merge branch 'amidaware:develop' into develop 2023-07-10 23:06:41 +01:00
dinger1986
15095d8c23 Update troubleshoot_server.sh 2023-07-10 10:13:06 +01:00
wh1te909
dfbebc7606 testing psycopg3 2023-07-09 07:04:38 +00:00
wh1te909
895309d93d pg 15 for ci 2023-07-09 07:03:48 +00:00
wh1te909
bcf50e821a update ansible for 0.16.0 2023-07-07 09:07:43 +00:00
wh1te909
30195800dd handle new dir 2023-07-07 03:59:48 +00:00
wh1te909
6532b0f149 back to dev 2023-07-07 03:58:36 +00:00
sadnub
5e108e4057 fix dockerfile 2023-07-05 21:16:43 -04:00
Dan
c2b2f4d222 Merge pull request #1550 from dinger1986/develop
changed find and delete of old backups
2023-07-05 17:49:12 -07:00
dinger1986
bc4329ad21 Update backup.sh 2023-07-05 22:38:50 +01:00
dinger1986
aec6d1b2f6 Update backup.sh 2023-07-05 22:38:30 +01:00
wh1te909
6fe4c5a2ed bump versions 2023-07-05 02:03:34 +00:00
wh1te909
4abc8e41d8 only chown if exists 2023-07-05 02:02:58 +00:00
wh1te909
af694f1ce9 arm64/deb12/ubuntu22 support, remove mongo, postgres 15 and node 18 2023-07-04 08:53:06 +00:00
wh1te909
7c3a5fcb83 update reqs 2023-07-03 23:35:45 +00:00
wh1te909
57f64b18c6 bump web ver [skip ci] 2023-06-30 20:34:12 +00:00
wh1te909
4cccc7c2f8 fix escape error 2023-06-27 23:00:12 +00:00
wh1te909
903a2d6a6e flaky test 2023-06-25 02:16:19 +00:00
wh1te909
34c674487a update reqs 2023-06-24 22:20:59 +00:00
Dan
d15a8c5af3 Merge pull request #1543 from dinger1986/develop
Update troubleshoot_server.sh
2023-06-23 20:23:51 -07:00
dinger1986
3e0dec9383 Update troubleshoot_server.sh 2023-06-24 00:02:41 +01:00
wh1te909
8b810aad81 add arm64 bin [skip ci] 2023-06-19 06:31:50 +00:00
wh1te909
e676bcb4f4 update reqs 2023-06-19 05:44:00 +00:00
wh1te909
a7aed77764 python 3.11.4 2023-06-19 05:29:10 +00:00
wh1te909
88875c0257 update reqs 2023-06-19 05:25:52 +00:00
wh1te909
f711a0c91a update reqs 2023-06-05 05:40:36 +00:00
wh1te909
d8a076cc6e update demo 2023-06-04 01:59:46 +00:00
wh1te909
c900831ee9 back to dev 2023-06-04 01:59:25 +00:00
wh1te909
ae5d0b1d81 bump versions 2023-05-30 23:42:49 +00:00
wh1te909
cd5e87be34 update reqs 2023-05-30 18:33:31 +00:00
wh1te909
3e967f58d2 ansible fixes [skip ci] 2023-05-29 07:51:16 +00:00
wh1te909
1ea005ba7e add cert expiring soon indicator #722 2023-05-27 23:34:51 +00:00
wh1te909
092772ba90 add wake-on-lan closes #1180 2023-05-27 00:45:30 +00:00
wh1te909
b959854a76 add serial number to search closes #1355 2023-05-26 22:55:44 +00:00
wh1te909
8ccb1ebe4f make cmd placeholder text customizable amidaware/tacticalrmm-web#5 2023-05-26 22:16:26 +00:00
wh1te909
91b3be6467 update reqs 2023-05-26 21:29:19 +00:00
wh1te909
d79d5feacc update requests 2023-05-25 20:56:04 +00:00
wh1te909
5cc78ef9d5 allow customizing dashboard colors #1514 2023-05-25 20:41:06 +00:00
wh1te909
8639cd5a72 back to django 4.1 2023-05-25 20:36:20 +00:00
wh1te909
021ddc17e7 remove mypy 2023-05-25 20:35:35 +00:00
wh1te909
ee47b8d004 update nats server 2023-05-23 04:54:01 +00:00
wh1te909
55d267c935 remove ignore 2023-05-23 04:48:29 +00:00
Dan
0fd0b9128d Merge pull request #1511 from dinger1986/develop
Updates to the Update script and backup script
2023-05-18 22:43:59 -07:00
wh1te909
d9cf505b50 change default to mixed closes #1513 2023-05-17 07:13:09 +00:00
wh1te909
6079332dda remove redundant ws close 2023-05-17 07:07:52 +00:00
wh1te909
929ec20365 remove deprecated django func 2023-05-14 06:30:25 +00:00
wh1te909
d0cad3055f comment flaky test for now 2023-05-14 06:22:00 +00:00
wh1te909
4974a13bc0 test django 4.2 2023-05-14 05:50:36 +00:00
wh1te909
bd048df225 update demo 2023-05-14 05:47:50 +00:00
dinger1986
ed83cbd574 Update update.sh 2023-05-11 20:02:25 +01:00
dinger1986
7230207853 Update backup.sh 2023-05-11 20:00:49 +01:00
dinger1986
1ead8a72ab Update update.sh 2023-05-11 19:59:39 +01:00
dinger1986
36a2e9d931 Update backup.sh 2023-05-11 19:58:29 +01:00
dinger1986
0f147a5518 Update backup.sh 2023-05-11 19:54:07 +01:00
dinger1986
fce511a18b Update backup.sh 2023-05-11 19:45:41 +01:00
wh1te909
64bb61b009 back to dev [skip ci] 2023-05-11 02:29:41 +00:00
wh1te909
4c6f829c92 bump versions 2023-05-11 02:16:45 +00:00
wh1te909
8c5cdd2acb back to dev [skip ci] 2023-05-10 20:04:26 +00:00
wh1te909
3800f19966 bump versions [skip ci] 2023-05-10 19:56:20 +00:00
wh1te909
7336f84a4b update reqs 2023-05-08 22:01:45 +00:00
wh1te909
7bf4a5b2b5 begin pytz removal 2023-05-08 21:12:08 +00:00
wh1te909
43a7b97218 update reqs 2023-05-07 02:22:28 +00:00
wh1te909
9f95c57a09 fix timezone logic bug for run once tasks 2023-04-23 21:47:01 +00:00
wh1te909
8f6056ae66 remove deprecated package 2023-04-13 20:52:23 +00:00
wh1te909
9bcac6b10e improve superseded update detection fixes #647 fixes #820 2023-04-13 20:35:03 +00:00
wh1te909
86318e1b7d async rework of bulk script and more async refactor 2023-04-11 07:14:16 +00:00
wh1te909
a8a1458833 update demo 2023-04-11 06:57:13 +00:00
wh1te909
942c1e2dfe catch exception and code cleanup 2023-04-11 06:56:35 +00:00
sadnub
a6b6814eae Merge pull request #1479 from sadnub/develop
open ai integration
2023-04-10 19:06:14 -04:00
sadnub
0af95aa9b1 add error handling 2023-04-10 18:57:44 -04:00
sadnub
b4b9256867 open ai integration 2023-04-09 22:36:57 -04:00
wh1te909
a6f1281a98 back to dev [skip ci] 2023-04-09 04:52:00 +00:00
wh1te909
741c74e267 bump versions 2023-04-09 03:43:16 +00:00
wh1te909
3061dba5ed add prefetch 2023-04-07 22:29:35 +00:00
wh1te909
09f5f4027e fix import 2023-04-07 20:33:57 +00:00
wh1te909
925695fd56 python 3.11.3 2023-04-07 20:30:24 +00:00
wh1te909
3c758be856 update ansible go 2023-04-07 20:28:55 +00:00
wh1te909
569b76a7e3 bump webver 2023-04-07 20:25:23 +00:00
wh1te909
dca69eff9c update nats-api 2023-04-07 20:24:43 +00:00
wh1te909
6b8fedc675 try flaky test again 2023-04-07 20:23:36 +00:00
wh1te909
c42a379e7c update reqs 2023-04-07 20:22:18 +00:00
wh1te909
a40858adbf optimize query 2023-04-07 20:21:22 +00:00
wh1te909
19bc720bc9 fix script email results not saving output to history 2023-04-07 16:56:36 +00:00
wh1te909
bf79ca30bb optimize query 2023-04-07 16:55:29 +00:00
Dan
75454895e5 Merge pull request #1467 from Supermanu/bulk_delete_agents_hostname
Enable hostname filtering for bulk agents deletion
2023-04-06 20:16:56 -07:00
wh1te909
c81aa2d6fe set nats reply expiration 2023-04-07 02:32:41 +00:00
Supermanu
376f6369b8 Enable combination of filters for bulk agents deletion 2023-04-04 13:42:30 +02:00
wh1te909
b1e67a1ed3 fix lint 2023-04-04 06:07:59 +00:00
wh1te909
7393a30bd1 fix flaky test 2023-04-04 05:45:37 +00:00
Dan
c934065f8e Merge pull request #1468 from jpros/add-custom-fields-to-agents-route
Added agent's custom fields to table result
2023-04-03 22:30:20 -07:00
wh1te909
56124d2b50 add prefetch to view to avoid n+1 queries 2023-04-04 05:13:17 +00:00
wh1te909
e8a003ff8a update reqs 2023-04-04 04:55:54 +00:00
João Paulo Ros
4c789225b2 Added agent's custom fields to table result
This action is to enable search and summary in the dashboard view.
2023-03-30 11:44:15 -07:00
Supermanu
59dcdd5393 Enable hostname filtering for bulk delete agents 2023-03-30 12:21:48 +02:00
wh1te909
b28316a4f2 back to dev [skip ci] 2023-03-23 21:00:49 +00:00
wh1te909
b5eed69712 bump versions 2023-03-23 19:54:21 +00:00
wh1te909
b79aacb2a7 validate hours fixes #1461 2023-03-23 19:42:38 +00:00
wh1te909
8a2eb7b058 use older syntax for systems with older systemd like synology 2023-03-22 22:15:53 +00:00
wh1te909
7316d076a2 make wording more clear 2023-03-22 22:14:27 +00:00
wh1te909
479d3bcb40 bump web ver [skip ci] 2023-03-21 19:01:27 +00:00
wh1te909
f2358f1530 remove overwrite backup script #1363 [skip ci] 2023-03-21 18:43:26 +00:00
wh1te909
47d9e1b966 fix formatting 2023-03-21 05:57:24 +00:00
wh1te909
c53657d693 update reqs and python3.11 2023-03-20 01:58:54 +00:00
Dan
f19ce59e00 Merge pull request #1413 from dinger1986/develop
Update restore.sh
2023-03-14 15:04:21 -07:00
wh1te909
076f3e05d6 update ansible 2023-03-10 02:50:35 +00:00
wh1te909
7d017f9494 feat: bulk run checks by client or site 2023-03-10 00:19:58 +00:00
wh1te909
675de4e420 update reqs and nats-api 2023-03-10 00:18:33 +00:00
wh1te909
418a709c6c test with 3.11.2 2023-03-06 01:37:45 +00:00
wh1te909
1d7dd1b754 update reqs 2023-03-05 08:05:37 +00:00
Dan
3fa70d6d2b Merge pull request #1444 from silversword411/develop
tweaking mesh check error
2023-02-27 10:57:43 -08:00
silversword411
9c67f52161 tweaking mesh check error 2023-02-27 13:28:36 -05:00
wh1te909
9f2f23fa96 blacked 2023-02-15 00:21:57 +00:00
wh1te909
46d955691a update reqs 2023-02-15 00:15:44 +00:00
dinger1986
3f8800187d Update restore.sh 2023-01-24 22:38:39 +00:00
Dan
ebbe90dfa8 Merge pull request #1391 from dinger1986/develop
Changes to Backup
2023-01-20 12:52:42 -08:00
wh1te909
a0e1783e18 bump versions [skip ci] 2023-01-18 20:13:30 +00:00
wh1te909
fc83e11d8b add note about celery stopping 2023-01-18 20:13:06 +00:00
wh1te909
f43627b170 bump dev versions 2023-01-17 00:47:51 +00:00
wh1te909
8964441f44 async refactor of bulk cmd 2023-01-16 08:48:52 +00:00
wh1te909
cfd7a0c621 add more filtering 2023-01-16 08:47:51 +00:00
wh1te909
15a422873e handle exception 2023-01-16 08:45:55 +00:00
wh1te909
d1f5583cd7 cleanup 2023-01-16 08:44:43 +00:00
wh1te909
08f07c6f3e update reqs 2023-01-16 08:43:15 +00:00
Dan
35a08debc3 Merge pull request #1402 from lcsnetworks/mesh_pe_options
Add options to enable/disable Mesh Compression, WebRTC
2023-01-12 15:21:02 -08:00
Joel DeTeves
a3424c480f Add options to enable/disable Mesh Compression, WebRTC 2023-01-12 13:12:35 -08:00
wh1te909
118ced0a43 async rework of remove orphaned tasks 2023-01-11 00:31:56 +00:00
wh1te909
6d355ef0cd add ignore 2023-01-10 23:48:56 +00:00
wh1te909
a8aa5ac231 add todo note 2023-01-10 22:20:13 +00:00
wh1te909
df6bc0b3c9 refactor 2023-01-10 22:18:55 +00:00
wh1te909
6b965b765c add new endpoint 2023-01-10 22:12:27 +00:00
Dan
d7aea6b5ba Merge pull request #1397 from mollymaemilano/develop
Update views.py
2023-01-07 14:01:03 -08:00
mollymaemilano
1e9a46855d Update views.py 2023-01-06 12:27:00 -06:00
wh1te909
91e9c18110 fix tests 2023-01-04 08:34:50 +00:00
wh1te909
8ffa6088d7 fix db conn leak 2023-01-04 08:18:46 +00:00
wh1te909
52d2f8364f delete task after expiry date fixes #1367 2023-01-04 07:57:16 +00:00
dinger1986
1f679af6fa Update backup.sh 2023-01-01 01:06:41 +00:00
dinger1986
1ba92cdcd5 Update backup.sh 2022-12-31 23:41:32 +00:00
dinger1986
45c60ba5f5 Update backup.sh 2022-12-31 23:30:54 +00:00
dinger1986
d3eef45608 Update backup.sh 2022-12-31 23:25:46 +00:00
wh1te909
1960c113d4 add more locks 2022-12-31 00:34:51 +00:00
wh1te909
63d6b4a1c9 fix tests 2022-12-30 23:58:18 +00:00
wh1te909
9f47bb1252 fix policy tasks 2022-12-30 23:52:43 +00:00
wh1te909
df4fea31d0 adjust some random sleeps 2022-12-30 23:41:21 +00:00
wh1te909
98ef1484c8 rework task to use threading 2022-12-30 22:27:09 +00:00
wh1te909
c4ef9960b9 auto scale celery workers in docker 2022-12-30 21:26:41 +00:00
wh1te909
6b6f7744aa add clearing of celery locks to docker 2022-12-30 21:21:45 +00:00
wh1te909
9192fa0fe2 improve some account management commands 2022-12-30 21:15:32 +00:00
wh1te909
3c7c2dc1a5 implement celery locking and rework some tasks 2022-12-30 01:24:57 +00:00
wh1te909
5c176a1af0 beat needs to stop first 2022-12-30 00:53:09 +00:00
wh1te909
6d03a1cc76 update note about format 2022-12-28 19:31:05 +00:00
wh1te909
1cf10edef1 code cleanup 2022-12-28 19:06:50 +00:00
wh1te909
6a97c63bf4 no longer need this file 2022-12-28 19:03:25 +00:00
wh1te909
15f9612bfa revert redis for celery 2022-12-28 01:49:22 +00:00
wh1te909
9a7c90b194 only if powershell 2022-12-27 07:49:50 +00:00
wh1te909
91f2708a87 fix tests and handle custom fields #1345 2022-12-24 02:04:00 +00:00
wh1te909
7bf3ecd89d better fix for #1345 2022-12-24 01:01:08 +00:00
wh1te909
4768581631 fix tests 2022-12-24 00:22:01 +00:00
wh1te909
aa4cd10e13 fix for when args contain single quote #1345 2022-12-24 00:07:18 +00:00
wh1te909
34ae57e6fe bump versions 2022-12-21 19:59:23 +00:00
wh1te909
107c2b50e2 update middleware 2022-12-21 19:09:11 +00:00
wh1te909
a832765203 add packaging to reqs 2022-12-21 17:33:56 +00:00
wh1te909
977fee82b5 bump docker nats [skip ci] 2022-12-21 17:04:25 +00:00
wh1te909
8c74cbc1c6 test with 3.11.1 2022-12-21 03:53:44 +00:00
wh1te909
b38eec5039 update bin 2022-12-21 03:10:54 +00:00
wh1te909
6c20b932fa update nats and add some debug callbacks 2022-12-21 03:09:52 +00:00
wh1te909
deb24c638f allow self reset without user perms #1378 2022-12-20 23:20:47 +00:00
wh1te909
40fcdb4d28 update reqs 2022-12-20 23:18:06 +00:00
wh1te909
f3e44cf458 back to dev [skip ci] 2022-12-08 07:22:58 +00:00
wh1te909
483bf331fa bump version 2022-12-08 06:59:40 +00:00
wh1te909
9d62b4acdd use iterator instead of all in case large queryset 2022-12-08 06:59:02 +00:00
wh1te909
c9deef6e76 add a migration for old tasks 2022-12-08 05:34:41 +00:00
wh1te909
8ba6f8b0e1 fix keyerror for old tasks 2022-12-07 20:26:24 +00:00
wh1te909
448c59ea88 bump docker nats 2022-12-04 23:33:05 +00:00
wh1te909
91b858bf33 bump versions 2022-12-04 23:31:57 +00:00
wh1te909
c12bede980 update bin 2022-12-04 07:14:05 +00:00
wh1te909
71e9fa3d16 update name and deps 2022-12-04 07:11:01 +00:00
wh1te909
6800b9aaae fix mgmt command 2022-12-04 06:14:37 +00:00
wh1te909
77d44f25f9 create and log an agent history event for alert failure/resolved script runs 2022-12-04 02:11:49 +00:00
wh1te909
ab6227828b add env vars for script checks 2022-12-03 09:56:44 +00:00
wh1te909
719ba56c59 remove db hit 2022-12-03 09:55:37 +00:00
wh1te909
dacedf4018 add ram check 2022-12-03 08:14:53 +00:00
wh1te909
2526fa3c47 fix backup/restore when OS are different 2022-12-01 06:33:27 +00:00
wh1te909
7e2295c382 rework celery config 2022-12-01 00:22:09 +00:00
wh1te909
6ef02004ff update reqs 2022-11-30 23:56:18 +00:00
wh1te909
0e60d062e9 feat: env vars 2022-11-30 23:25:52 +00:00
wh1te909
80a94f97c4 add ws compression option 2022-11-30 23:25:52 +00:00
wh1te909
c18bc5fe67 switch to stringio 2022-11-30 23:25:52 +00:00
wh1te909
02b98a2429 don't call now twice 2022-11-30 23:25:52 +00:00
Dan
0383aeaa87 Merge pull request #1358 from styx-tdo/develop
Fix wrong service name
2022-11-30 12:49:25 -08:00
Spam Me
15a41d532e Fix wrong service name 2022-11-30 15:01:17 +01:00
wh1te909
0f49725789 update reqs 2022-11-22 19:13:44 +00:00
wh1te909
1db6733e66 add checkin config 2022-11-22 19:06:18 +00:00
wh1te909
0343ee4f6b fix mypy 2022-11-13 08:05:42 +00:00
wh1te909
2c37d2233a add flake8 2022-11-13 07:44:23 +00:00
wh1te909
0cb8ccfddd back to dev [skip ci] 2022-11-13 01:53:05 +00:00
wh1te909
35b1a39ed8 bump versions 2022-11-13 01:36:39 +00:00
wh1te909
61a577ba70 add arch check 2022-11-12 20:58:29 +00:00
wh1te909
a1e32584fa bump docker nats 2022-11-12 20:57:57 +00:00
wh1te909
28e0ee536d update reqs 2022-11-12 20:28:27 +00:00
wh1te909
9d64a9c038 update wheel 2022-11-08 21:01:16 +00:00
wh1te909
702ba969c2 revert caching of the check results. will refactor it and do another way 2022-11-08 21:00:49 +00:00
wh1te909
6dde8ee2b8 update reqs 2022-11-08 07:23:22 +00:00
wh1te909
018420310c code cleanup 2022-11-08 07:22:31 +00:00
wh1te909
6d49d34033 update go deps 2022-11-08 07:21:25 +00:00
wh1te909
1fbd403164 fix params 2022-11-08 07:16:32 +00:00
wh1te909
13f544d2be add more caching to speed up agent table loading 2022-11-03 07:35:01 +00:00
wh1te909
3c9e64de81 update reqs 2022-11-03 07:32:04 +00:00
Dan
5a9bafbc32 Merge pull request #1294 from Can-eh-dian11/develop
ability to bulk delete using client and site
2022-10-29 14:27:24 -07:00
wh1te909
b89d96b66f add python 3.11 to testing matrix 2022-10-28 19:16:17 +00:00
wh1te909
b7176191ac update reqs 2022-10-28 04:55:31 +00:00
wh1te909
453c5f47c2 just kidding 2022-10-28 04:43:28 +00:00
wh1te909
eea62e1263 fixed slow dashboard loading due to bad query 2022-10-28 01:02:53 +00:00
Scott MacDowall
4fb2a0f1ca Merge branch 'amidaware:develop' into develop 2022-10-25 23:02:02 -04:00
wh1te909
bf3c65778e bump versions [skip ci] 2022-10-25 22:10:50 +00:00
wh1te909
df7fe3e6b4 bump mesh [skip ci] 2022-10-25 21:40:44 +00:00
wh1te909
b657468b62 update uninstall for new path 2022-10-25 19:03:57 +00:00
wh1te909
4edc0058d3 update reqs 2022-10-25 06:32:51 +00:00
wh1te909
2c3b35293b bump versions 2022-10-25 06:32:38 +00:00
wh1te909
be0c9a4d46 add path 2022-10-25 05:59:14 +00:00
wh1te909
dd4140558e fix tests 2022-10-21 16:51:52 +00:00
wh1te909
71c2519b8e fix wording 2022-10-21 16:40:11 +00:00
wh1te909
badfc26aed fix fake agents script 2022-10-21 16:39:55 +00:00
wh1te909
b2bc3adb3d update demo 2022-10-20 19:03:37 +00:00
wh1te909
5ccf408fd6 back to dev [skip ci] 2022-10-19 23:14:45 +00:00
wh1te909
af16912541 bump versions [skip ci] 2022-10-19 22:50:59 +00:00
wh1te909
1bf9e2a5e6 update reqs 2022-10-19 06:50:01 +00:00
wh1te909
5a572651ff add token expired check 2022-10-19 06:45:53 +00:00
wh1te909
5a191e387f update tests 2022-10-19 04:02:21 +00:00
wh1te909
18f29f5790 update reqs 2022-10-18 19:54:48 +00:00
wh1te909
054a73e0f8 fix tests 2022-10-18 05:07:00 +00:00
wh1te909
14824db7b0 fix tests 2022-10-18 05:02:35 +00:00
wh1te909
721c48ea88 python 3.10.8 2022-10-18 04:54:04 +00:00
wh1te909
ed7bfcfb58 daphne/channels 4 2022-10-18 04:52:04 +00:00
wh1te909
773a40a126 bump docker nats 2022-10-18 04:50:48 +00:00
wh1te909
961252ef26 rework uwsgi conf 2022-10-18 00:23:10 +00:00
wh1te909
a2650f3c47 update setuptools 2022-10-18 00:21:11 +00:00
wh1te909
d71ee194e1 add optional nats monitoring 2022-10-18 00:12:57 +00:00
wh1te909
22e1a4cf41 update reqs 2022-10-14 07:41:16 +00:00
wh1te909
a50bf901d3 add check for wget fixes #1317 2022-10-14 07:39:27 +00:00
wh1te909
c9469635b5 remove unneeded chmod fixes #1307 2022-10-14 07:21:43 +00:00
Dan
36df3278e5 Merge pull request #1311 from dinger1986/develop
Update agent_linux.sh
2022-10-11 16:14:06 -07:00
dinger1986
cb2258aaa8 Update agent_linux.sh 2022-10-10 23:53:24 +01:00
wh1te909
0391d9eb7e update reqs 2022-10-10 17:07:00 +00:00
Dan
12698b4c20 Merge pull request #1310 from dinger1986/develop
Update agent_linux.sh
2022-10-10 10:01:50 -07:00
dinger1986
f7b9d459ab Update agent_linux.sh 2022-10-10 17:51:35 +01:00
wh1te909
65ab14e68b cleanup socket fixes #1210 2022-10-07 17:15:37 +00:00
Dan
93a5dd5de4 Merge pull request #1213 from stavros-k/json
Updating meshcentral's config casing to match upstreams json schema.
2022-10-06 09:38:16 -07:00
Dan
61807bdaaa Merge pull request #1301 from af7567/policyselect2
break from loop once a valid policy is found. remove old unused list
2022-10-06 09:36:59 -07:00
Dan
a1a5d1adba Merge pull request #1295 from silversword411/develop
typo
2022-10-06 09:36:20 -07:00
silversword411
9dd4aefea5 Merge branch 'amidaware:develop' into develop 2022-10-06 07:52:47 -04:00
Adam
db4540089a remove parentheses from if statement to fix codestyle test
Signed-off-by: Adam <adam@csparker.co.uk>
2022-10-03 13:17:48 +01:00
Adam
24c899c91a break from loop once a valid policy is found. remove old unused list
Signed-off-by: Adam <adam@csparker.co.uk>
2022-10-03 12:05:32 +01:00
wh1te909
ade1a73966 code cleanup and optimizations 2022-10-02 21:42:46 +00:00
wh1te909
fb9ec2b040 update reqs 2022-10-02 21:42:03 +00:00
wh1te909
3a683812e9 change tempdir 2022-10-02 21:37:04 +00:00
wh1te909
6d317603c9 fix script snippets fixes #1298 2022-09-27 23:21:26 +00:00
silversword411
5a3d2d196c typo 2022-09-25 17:21:17 -04:00
Scott MacDowall
e740c4d980 fix formatting 2022-09-25 14:59:05 -04:00
Scott MacDowall
253e4596e2 ability to bulk delete using client and site 2022-09-25 14:32:40 -04:00
wh1te909
4f885c9a79 bump versions 2022-09-24 03:08:44 +00:00
wh1te909
b519d2afac update readme for macOS support 2022-09-24 03:07:38 +00:00
wh1te909
6b61e3b76b bump docker nats 2022-09-24 02:52:12 +00:00
wh1te909
30b9c72c31 bump nats 2022-09-24 01:51:59 +00:00
wh1te909
385bf74f6e change dl cmd 2022-09-24 01:51:52 +00:00
wh1te909
be5615e530 bump web dev 2022-09-23 23:07:48 +00:00
wh1te909
d81a03c093 mac agent 2022-09-23 22:57:29 +00:00
wh1te909
f8249c8267 switch to corp funding 2022-09-23 22:10:05 +00:00
wh1te909
5a1cbdcd3b update reqs 2022-09-23 06:59:44 +00:00
wh1te909
e0c99d87bd update template 2022-09-23 06:59:26 +00:00
wh1te909
548250029d add missing field to serializer fixes #1283 2022-09-13 19:18:16 +00:00
wh1te909
66a354dbdc back to dev [skip ci] 2022-09-12 07:03:05 +00:00
wh1te909
1f693ca4f6 bump versions 2022-09-12 06:50:55 +00:00
wh1te909
97a0bc6045 optimize query to use less ram 2022-09-12 05:33:07 +00:00
wh1te909
8b75cdfefd update bin 2022-09-12 01:44:21 +00:00
wh1te909
917aecf1ff update deps and go 1.19 2022-09-12 01:42:24 +00:00
wh1te909
663dcd0396 fix tests 2022-09-11 20:51:57 +00:00
wh1te909
8f2dffb1ad update reqs 2022-09-11 20:29:45 +00:00
wh1te909
20228e3d19 add postgres ready check 2022-09-11 20:23:18 +00:00
wh1te909
81c6cc11b3 update supported version 2022-09-11 01:37:45 +00:00
wh1te909
2ccacbe5f3 fix for newer mesh 2022-09-11 01:33:00 +00:00
Dan
a5345e8468 Merge pull request #1277 from af7567/winupdatesfix
When checking if patches are to be installed, don't exit the function after finding a patched machine.
2022-09-06 12:02:26 -07:00
Dan
8f5d62bb81 Merge pull request #1273 from silversword411/develop
Adding client and site to mgmt command find_software
2022-09-06 11:59:07 -07:00
Adam
28f6838560 continue to next agent rather than return from function, otherwise other agents won't receive updates
Signed-off-by: Adam <adam@csparker.co.uk>
2022-09-04 18:09:58 +01:00
silversword411
c86aacb31c format tweak 2022-09-02 17:24:33 -04:00
silversword411
f62f5192d6 Adding client and site to mgmt command find_software 2022-09-02 14:28:45 -04:00
wh1te909
b14ea1fe3e fix maintenance mode and assigning policy via agent context menu not saving 2022-08-24 08:29:13 +00:00
wh1te909
552633a00b fix configs 2022-08-24 08:27:50 +00:00
wh1te909
7faba2a690 add migrations for updated pytz 2022-08-24 07:33:55 +00:00
wh1te909
db910aff06 back to dev 2022-08-23 06:02:48 +00:00
wh1te909
75d9f6a7e7 bump versions 2022-08-23 05:09:55 +00:00
wh1te909
de677294c6 refactor 2022-08-23 00:54:07 +00:00
wh1te909
da1e6b8259 update reqs 2022-08-22 06:47:52 +00:00
wh1te909
a9633b3990 update reqs 2022-08-20 20:09:53 +00:00
sadnub
7ac9af1cc1 fix sed command to do an inplace update of nginx.conf 2022-08-17 11:52:54 -04:00
Dan
00d8b8cd61 Merge pull request #1244 from dinger1986/develop
Update troubleshoot_server.sh
2022-08-17 08:43:13 -07:00
dinger1986
af7ff7f5cf Update troubleshoot_server.sh
Change SSL check as wasnt working and add output
2022-08-17 11:42:05 +01:00
wh1te909
2a20719130 fix demo script check history graph 2022-08-17 06:57:05 +00:00
sadnub
f481940180 fix tests 2022-08-14 21:56:09 -04:00
sadnub
ca8824d1e3 fix clients not filtering by role in policy overview 2022-08-14 21:49:38 -04:00
sadnub
f4be199b77 fix checks/tasks return cache values for other plats 2022-08-14 19:45:11 -04:00
sadnub
6bcef8334e fix nginx entypoint 2022-08-14 10:26:55 -04:00
wh1te909
3955eff683 more dev setup 2022-08-14 08:17:08 +00:00
wh1te909
aa0f6ecd75 python 3.10.6 2022-08-14 08:14:59 +00:00
wh1te909
ef4a94ed78 more ansible 2022-08-12 17:21:25 +00:00
wh1te909
b5c803ce65 update reqs 2022-08-12 17:19:04 +00:00
wh1te909
d4325ed82e more ansible dev 2022-08-12 06:15:21 +00:00
wh1te909
3805fb8f26 expose more fields in search amidaware/tacticalrmm-web@93dbc74e33 closes #652 2022-08-12 01:23:33 +00:00
wh1te909
d4d938c655 update paths 2022-08-12 01:06:56 +00:00
wh1te909
1c6911e361 start moving to nested serializers 2022-08-10 07:15:23 +00:00
wh1te909
a1b364f337 drop support for agent < 2.0.0 2022-08-10 07:12:51 +00:00
wh1te909
ece5c3da86 update reqs 2022-08-10 00:49:20 +00:00
wh1te909
5d1ae6047b back to dev 2022-08-10 00:38:13 +00:00
wh1te909
66bbcf0733 fix tests 2022-08-09 21:35:23 +00:00
wh1te909
acc23ea7bb bump versions 2022-08-09 21:18:41 +00:00
wh1te909
663bd0c9f0 remove dead code 2022-08-09 21:16:18 +00:00
wh1te909
39b1025dfa fix tests 2022-08-05 17:35:40 +00:00
sadnub
d2875e90b2 fix docker dev 2022-08-05 12:10:52 -04:00
wh1te909
ff461d1d02 fixes #1174 amidaware/tacticalrmm-web@76f330fb9c 2022-08-05 07:22:46 +00:00
wh1te909
58164ea2d3 dev 2022-08-05 05:57:38 +00:00
wh1te909
1bf4834004 Django 4.1 2022-08-04 23:43:57 +00:00
wh1te909
bf58d78281 fix return tuple formatting 2022-08-04 23:40:35 +00:00
wh1te909
a8aedfde55 bump version 2022-08-01 22:56:21 +00:00
wh1te909
9b92d1b673 bump version 2022-08-01 17:50:33 +00:00
wh1te909
febc9aed11 feat: run as user amidaware/tacticalrmm-web@137a5648ce amidaware/rmmagent@50cebb950d 2022-07-31 22:23:19 +00:00
wh1te909
de2462677e fix working dir 2022-07-31 21:31:58 +00:00
wh1te909
8bd94d46eb fix empty statement 2022-07-28 17:28:49 +00:00
wh1te909
d43cefe28f add file associations for yaml [skip ci] 2022-07-27 07:31:54 +00:00
wh1te909
b82874e261 back to develop 2022-07-27 07:30:53 +00:00
wh1te909
f901614056 bump version 2022-07-27 06:11:41 +00:00
wh1te909
b555d217ab remove check 2022-07-27 06:10:33 +00:00
wh1te909
775c600234 docker nginx changes 2022-07-27 04:19:17 +00:00
wh1te909
128f2570b8 catch exception if mesh is down 2022-07-27 02:01:29 +00:00
wh1te909
3cd53e79b4 add signing key 2022-07-27 01:59:55 +00:00
wh1te909
ebba84ffda switch to official nginx repo to get latest version 2022-07-26 08:09:49 +00:00
wh1te909
1e1a42fe98 update web ver 2022-07-26 08:08:43 +00:00
wh1te909
8a744a440d update reqs 2022-07-26 07:47:21 +00:00
wh1te909
f4fc3c7d55 unused var 2022-07-26 04:39:11 +00:00
wh1te909
0594d121de add agent ver to status closes #1224 2022-07-24 01:18:54 +00:00
wh1te909
12c85d6234 start ansible role to deploy dev environment 2022-07-20 07:16:47 +00:00
Stavros kois
87d05223af apply json fix to docker aswell 2022-07-18 22:08:06 +03:00
Stavros kois
babf6366e8 chore(install script): use same casing as the json schema 2022-07-18 21:13:20 +03:00
wh1te909
5e37728f66 remove devskum 2022-07-18 17:22:24 +00:00
wh1te909
e8e19fede7 don't allow dates in past #1174 2022-07-18 08:04:15 +00:00
wh1te909
e565dbfa66 invalidate cache on policy script change 2022-07-12 20:16:07 +00:00
wh1te909
d180d6820c back to develop 2022-07-10 03:35:15 +00:00
wh1te909
41db8681f8 no sudo 2022-07-10 00:38:09 +00:00
wh1te909
26cd58fd6d bump version 2022-07-10 00:16:32 +00:00
wh1te909
63c7e1aa9d update reqs 2022-07-10 00:16:13 +00:00
wh1te909
d5a6063e5e remove extra space 2022-07-09 09:31:05 +00:00
wh1te909
00affdbdec update supported version 2022-07-09 09:30:58 +00:00
wh1te909
db3f0bbd4f increase nginx open file limit 2022-07-09 08:09:24 +00:00
wh1te909
020a59cb97 remove un-needed expose 2022-07-09 08:08:09 +00:00
wh1te909
ff4fa6402d back to dev 2022-07-08 06:40:55 +00:00
wh1te909
10cc187c5d bump versions 2022-07-08 06:38:20 +00:00
wh1te909
25843edb48 bump script ver 2022-07-07 20:34:47 +00:00
wh1te909
54294141b0 bump versions 2022-07-07 19:44:11 +00:00
wh1te909
f3a8886b50 remove check for 4222 2022-07-07 16:31:01 +00:00
wh1te909
268cfaf234 bump web ver 2022-07-07 03:06:52 +00:00
wh1te909
651ae20304 forgot decorator 2022-07-06 23:37:52 +00:00
wh1te909
e22f69a5dc fix variable 2022-07-06 01:15:55 +00:00
wh1te909
a39808f44c update reqs 2022-07-05 18:07:20 +00:00
wh1te909
fcb541f734 update nginx conf for nats websocket 2022-07-03 22:44:59 +00:00
wh1te909
79ca0f1684 bump web ver 2022-07-03 01:48:39 +00:00
wh1te909
f2ebc38044 don't load swagger app unless enabled 2022-07-03 01:36:26 +00:00
wh1te909
d4335675f1 update nats-api and add debug info 2022-06-30 06:37:58 +00:00
wh1te909
be4b05423e update reqs 2022-06-30 06:27:45 +00:00
wh1te909
d9fe8db2a7 bump mesh 2022-06-30 06:27:28 +00:00
wh1te909
f92e780765 add delete endpoint 2022-06-30 06:21:13 +00:00
wh1te909
7aebdb7c78 add back middleware for django admin 2022-06-30 06:20:38 +00:00
wh1te909
abb2dd842b bigint for retcode 2022-06-29 07:57:58 +00:00
wh1te909
75713c8015 bump mesh and web 2022-06-27 08:09:55 +00:00
wh1te909
42e1717455 add nats websocket support 2022-06-26 22:16:28 +00:00
wh1te909
bfb19a9eb7 bump docker nats 2022-06-26 21:14:27 +00:00
wh1te909
3e08585114 add endpoint for trmm server monitoring and stats 2022-06-26 07:35:11 +00:00
wh1te909
12e82c7a8d update channels 2022-06-26 07:33:23 +00:00
wh1te909
0fcc683903 remove debug print 2022-06-26 07:02:15 +00:00
wh1te909
ed7a8dc0f5 add websocket endpoint for sendcmd 2022-06-25 09:05:26 +00:00
wh1te909
0a9d29c98d add dev check to version 2022-06-25 09:05:26 +00:00
wh1te909
f63e801608 auto scale uwsgi workers based on load 2022-06-25 09:05:26 +00:00
Dan
77f04e1a32 Merge pull request #1186 from silversword411/develop
Syncing readme with docs
2022-06-23 11:04:09 -07:00
silversword411
362819ce16 Syncing readme with docs 2022-06-23 12:14:38 -04:00
wh1te909
1d9165a627 more tests 2022-06-23 05:25:18 +00:00
wh1te909
7ee8aaa027 update reqs 2022-06-23 05:16:34 +00:00
wh1te909
516e279fc3 add migration 2022-06-23 05:15:08 +00:00
wh1te909
880611eddb forgot to call 2022-06-23 01:21:33 +00:00
wh1te909
c4bf776069 fix coverage 2022-06-23 00:33:15 +00:00
wh1te909
097d6464c0 update coverage 2022-06-23 00:23:53 +00:00
wh1te909
b86e4e017f add back coverage 2022-06-22 22:41:47 +00:00
wh1te909
bbec17d498 isort 2022-06-22 22:18:04 +00:00
wh1te909
3b7b5f4ec3 more tests 2022-06-22 22:10:03 +00:00
wh1te909
0986efef29 tests 2022-06-22 06:51:25 +00:00
wh1te909
06091cbf1c fix undefined var 2022-06-22 03:07:41 +00:00
wh1te909
b588bab268 start refactor for mac agent and break all the tests 2022-06-20 19:35:51 +00:00
wh1te909
0736cfe959 update reqs 2022-06-17 06:04:52 +00:00
Dan
400352254a Merge pull request #1173 from sarog/develop
Generify shell script interpreters
2022-06-15 13:39:56 -07:00
Saro G
259c3dc781 Generify shell script interpreters. 2022-06-15 16:04:58 -04:00
wh1te909
506055a815 update reqs 2022-06-07 16:53:51 +00:00
wh1te909
3edf6c57ba add goarch constant 2022-06-06 04:05:19 +00:00
wh1te909
c404ae7ac8 add bulk recovery endpoint, tests 2022-06-03 00:36:19 +00:00
wh1te909
312774e472 add tests 2022-06-01 19:14:30 +00:00
wh1te909
c540f802b0 add mgmt command to help with restores 2022-05-30 07:33:55 +00:00
wh1te909
6a2a2761e1 update docker for new web build 2022-05-29 07:40:49 +00:00
wh1te909
2508458c80 update reqs 2022-05-29 07:40:15 +00:00
wh1te909
025d9e0141 add find service 2022-05-26 18:54:02 +00:00
wh1te909
734b3b07ab update scripts for frontend repo move 2022-05-24 22:03:24 +00:00
wh1te909
e4250a857a add web version 2022-05-24 21:02:19 +00:00
wh1te909
56d1b2716c add mgmt command for scripts 2022-05-24 19:17:48 +00:00
wh1te909
c5d7e61e6c optimize query 2022-05-24 19:16:28 +00:00
wh1te909
6222a127bd update reqs 2022-05-24 19:15:33 +00:00
wh1te909
f0b7e515b6 fix workflows 2022-05-19 04:00:37 +00:00
wh1te909
98d8c23868 web now in separate repo 2022-05-19 03:45:31 +00:00
wh1te909
978bb9afd0 update certifi 2022-05-18 19:46:13 +00:00
wh1te909
058598b5f3 custom field enums 2022-05-18 19:20:28 +00:00
wh1te909
5b7ab3a10f more task enums 2022-05-18 18:37:35 +00:00
wh1te909
e42243c78b task type enum 2022-05-18 07:18:53 +00:00
wh1te909
c650ee8498 alert enums 2022-05-18 06:49:29 +00:00
wh1te909
50f8968901 agent history enum, remove unused model field 2022-05-18 06:11:40 +00:00
wh1te909
b0fa2e6d80 mon type enum 2022-05-18 05:48:02 +00:00
wh1te909
d59589425e status const 2022-05-18 05:18:06 +00:00
wh1te909
6c810e514b fix tests 2022-05-18 04:14:20 +00:00
wh1te909
efa41dbd22 add plat enum 2022-05-18 03:52:51 +00:00
sadnub
f34bcfd56d move to quasar vite plugin 2022-05-16 22:42:01 -04:00
sadnub
8ff2e3fb29 Fix removal of orphaned win tasks not completing if having issues communicating with an agent 2022-05-15 14:38:28 -04:00
wh1te909
033c04a0f2 back to develop [skip-ci] 2022-05-15 08:21:41 +00:00
wh1te909
cef1ab9512 bump versions 2022-05-15 07:01:47 +00:00
wh1te909
94f02bfca3 only if exists 2022-05-15 06:59:00 +00:00
sadnub
a941bb1744 disable auto quoting when using variable substitution on batch scripts. Fixes #1020 2022-05-14 22:45:25 -04:00
sadnub
6ff591427a Add watcher in agent view for route changes and set new active agent. Fixes #1110 2022-05-14 22:23:03 -04:00
sadnub
809e172280 Fixes check policy copy issue where copied checks are all of type diskspace 2022-05-14 22:01:29 -04:00
wh1te909
17aedae0a9 recreate env 2022-05-14 23:59:32 +00:00
wh1te909
ef817ccb3a isort 2022-05-14 23:30:01 +00:00
wh1te909
0fb55b0bee remove middleware 2022-05-14 23:23:24 +00:00
wh1te909
a1a6eddc31 update reqs 2022-05-14 23:00:31 +00:00
wh1te909
ff3d0b6b57 handle empty payload, add monitoring endpoint 2022-05-13 00:50:53 +00:00
wh1te909
dd64cef4c4 optimize endpoints, remove deprecated endpoint, start reworking tests 2022-05-11 22:38:03 +00:00
Dan
9796848079 Merge pull request #1127 from silversword411/develop
typo fix
2022-05-11 14:54:05 -07:00
silversword411
fea7eb4312 typo fix 2022-05-11 17:10:06 -04:00
wh1te909
c12cd0e755 fix task run doesn't show name in history tab fixes #1097 2022-05-10 19:21:49 +00:00
wh1te909
d86a72f858 remove py 3.10.2 2022-05-10 18:52:47 +00:00
wh1te909
50cd7f219a update reqs 2022-05-10 18:45:33 +00:00
wh1te909
8252b3eccc more enum 2022-05-10 17:09:06 +00:00
wh1te909
d0c6e3a158 move tests 2022-05-10 16:29:09 +00:00
wh1te909
1505fa547e update nats reqs and change mod name 2022-05-10 16:27:51 +00:00
wh1te909
9017bad884 fix test 2022-05-09 17:35:07 +00:00
wh1te909
2ac5e316a5 refactor recovery func 2022-05-09 06:43:04 +00:00
wh1te909
29f9113062 update coverage 2022-05-09 06:40:33 +00:00
wh1te909
46349672d8 optimize checkrunner result endpoint 2022-05-09 06:23:59 +00:00
wh1te909
4787be2db0 fix tabs in popout view #1110 2022-05-04 22:17:45 +00:00
wh1te909
f0a8c5d732 fix site sorting fixes #1118 2022-05-04 15:46:05 +00:00
wh1te909
9ad520bf7c remove unnecessary middleware 2022-05-02 00:57:40 +00:00
wh1te909
bd0cc51554 update asgi 2022-05-02 00:57:01 +00:00
wh1te909
12f599f974 add linux mint 2022-05-01 20:34:31 +00:00
wh1te909
0118d5fb40 log enums 2022-04-30 02:01:20 +00:00
wh1te909
65cadb311a more enum 2022-04-29 19:09:17 +00:00
wh1te909
dd75bd197d add back dummy cache 2022-04-29 17:22:10 +00:00
wh1te909
7e155bdb43 typing 2022-04-29 06:45:20 +00:00
wh1te909
993b6fddf4 redundant 2022-04-29 06:41:40 +00:00
wh1te909
6ba51df6a7 add sh to download 2022-04-29 06:29:43 +00:00
wh1te909
1185ac58e1 more enum 2022-04-29 06:21:48 +00:00
wh1te909
f835997f49 switch runners and use redis cache during testing 2022-04-29 05:23:50 +00:00
wh1te909
a597dba775 fix role cache 2022-04-29 05:21:51 +00:00
wh1te909
3194e83a66 update reqs 2022-04-29 00:27:01 +00:00
wh1te909
096c3cdd34 more enum 2022-04-28 18:01:11 +00:00
wh1te909
3a1ea42333 fix settings 2022-04-28 17:33:51 +00:00
wh1te909
64877d4299 fix black not auto formatting 2022-04-28 17:25:21 +00:00
wh1te909
e957dc5e2c black 2022-04-28 17:24:45 +00:00
wh1te909
578d5c5830 more enum 2022-04-28 17:07:58 +00:00
sadnub
96284f9508 make eslint error on warnings 2022-04-28 09:50:41 -04:00
wh1te909
698b38dcba fix warning 2022-04-28 07:07:33 +00:00
wh1te909
6db826befe fix dir again 2022-04-28 07:02:38 +00:00
wh1te909
1a3d412d73 pwd 2022-04-28 07:01:44 +00:00
wh1te909
b8461c9dd8 fix dir 2022-04-28 07:00:09 +00:00
wh1te909
699bd9de10 fix workflow 2022-04-28 06:56:03 +00:00
Dan
54b6866e21 Merge pull request #1102 from sadnub/typescript
Typescript prep and add some linting
2022-04-27 23:45:33 -07:00
sadnub
afd155e9c1 fix actions 2022-04-27 22:48:29 -04:00
sadnub
910a717230 add name to gh action 2022-04-27 22:34:52 -04:00
sadnub
70fbd33d61 add lint and formatting gh action 2022-04-27 21:18:52 -04:00
sadnub
2da0d5ee21 add typescript support and stricter formatting/linting 2022-04-27 20:47:32 -04:00
wh1te909
98f64e057a update reqs 2022-04-27 05:07:22 +00:00
wh1te909
3d9d936c56 update url 2022-04-27 05:06:21 +00:00
wh1te909
2b4cb59df8 add more typing to dev reqs 2022-04-26 23:34:10 +00:00
wh1te909
9d80da52e3 more demo stuff 2022-04-26 23:33:45 +00:00
wh1te909
fd176d2c64 start moving to enums for choicefields 2022-04-26 01:13:18 +00:00
sadnub
538b6de36b set default alert severities for checks and tasks so that blank alert templates being applied to agent don't stop all alerts 2022-04-25 14:38:34 -04:00
sadnub
f7eca8aee0 Ignore timezone when editing automated tasks 2022-04-25 14:17:57 -04:00
wh1te909
a754d94c2c remove test multiprocessing 2022-04-25 08:15:06 +00:00
wh1te909
5e3493e6a9 fix tests 2022-04-25 07:54:31 +00:00
wh1te909
619a14c26b refactor action_type 2022-04-25 07:47:58 +00:00
wh1te909
7d9a8decf0 start choice field refactor, remove deprecated model fields 2022-04-25 07:10:33 +00:00
wh1te909
d11e14ad89 add migration 2022-04-25 06:51:42 +00:00
wh1te909
69189cf2af isort 2022-04-25 06:50:48 +00:00
wh1te909
6e7d2f19d2 update middleware for django 4, refactor a func to fix circular import, start fixing fake_agents script 2022-04-25 06:48:14 +00:00
wh1te909
d99ebf5d6a remove deprecated model field 2022-04-25 06:43:58 +00:00
wh1te909
ef2d19e95b back to develop 2022-04-25 01:36:12 +00:00
487 changed files with 23129 additions and 53081 deletions

View File

@@ -1,11 +1,11 @@
# pulls community scripts from git repo
FROM python:3.10-slim AS GET_SCRIPTS_STAGE
FROM python:3.11.8-slim AS GET_SCRIPTS_STAGE
RUN apt-get update && \
apt-get install -y --no-install-recommends git && \
git clone https://github.com/amidaware/community-scripts.git /community-scripts
FROM python:3.10-slim
FROM python:3.11.8-slim
ENV TACTICAL_DIR /opt/tactical
ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready
@@ -18,7 +18,7 @@ ENV PYTHONUNBUFFERED=1
EXPOSE 8000 8383 8005
RUN apt-get update && \
apt-get install -y build-essential
apt-get install -y build-essential weasyprint
RUN groupadd -g 1000 tactical && \
useradd -u 1000 -g 1000 tactical
@@ -27,7 +27,7 @@ RUN groupadd -g 1000 tactical && \
COPY --from=GET_SCRIPTS_STAGE /community-scripts /community-scripts
# Copy dev python reqs
COPY .devcontainer/requirements.txt /
COPY .devcontainer/requirements.txt /
# Copy docker entrypoint.sh
COPY .devcontainer/entrypoint.sh /

View File

@@ -22,21 +22,6 @@ services:
aliases:
- tactical-backend
app-dev:
container_name: trmm-app-dev
image: node:16-alpine
restart: always
command: /bin/sh -c "npm install npm@latest -g && npm install && npm run serve -- --host 0.0.0.0 --port ${APP_PORT}"
working_dir: /workspace/web
volumes:
- ..:/workspace:cached
ports:
- "8080:${APP_PORT}"
networks:
dev:
aliases:
- tactical-frontend
# nats
nats-dev:
container_name: trmm-nats-dev
@@ -231,6 +216,7 @@ services:
- "443:4443"
volumes:
- tactical-data-dev:/opt/tactical
- ..:/workspace:cached
volumes:
tactical-data-dev: null

View File

@@ -15,10 +15,7 @@ set -e
: "${MESH_PASS:=meshcentralpass}"
: "${MESH_HOST:=tactical-meshcentral}"
: "${API_HOST:=tactical-backend}"
: "${APP_HOST:=tactical-frontend}"
: "${REDIS_HOST:=tactical-redis}"
: "${HTTP_PROTOCOL:=http}"
: "${APP_PORT:=8080}"
: "${API_PORT:=8000}"
: "${CERT_PRIV_PATH:=${TACTICAL_DIR}/certs/privkey.pem}"
@@ -36,12 +33,12 @@ function check_tactical_ready {
}
function django_setup {
until (echo > /dev/tcp/"${POSTGRES_HOST}"/"${POSTGRES_PORT}") &> /dev/null; do
until (echo >/dev/tcp/"${POSTGRES_HOST}"/"${POSTGRES_PORT}") &>/dev/null; do
echo "waiting for postgresql container to be ready..."
sleep 5
done
until (echo > /dev/tcp/"${MESH_SERVICE}"/4443) &> /dev/null; do
until (echo >/dev/tcp/"${MESH_SERVICE}"/4443) &>/dev/null; do
echo "waiting for meshcentral container to be ready..."
sleep 5
done
@@ -52,8 +49,11 @@ function django_setup {
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)
localvars="$(cat << EOF
BASE_DOMAIN=$(echo "import tldextract; no_fetch_extract = tldextract.TLDExtract(suffix_list_urls=()); extracted = no_fetch_extract('${API_HOST}'); print(f'{extracted.domain}.{extracted.suffix}')" | python)
localvars="$(
cat <<EOF
SECRET_KEY = '${DJANGO_SEKRET}'
DEBUG = True
@@ -67,11 +67,17 @@ KEY_FILE = '${CERT_PRIV_PATH}'
SCRIPTS_DIR = '/community-scripts'
ALLOWED_HOSTS = ['${API_HOST}', '*']
ADMIN_URL = 'admin/'
CORS_ORIGIN_ALLOW_ALL = True
ALLOWED_HOSTS = ['${API_HOST}', '${APP_HOST}', '*']
CORS_ORIGIN_WHITELIST = ['https://${APP_HOST}']
SESSION_COOKIE_DOMAIN = '${BASE_DOMAIN}'
CSRF_COOKIE_DOMAIN = '${BASE_DOMAIN}'
CSRF_TRUSTED_ORIGINS = ['https://${API_HOST}', 'https://${APP_HOST}']
HEADLESS_FRONTEND_URLS = {'socialaccount_login_error': 'https://${APP_HOST}/account/provider/callback'}
DATABASES = {
'default': {
@@ -81,6 +87,17 @@ DATABASES = {
'PASSWORD': '${POSTGRES_PASS}',
'HOST': '${POSTGRES_HOST}',
'PORT': '${POSTGRES_PORT}',
},
'reporting': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': '${POSTGRES_DB}',
'USER': 'reporting_user',
'PASSWORD': 'read_password',
'HOST': '${POSTGRES_HOST}',
'PORT': '${POSTGRES_PORT}',
'OPTIONS': {
'options': '-c default_transaction_read_only=on'
}
}
}
@@ -90,14 +107,16 @@ MESH_TOKEN_KEY = '${MESH_TOKEN}'
REDIS_HOST = '${REDIS_HOST}'
MESH_WS_URL = '${MESH_WS_URL}'
ADMIN_ENABLED = True
TRMM_INSECURE = True
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
"${VIRTUAL_ENV}"/bin/python manage.py pre_update_tasks
"${VIRTUAL_ENV}"/bin/python manage.py migrate --no-input
"${VIRTUAL_ENV}"/bin/python manage.py generate_json_schemas
"${VIRTUAL_ENV}"/bin/python manage.py collectstatic --no-input
"${VIRTUAL_ENV}"/bin/python manage.py initial_db_setup
"${VIRTUAL_ENV}"/bin/python manage.py initial_mesh_setup
@@ -107,9 +126,8 @@ EOF
"${VIRTUAL_ENV}"/bin/python manage.py create_natsapi_conf
"${VIRTUAL_ENV}"/bin/python manage.py create_installer_user
"${VIRTUAL_ENV}"/bin/python manage.py post_update_tasks
# 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
}
@@ -123,6 +141,8 @@ if [ "$1" = 'tactical-init-dev' ]; then
mkdir -p /meshcentral-data
mkdir -p ${TACTICAL_DIR}/tmp
mkdir -p ${TACTICAL_DIR}/certs
mkdir -p ${TACTICAL_DIR}/reporting
mkdir -p ${TACTICAL_DIR}/reporting/assets
mkdir -p /mongo/data/db
mkdir -p /redis/data
touch /meshcentral-data/.initialized && chown -R 1000:1000 /meshcentral-data
@@ -130,6 +150,7 @@ if [ "$1" = 'tactical-init-dev' ]; then
touch ${TACTICAL_DIR}/certs/.initialized && chown -R 1000:1000 ${TACTICAL_DIR}/certs
touch /mongo/data/db/.initialized && chown -R 1000:1000 /mongo/data/db
touch /redis/data/.initialized && chown -R 1000:1000 /redis/data
touch ${TACTICAL_DIR}/reporting && chown -R 1000:1000 ${TACTICAL_DIR}/reporting
mkdir -p ${TACTICAL_DIR}/api/tacticalrmm/private/exe
mkdir -p ${TACTICAL_DIR}/api/tacticalrmm/private/log
touch ${TACTICAL_DIR}/api/tacticalrmm/private/log/django_debug.log
@@ -142,16 +163,6 @@ if [ "$1" = 'tactical-init-dev' ]; then
django_setup
# create .env file for frontend
webenv="$(cat << EOF
PROD_URL = "${HTTP_PROTOCOL}://${API_HOST}"
DEV_URL = "${HTTP_PROTOCOL}://${API_HOST}"
APP_URL = "https://${APP_HOST}"
DOCKER_BUILD = 1
EOF
)"
echo "${webenv}" | tee "${WORKSPACE_DIR}"/web/.env > /dev/null
# chown everything to tactical user
chown -R "${TACTICAL_USER}":"${TACTICAL_USER}" "${WORKSPACE_DIR}"
chown -R "${TACTICAL_USER}":"${TACTICAL_USER}" "${TACTICAL_DIR}"

View File

@@ -1,41 +1,3 @@
# 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
asgiref==3.5.0
celery==5.2.6
channels==3.0.4
channels_redis==3.4.0
daphne==3.0.2
Django==4.0.4
django-cors-headers==3.11.0
django-ipware==4.0.2
django-rest-knox==4.2.0
djangorestframework==3.13.1
future==0.18.2
msgpack==1.0.3
nats-py==2.1.0
packaging==21.3
psycopg2-binary==2.9.3
pycryptodome==3.14.1
pyotp==2.6.0
pytz==2022.1
qrcode==7.3.1
redis==4.2.2
requests==2.27.1
twilio==7.8.1
urllib3==1.26.9
validators==0.18.2
websockets==10.2
drf_spectacular==0.22.0
meshctrl==0.1.15
hiredis==2.0.0
# dev
black==22.3.0
django-extensions==3.1.5
isort==5.10.1
mypy==0.942
types-pytz==2021.3.6
model-bakery==1.5.0
coverage==6.3.2
django-silk==4.3.0
django-stubs==1.10.1
djangorestframework-stubs==1.5.0
-r /workspace/api/tacticalrmm/requirements.txt
-r /workspace/api/tacticalrmm/requirements-dev.txt
-r /workspace/api/tacticalrmm/requirements-test.txt

4
.github/FUNDING.yml vendored
View File

@@ -1,9 +1,9 @@
# These are supported funding model platforms
github: wh1te909
github: amidaware
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: tacticalrmm
ko_fi: # tacticalrmm
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username

View File

@@ -14,11 +14,12 @@ assignees: ''
**Installation Method:**
- [ ] Standard
- [ ] Standard with `--insecure` flag at install
- [ ] Docker
**Agent Info (please complete the following information):**
- Agent version (as shown in the 'Summary' tab of the agent from web UI):
- Agent OS: [e.g. Win 10 v2004, Server 2012 R2]
- Agent OS: [e.g. Win 10 v2004, Server 2016]
**Describe the bug**
A clear and concise description of what the bug is.

View File

@@ -10,23 +10,37 @@ on:
jobs:
test:
runs-on: self-hosted
runs-on: ubuntu-latest
name: Tests
strategy:
matrix:
python-version: ["3.11.8"]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Setup virtual env and install requirements
- uses: harmon758/postgresql-action@v1
with:
postgresql version: "15"
postgresql db: "pipeline"
postgresql user: "pipeline"
postgresql password: "pipeline123456"
- name: Setup Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
check-latest: true
- name: Install redis
run: |
sudo apt update
sudo apt install -y redis
redis-server --version
- name: Install requirements
working-directory: api/tacticalrmm
run: |
sudo -u postgres psql -c 'DROP DATABASE IF EXISTS pipeline'
sudo -u postgres psql -c 'DROP DATABASE IF EXISTS test_pipeline'
sudo -u postgres psql -c 'CREATE DATABASE pipeline'
sudo -u postgres psql -c "SET client_encoding = 'UTF8'" pipeline
pwd
rm -rf /actions-runner/_work/trmm-actions/trmm-actions/api/env
cd api
python3.10 -m venv env
source env/bin/activate
cd tacticalrmm
python --version
SETTINGS_FILE="tacticalrmm/settings.py"
SETUPTOOLS_VER=$(grep "^SETUPTOOLS_VER" "$SETTINGS_FILE" | awk -F'[= "]' '{print $5}')
@@ -35,28 +49,33 @@ jobs:
pip install setuptools==${SETUPTOOLS_VER} wheel==${WHEEL_VER}
pip install -r requirements.txt -r requirements-test.txt
- name: Run django tests
env:
GHACTIONS: "yes"
run: |
cd api/tacticalrmm
source ../env/bin/activate
rm -f .coverage coverage.lcov
pytest
if [ $? -ne 0 ]; then
exit 1
fi
- name: Codestyle black
working-directory: api
run: |
cd api
source env/bin/activate
black --exclude migrations/ --check tacticalrmm
if [ $? -ne 0 ]; then
exit 1
fi
- uses: codecov/codecov-action@v2
- name: Lint with flake8
working-directory: api/tacticalrmm
run: |
flake8 --config .flake8 .
if [ $? -ne 0 ]; then
exit 1
fi
- name: Run django tests
env:
GHACTIONS: "yes"
working-directory: api/tacticalrmm
run: |
pytest
if [ $? -ne 0 ]; then
exit 1
fi
- uses: codecov/codecov-action@v3
with:
directory: ./api/tacticalrmm
files: ./api/tacticalrmm/coverage.xml

View File

@@ -1,70 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ develop ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ develop ]
schedule:
- cron: '19 14 * * 6'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'go', 'javascript', 'python' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://git.io/codeql-language-support
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@@ -1,34 +0,0 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
name: DevSkim
on:
push:
branches: [ develop ]
pull_request:
branches: [ develop ]
schedule:
- cron: '19 5 * * 0'
jobs:
lint:
name: DevSkim
runs-on: ubuntu-20.04
permissions:
actions: read
contents: read
security-events: write
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Run DevSkim scanner
uses: microsoft/DevSkim-Action@v1
- name: Upload DevSkim scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v1
with:
sarif_file: devskim-results.sarif

View File

@@ -9,24 +9,24 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Get Github Tag
id: prep
run: |
echo ::set-output name=version::${GITHUB_REF#refs/tags/v}
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Tactical Image
uses: docker/build-push-action@v2
with:
@@ -36,7 +36,7 @@ jobs:
file: ./docker/containers/tactical/dockerfile
platforms: linux/amd64
tags: tacticalrmm/tactical:${{ steps.prep.outputs.version }},tacticalrmm/tactical:latest
- name: Build and Push Tactical MeshCentral Image
uses: docker/build-push-action@v2
with:
@@ -46,7 +46,7 @@ jobs:
file: ./docker/containers/tactical-meshcentral/dockerfile
platforms: linux/amd64
tags: tacticalrmm/tactical-meshcentral:${{ steps.prep.outputs.version }},tacticalrmm/tactical-meshcentral:latest
- name: Build and Push Tactical NATS Image
uses: docker/build-push-action@v2
with:
@@ -56,7 +56,7 @@ jobs:
file: ./docker/containers/tactical-nats/dockerfile
platforms: linux/amd64
tags: tacticalrmm/tactical-nats:${{ steps.prep.outputs.version }},tacticalrmm/tactical-nats:latest
- name: Build and Push Tactical Frontend Image
uses: docker/build-push-action@v2
with:
@@ -66,7 +66,7 @@ jobs:
file: ./docker/containers/tactical-frontend/dockerfile
platforms: linux/amd64
tags: tacticalrmm/tactical-frontend:${{ steps.prep.outputs.version }},tacticalrmm/tactical-frontend:latest
- name: Build and Push Tactical Nginx Image
uses: docker/build-push-action@v2
with:

4
.gitignore vendored
View File

@@ -55,3 +55,7 @@ coverage.lcov
daphne.sock.lock
.pytest_cache
coverage.xml
setup_dev.yml
11env/
query_schema.json
gunicorn_config.py

23
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,23 @@
{
"recommendations": [
// frontend
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"editorconfig.editorconfig",
"vue.volar",
"wayou.vscode-todo-highlight",
// python
"matangover.mypy",
"ms-python.python",
// golang
"golang.go"
],
"unwantedRecommendations": [
"octref.vetur",
"hookyqr.beautify",
"dbaeumer.jshint",
"ms-vscode.vscode-typescript-tslint-plugin"
]
}

141
.vscode/settings.json vendored
View File

@@ -1,86 +1,59 @@
{
"python.defaultInterpreterPath": "api/tacticalrmm/env/bin/python",
"python.languageServer": "Pylance",
"python.analysis.extraPaths": [
"api/tacticalrmm",
"api/env",
],
"python.analysis.diagnosticSeverityOverrides": {
"reportUnusedImport": "error",
"reportDuplicateImport": "error",
"reportGeneralTypeIssues": "none"
"python.defaultInterpreterPath": "api/env/bin/python",
"python.languageServer": "Pylance",
"python.analysis.extraPaths": ["api/tacticalrmm", "api/env"],
"python.analysis.diagnosticSeverityOverrides": {
"reportUnusedImport": "error",
"reportDuplicateImport": "error",
"reportGeneralTypeIssues": "none",
"reportOptionalMemberAccess": "none",
},
"python.analysis.typeCheckingMode": "basic",
"editor.bracketPairColorization.enabled": true,
"editor.guides.bracketPairs": true,
"editor.formatOnSave": true,
"files.associations": {
"**/ansible/**/*.yml": "ansible",
"**/docker/**/docker-compose*.yml": "dockercompose"
},
"files.watcherExclude": {
"**/.git/objects/**": true,
"**/.git/subtree-cache/**": true,
"**/node_modules/": true,
"/node_modules/**": true,
"**/env/": true,
"/env/**": true,
"**/__pycache__": true,
"/__pycache__/**": true,
"**/.cache": true,
"**/.eggs": true,
"**/.ipynb_checkpoints": true,
"**/.mypy_cache": true,
"**/.pytest_cache": true,
"**/*.egg-info": true,
"**/*.feather": true,
"**/*.parquet*": true,
"**/*.pyc": true,
"**/*.zip": true
},
"go.useLanguageServer": true,
"[go]": {
"editor.codeActionsOnSave": {
"source.organizeImports": "never"
},
"python.analysis.typeCheckingMode": "basic",
"mypy.runUsingActiveInterpreter": true,
"python.linting.enabled": true,
"python.linting.mypyEnabled": true,
"python.linting.mypyArgs": [
"--ignore-missing-imports",
"--follow-imports=silent",
"--show-column-numbers",
"--strict"
],
"python.formatting.provider": "black",
"editor.formatOnSave": true,
"vetur.format.defaultFormatter.js": "prettier",
"vetur.format.defaultFormatterOptions": {
"prettier": {
"semi": true,
"printWidth": 120,
"tabWidth": 2,
"useTabs": false,
"arrowParens": "avoid",
}
},
"vetur.format.options.tabSize": 2,
"vetur.format.options.useTabs": false,
"files.watcherExclude": {
"files.watcherExclude": {
"**/.git/objects/**": true,
"**/.git/subtree-cache/**": true,
"**/node_modules/": true,
"/node_modules/**": true,
"**/env/": true,
"/env/**": true,
"**/__pycache__": true,
"/__pycache__/**": true,
"**/.cache": true,
"**/.eggs": true,
"**/.ipynb_checkpoints": true,
"**/.mypy_cache": true,
"**/.pytest_cache": true,
"**/*.egg-info": true,
"**/*.feather": true,
"**/*.parquet*": true,
"**/*.pyc": 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,
},
"mypy.targets": [
"api/tacticalrmm"
],
"python.linting.ignorePatterns": [
"**/site-packages/**/*.py",
".vscode/*.py",
"**env/**"
]
}
"editor.snippetSuggestions": "none"
},
"[go.mod]": {
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
}
},
"gopls": {
"usePlaceholders": true,
"completeUnimported": true,
"staticcheck": true
},
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter"
}
}

View File

@@ -7,7 +7,8 @@
Tactical RMM is a remote monitoring & management tool, built with Django and Vue.\
It uses an [agent](https://github.com/amidaware/rmmagent) written in golang and integrates with [MeshCentral](https://github.com/Ylianst/MeshCentral)
# [LIVE DEMO](https://rmm.tacticalrmm.io/)
# [LIVE DEMO](https://demo.tacticalrmm.com/)
Demo database resets every hour. A lot of features are disabled for obvious reasons due to the nature of this app.
### [Discord Chat](https://discord.gg/upGTkWp)
@@ -19,11 +20,11 @@ Demo database resets every hour. A lot of features are disabled for obvious reas
- Teamviewer-like remote desktop control
- Real-time remote shell
- Remote file browser (download and upload files)
- Remote command and script execution (batch, powershell and python scripts)
- Remote command and script execution (batch, powershell, python, nushell and deno scripts)
- Event log viewer
- Services management
- Windows patch management
- Automated checks with email/SMS alerting (cpu, disk, memory, services, scripts, event logs)
- Automated checks with email/SMS/Webhook alerting (cpu, disk, memory, services, scripts, event logs)
- Automated task runner (run scripts on a schedule)
- Remote software installation via chocolatey
- Software and hardware inventory
@@ -33,7 +34,19 @@ Demo database resets every hour. A lot of features are disabled for obvious reas
- Windows 7, 8.1, 10, 11, Server 2008R2, 2012R2, 2016, 2019, 2022
## Linux agent versions supported
- Any distro with systemd
- Any distro with systemd which includes but is not limited to: Debian (10, 11), Ubuntu x86_64 (18.04, 20.04, 22.04), Synology 7, centos, freepbx and more!
## Mac agent versions supported
- 64 bit Intel and Apple Silicon (M-Series)
## Sponsorship Features
- Mac and Linux Agents
- Windows [Code Signed](https://docs.tacticalrmm.com/code_signing/) Agents
- Fully Customizable [Reporting](https://docs.tacticalrmm.com/ee/reporting/reporting_overview/) Module
- [Single Sign-On](https://docs.tacticalrmm.com/ee/sso/sso/) (SSO)
## Installation / Backup / Restore / Usage

View File

@@ -2,10 +2,7 @@
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| 0.12.2 | :white_check_mark: |
| < 0.12.2 | :x: |
[Latest](https://github.com/amidaware/tacticalrmm/releases/latest) release
## Reporting a Vulnerability

3
ansible/README.md Normal file
View File

@@ -0,0 +1,3 @@
### tacticalrmm ansible WIP
ansible role to setup a Debian 11 VM for tacticalrmm local development

View File

@@ -0,0 +1,40 @@
---
user: "tactical"
python_ver: "3.11.8"
go_ver: "1.20.7"
backend_repo: "https://github.com/amidaware/tacticalrmm.git"
frontend_repo: "https://github.com/amidaware/tacticalrmm-web.git"
scripts_repo: "https://github.com/amidaware/community-scripts.git"
backend_dir: "/opt/trmm"
frontend_dir: "/opt/trmm-web"
scripts_dir: "/opt/trmm-community-scripts"
trmm_dir: "{{ backend_dir }}/api/tacticalrmm/tacticalrmm"
mesh_dir: "/opt/meshcentral"
settings_file: "{{ trmm_dir }}/settings.py"
local_settings_file: "{{ trmm_dir }}/local_settings.py"
fullchain_dest: /etc/ssl/certs/fullchain.pem
privkey_dest: /etc/ssl/certs/privkey.pem
base_pkgs:
- build-essential
- curl
- wget
- dirmngr
- gnupg
- openssl
- gcc
- g++
- make
- ca-certificates
- git
python_pkgs:
- zlib1g-dev
- libncurses5-dev
- libgdbm-dev
- libnss3-dev
- libssl-dev
- libreadline-dev
- libffi-dev
- libsqlite3-dev
- libbz2-dev

View File

@@ -0,0 +1,31 @@
worker_rlimit_nofile 1000000;
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 4096;
}
http {
sendfile on;
server_tokens off;
tcp_nopush on;
types_hash_max_size 2048;
server_names_hash_bucket_size 256;
include /etc/nginx/mime.types;
default_type application/octet-stream;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers EECDH+AESGCM:EDH+AESGCM;
ssl_ecdh_curve secp384r1;
ssl_stapling on;
ssl_stapling_verify on;
add_header X-Content-Type-Options nosniff;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
gzip on;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}

View File

@@ -0,0 +1,20 @@
" This file loads the default vim options at the beginning and prevents
" that they are being loaded again later. All other options that will be set,
" are added, or overwrite the default settings. Add as many options as you
" whish at the end of this file.
" Load the defaults
source $VIMRUNTIME/defaults.vim
" Prevent the defaults from being loaded again later, if the user doesn't
" have a local vimrc (~/.vimrc)
let skip_defaults_vim = 1
" Set more options (overwrites settings from /usr/share/vim/vim80/defaults.vim)
" Add as many options as you whish
" Set the mouse mode to 'r'
if has('mouse')
set mouse=r
endif

View File

@@ -0,0 +1,634 @@
---
- name: Append subdomains to hosts
tags: hosts
become: yes
ansible.builtin.lineinfile:
path: /etc/hosts
backrefs: yes
regexp: '^(127\.0\.1\.1 .*)$'
line: "\\1 {{ api }} {{ mesh }} {{ rmm }}"
- name: set mouse mode for vim
tags: vim
become: yes
ansible.builtin.copy:
src: vimrc.local
dest: /etc/vim/vimrc.local
owner: "root"
group: "root"
mode: "0644"
- name: set max_user_watches
tags: sysctl
become: yes
ansible.builtin.lineinfile:
path: /etc/sysctl.conf
line: fs.inotify.max_user_watches=524288
- name: reload sysctl
tags: sysctl
become: yes
ansible.builtin.command:
cmd: sysctl -p
- name: install base packages
tags: base
become: yes
ansible.builtin.apt:
pkg: "{{ item }}"
state: present
update_cache: yes
with_items:
- "{{ base_pkgs }}"
- name: set arch fact
ansible.builtin.set_fact:
goarch: "{{ 'amd64' if ansible_architecture == 'x86_64' else 'arm64' }}"
- name: download and install golang
tags: golang
become: yes
ansible.builtin.unarchive:
src: "https://go.dev/dl/go{{ go_ver }}.linux-{{ goarch }}.tar.gz"
dest: /usr/local
remote_src: yes
- name: add golang to path
become: yes
tags: golang
ansible.builtin.copy:
dest: /etc/profile.d/golang.sh
content: "PATH=$PATH:/usr/local/go/bin"
- name: install python prereqs
tags: python
become: yes
ansible.builtin.apt:
pkg: "{{ item }}"
state: present
with_items:
- "{{ python_pkgs }}"
- name: get cpu core count
tags: python
ansible.builtin.command: nproc
register: numprocs
- name: Create python tmpdir
tags: python
ansible.builtin.tempfile:
state: directory
suffix: python
register: python_tmp
- name: download and extract python
tags: python
ansible.builtin.unarchive:
src: "https://www.python.org/ftp/python/{{ python_ver }}/Python-{{ python_ver }}.tgz"
dest: "{{ python_tmp.path }}"
remote_src: yes
- name: compile python
tags: python
ansible.builtin.shell:
chdir: "{{ python_tmp.path }}/Python-{{ python_ver }}"
cmd: |
./configure --enable-optimizations
make -j {{ numprocs.stdout }}
- name: alt install python
tags: python
become: yes
ansible.builtin.shell:
chdir: "{{ python_tmp.path }}/Python-{{ python_ver }}"
cmd: |
make altinstall
- name: install redis
tags: redis
become: yes
ansible.builtin.apt:
pkg: redis
state: present
- name: create postgres repo
tags: postgres
become: yes
ansible.builtin.copy:
content: "deb http://apt.postgresql.org/pub/repos/apt {{ ansible_distribution_release }}-pgdg main"
dest: /etc/apt/sources.list.d/pgdg.list
owner: root
group: root
mode: "0644"
- name: import postgres repo signing key
tags: postgres
become: yes
ansible.builtin.apt_key:
url: https://www.postgresql.org/media/keys/ACCC4CF8.asc
state: present
- name: install postgresql
tags: postgres
become: yes
ansible.builtin.apt:
pkg: postgresql-15
state: present
update_cache: yes
- name: ensure postgres enabled and started
tags: postgres
become: yes
ansible.builtin.service:
name: postgresql
enabled: yes
state: started
- name: setup trmm database
tags: postgres
become: yes
become_user: postgres
ansible.builtin.shell:
cmd: |
psql -c "CREATE DATABASE tacticalrmm"
psql -c "CREATE USER {{ db_user }} WITH PASSWORD '{{ db_passwd }}'"
psql -c "ALTER ROLE {{ db_user }} SET client_encoding TO 'utf8'"
psql -c "ALTER ROLE {{ db_user }} SET default_transaction_isolation TO 'read committed'"
psql -c "ALTER ROLE {{ db_user }} SET timezone TO 'UTC'"
psql -c "ALTER ROLE {{ db_user }} CREATEDB"
psql -c "GRANT ALL PRIVILEGES ON DATABASE tacticalrmm TO {{ db_user }}"
psql -c "ALTER DATABASE tacticalrmm OWNER TO {{ db_user }}"
psql -c "GRANT USAGE, CREATE ON SCHEMA PUBLIC TO {{ db_user }}"
- name: setup mesh database
tags: postgres
become: yes
become_user: postgres
ansible.builtin.shell:
cmd: |
psql -c "CREATE DATABASE meshcentral"
psql -c "CREATE USER {{ mesh_db_user }} WITH PASSWORD '{{ mesh_db_passwd }}'"
psql -c "ALTER ROLE {{ mesh_db_user }} SET client_encoding TO 'utf8'"
psql -c "ALTER ROLE {{ mesh_db_user }} SET default_transaction_isolation TO 'read committed'"
psql -c "ALTER ROLE {{ mesh_db_user }} SET timezone TO 'UTC'"
psql -c "GRANT ALL PRIVILEGES ON DATABASE meshcentral TO {{ mesh_db_user }}"
psql -c "ALTER DATABASE meshcentral OWNER TO {{ mesh_db_user }}"
psql -c "GRANT USAGE, CREATE ON SCHEMA PUBLIC TO {{ mesh_db_user }}"
- name: create repo dirs
become: yes
tags: git
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ user }}"
group: "{{ user }}"
mode: "0755"
with_items:
- "{{ backend_dir }}"
- "{{ frontend_dir }}"
- "{{ scripts_dir }}"
- name: git clone repos
tags: git
ansible.builtin.git:
repo: "{{ item.repo }}"
dest: "{{ item.dest }}"
version: "{{ item.version }}"
with_items:
- {
repo: "{{ backend_repo }}",
dest: "{{ backend_dir }}",
version: develop,
}
- {
repo: "{{ frontend_repo }}",
dest: "{{ frontend_dir }}",
version: develop,
}
- { repo: "{{ scripts_repo }}", dest: "{{ scripts_dir }}", version: main }
- name: get nats_server_ver
tags: nats
ansible.builtin.shell: grep "^NATS_SERVER_VER" {{ settings_file }} | awk -F'[= "]' '{print $5}'
register: nats_server_ver
- name: Create nats tmpdir
tags: nats
ansible.builtin.tempfile:
state: directory
suffix: nats
register: nats_tmp
- name: download and extract nats
tags: nats
ansible.builtin.unarchive:
src: "https://github.com/nats-io/nats-server/releases/download/v{{ nats_server_ver.stdout }}/nats-server-v{{ nats_server_ver.stdout }}-linux-{{ goarch }}.tar.gz"
dest: "{{ nats_tmp.path }}"
remote_src: yes
- name: install nats
tags: nats
become: yes
ansible.builtin.copy:
remote_src: yes
src: "{{ nats_tmp.path }}/nats-server-v{{ nats_server_ver.stdout }}-linux-{{ goarch }}/nats-server"
dest: /usr/local/bin/nats-server
owner: "{{ user }}"
group: "{{ user }}"
mode: "0755"
- name: Create nodejs tmpdir
tags: nodejs
ansible.builtin.tempfile:
state: directory
suffix: nodejs
register: nodejs_tmp
- name: download nodejs setup
tags: nodejs
ansible.builtin.get_url:
url: https://deb.nodesource.com/setup_18.x
dest: "{{ nodejs_tmp.path }}/setup_node.sh"
mode: "0755"
- name: run node setup script
tags: nodejs
become: yes
ansible.builtin.command:
cmd: "{{ nodejs_tmp.path }}/setup_node.sh"
- name: install nodejs
tags: nodejs
become: yes
ansible.builtin.apt:
pkg: nodejs
state: present
update_cache: yes
- name: update npm
tags: nodejs
become: yes
ansible.builtin.shell:
cmd: npm install -g npm
- name: install quasar cli
tags: quasar
become: yes
ansible.builtin.shell:
cmd: npm install -g @quasar/cli
- name: install frontend
tags: quasar
ansible.builtin.shell:
chdir: "{{ frontend_dir }}"
cmd: npm install
- name: add quasar env
tags: quasar
ansible.builtin.template:
src: quasar.env.j2
dest: "{{ frontend_dir }}/.env"
owner: "{{ user }}"
group: "{{ user }}"
mode: "0644"
- name: remove tempdirs
tags: cleanup
become: yes
ignore_errors: yes
ansible.builtin.file:
path: "{{ item }}"
state: absent
with_items:
- "{{ nats_tmp.path }}"
- "{{ python_tmp.path }}"
- "{{ nodejs_tmp.path }}"
- name: deploy fullchain
tags: certs
become: yes
ansible.builtin.copy:
src: "{{ fullchain_src }}"
dest: "{{ fullchain_dest }}"
owner: "{{ user }}"
group: "{{ user }}"
mode: "0440"
- name: deploy privkey
tags: certs
become: yes
ansible.builtin.copy:
src: "{{ privkey_src }}"
dest: "{{ privkey_dest }}"
owner: "{{ user }}"
group: "{{ user }}"
mode: "0440"
- name: import nginx signing key
tags: nginx
become: yes
ansible.builtin.apt_key:
url: https://nginx.org/keys/nginx_signing.key
state: present
- name: add nginx repo
tags: nginx
become: yes
ansible.builtin.template:
src: nginx.repo.j2
dest: /etc/apt/sources.list.d/nginx.list
owner: "root"
group: "root"
mode: "0644"
- name: install nginx
tags: nginx
become: yes
ansible.builtin.apt:
pkg: nginx
state: present
update_cache: yes
- name: set nginx default conf
tags: nginx
become: yes
ansible.builtin.copy:
src: nginx-default.conf
dest: /etc/nginx/nginx.conf
owner: "root"
group: "root"
mode: "0644"
- name: create nginx dirs
become: yes
tags: nginx
ansible.builtin.file:
state: directory
path: "{{ item }}"
mode: "0755"
with_items:
- /etc/nginx/sites-available
- /etc/nginx/sites-enabled
- name: deploy nginx sites
become: yes
tags: nginx
ansible.builtin.template:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
mode: "0644"
owner: root
group: root
with_items:
- { src: backend.nginx.j2, dest: /etc/nginx/sites-available/backend.conf }
- { src: mesh.nginx.j2, dest: /etc/nginx/sites-available/mesh.conf }
- name: enable nginx sites
become: yes
tags: nginx
ansible.builtin.file:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
mode: "0644"
owner: root
group: root
state: link
with_items:
- {
src: /etc/nginx/sites-available/backend.conf,
dest: /etc/nginx/sites-enabled/backend.conf,
}
- {
src: /etc/nginx/sites-available/mesh.conf,
dest: /etc/nginx/sites-enabled/mesh.conf,
}
- name: ensure nginx enabled and restarted
tags: nginx
become: yes
ansible.builtin.service:
name: nginx
enabled: yes
state: restarted
- name: set natsapi fact
ansible.builtin.set_fact:
natsapi: "{{ 'nats-api' if ansible_architecture == 'x86_64' else 'nats-api-arm64' }}"
- name: copy nats-api bin
tags: nats-api
become: yes
ansible.builtin.copy:
remote_src: yes
src: "{{ backend_dir }}/natsapi/bin/{{ natsapi }}"
dest: /usr/local/bin/nats-api
owner: "{{ user }}"
group: "{{ user }}"
mode: "0755"
- name: get setuptools_ver
tags: pip
ansible.builtin.shell: grep "^SETUPTOOLS_VER" {{ settings_file }} | awk -F'[= "]' '{print $5}'
register: setuptools_ver
- name: get wheel_ver
tags: pip
ansible.builtin.shell: grep "^WHEEL_VER" {{ settings_file }} | awk -F'[= "]' '{print $5}'
register: wheel_ver
- name: setup virtual env
tags: pip
ansible.builtin.shell:
chdir: "{{ backend_dir }}/api"
cmd: python3.11 -m venv env
- name: update pip to latest
tags: pip
ansible.builtin.pip:
virtualenv: "{{ backend_dir }}/api/env"
name: pip
state: latest
- name: install setuptools and wheel
tags: pip
ansible.builtin.pip:
virtualenv: "{{ backend_dir }}/api/env"
name: "{{ item }}"
with_items:
- "setuptools=={{ setuptools_ver.stdout }}"
- "wheel=={{ wheel_ver.stdout }}"
- name: install python packages
tags: pip
ansible.builtin.pip:
virtualenv: "{{ backend_dir }}/api/env"
chdir: "{{ backend_dir }}/api/tacticalrmm"
requirements: "{{ item }}"
with_items:
- requirements.txt
- requirements-dev.txt
- requirements-test.txt
- name: deploy django local settings
tags: django
ansible.builtin.template:
src: local_settings.j2
dest: "{{ local_settings_file }}"
mode: "0644"
owner: "{{ user }}"
group: "{{ user }}"
- name: setup django
tags: django
ansible.builtin.shell:
chdir: "{{ backend_dir }}/api/tacticalrmm"
cmd: |
. ../env/bin/activate
python manage.py migrate --no-input
python manage.py collectstatic --no-input
python manage.py create_natsapi_conf
python manage.py load_chocos
python manage.py load_community_scripts
echo "from accounts.models import User; User.objects.create_superuser('{{ django_user }}', '{{ github_email }}', '{{ django_password }}') if not User.objects.filter(username='{{ django_user }}').exists() else 0;" | python manage.py shell
python manage.py create_installer_user
- name: deploy services
tags: services
become: yes
ansible.builtin.template:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
mode: "0644"
owner: "root"
group: "root"
with_items:
- { src: nats-api.systemd.j2, dest: /etc/systemd/system/nats-api.service }
- { src: nats-server.systemd.j2, dest: /etc/systemd/system/nats.service }
- { src: mesh.systemd.j2, dest: /etc/systemd/system/meshcentral.service }
- name: get mesh_ver
tags: mesh
ansible.builtin.shell: grep "^MESH_VER" {{ settings_file }} | awk -F'[= "]' '{print $5}'
register: mesh_ver
- name: create meshcentral data directory
tags: mesh
become: yes
ansible.builtin.file:
path: "{{ mesh_dir }}/meshcentral-data"
state: directory
owner: "{{ user }}"
group: "{{ user }}"
mode: "0755"
- name: install meshcentral
tags: mesh
ansible.builtin.command:
chdir: "{{ mesh_dir }}"
cmd: "npm install meshcentral@{{ mesh_ver.stdout }}"
- name: deploy mesh config
tags: mesh
ansible.builtin.template:
src: mesh.cfg.j2
dest: "{{ mesh_dir }}/meshcentral-data/config.json"
mode: "0644"
owner: "{{ user }}"
group: "{{ user }}"
- name: start meshcentral
tags: mesh
become: yes
ansible.builtin.systemd:
name: meshcentral.service
state: started
enabled: yes
daemon_reload: yes
- name: wait for meshcentral to be ready
tags: mesh
uri:
url: "https://{{ mesh }}"
return_content: yes
validate_certs: yes
status_code: 200
register: mesh_status
until: mesh_status.status == 200
retries: 20
delay: 3
- name: get meshcentral login token key
tags: mesh_key
ansible.builtin.command:
chdir: "{{ mesh_dir }}"
cmd: node node_modules/meshcentral --logintokenkey
register: mesh_token_key
- name: add mesh key to django settings file
tags: mesh_key
ansible.builtin.lineinfile:
path: "{{ local_settings_file }}"
line: 'MESH_TOKEN_KEY = "{{ mesh_token_key.stdout }}"'
- name: stop meshcentral service
tags: mesh_user
become: yes
ansible.builtin.service:
name: meshcentral.service
state: stopped
- name: create mesh user
tags: mesh_user
ansible.builtin.shell:
chdir: "{{ mesh_dir }}"
cmd: |
node node_modules/meshcentral --createaccount {{ mesh_user }} --pass {{ mesh_password }} --email {{ github_email }}
node node_modules/meshcentral --adminaccount {{ mesh_user }}
- name: start meshcentral service
tags: mesh_user
become: yes
ansible.builtin.service:
name: meshcentral.service
state: started
- name: wait for meshcentral to be ready
tags: mesh_user
uri:
url: "https://{{ mesh }}"
return_content: yes
validate_certs: yes
status_code: 200
register: mesh_status
until: mesh_status.status == 200
retries: 20
delay: 3
- name: create mesh device group
tags: mesh_user
ansible.builtin.shell:
chdir: "{{ mesh_dir }}"
cmd: |
node node_modules/meshcentral/meshctrl.js --url wss://{{ mesh }}:443 --loginuser {{ mesh_user }} --loginpass {{ mesh_password }} AddDeviceGroup --name TacticalRMM
- name: finish up django
tags: mesh_user
ansible.builtin.shell:
chdir: "{{ backend_dir }}/api/tacticalrmm"
cmd: |
. ../env/bin/activate
python manage.py initial_db_setup
python manage.py reload_nats
- name: restart services
tags: services
become: yes
ansible.builtin.systemd:
daemon_reload: yes
enabled: yes
state: restarted
name: "{{ item }}.service"
with_items:
- nats
- nats-api

View File

@@ -0,0 +1,20 @@
server {
listen 443 ssl reuseport;
listen [::]:443 ssl;
server_name {{ api }};
client_max_body_size 300M;
ssl_certificate {{ fullchain_dest }};
ssl_certificate_key {{ privkey_dest }};
location ~ ^/natsws {
proxy_pass http://127.0.0.1:9235;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-Host $host:$server_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

View File

@@ -0,0 +1,20 @@
SECRET_KEY = "{{ django_secret }}"
DEBUG = True
ALLOWED_HOSTS = ['{{ api }}']
ADMIN_URL = "admin/"
CORS_ORIGIN_ALLOW_ALL = True
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'tacticalrmm',
'USER': '{{ db_user }}',
'PASSWORD': '{{ db_passwd }}',
'HOST': 'localhost',
'PORT': '5432',
}
}
ADMIN_ENABLED = True
CERT_FILE = "{{ fullchain_dest }}"
KEY_FILE = "{{ privkey_dest }}"
MESH_USERNAME = "{{ mesh_user }}"
MESH_SITE = "https://{{ mesh }}"

View File

@@ -0,0 +1,37 @@
{
"settings": {
"Cert": "{{ mesh }}",
"WANonly": true,
"Minify": 1,
"Port": 4430,
"AliasPort": 443,
"RedirPort": 800,
"AllowLoginToken": true,
"AllowFraming": true,
"AgentPing": 35,
"AllowHighQualityDesktop": true,
"TlsOffload": "127.0.0.1",
"agentCoreDump": false,
"Compression": true,
"WsCompression": true,
"AgentWsCompression": true,
"MaxInvalidLogin": { "time": 5, "count": 5, "coolofftime": 30 },
"postgres": {
"user": "{{ mesh_db_user }}",
"password": "{{ mesh_db_passwd }}",
"port": "5432",
"host": "localhost"
}
},
"domains": {
"": {
"Title": "Tactical RMM Dev",
"Title2": "Tactical RMM Dev",
"NewAccounts": false,
"CertUrl": "https://{{ mesh }}:443/",
"GeoLocation": true,
"CookieIpCheck": false,
"mstsc": true
}
}
}

View File

@@ -0,0 +1,22 @@
server {
listen 443 ssl;
listen [::]:443 ssl;
proxy_send_timeout 330s;
proxy_read_timeout 330s;
server_name {{ mesh }};
ssl_certificate {{ fullchain_dest }};
ssl_certificate_key {{ privkey_dest }};
ssl_session_cache shared:WEBSSL:10m;
location / {
proxy_pass http://127.0.0.1:4430/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-Host $host:$server_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

View File

@@ -0,0 +1,17 @@
[Unit]
Description=MeshCentral Server
After=network.target postgresql.service nginx.service
[Service]
Type=simple
LimitNOFILE=1000000
ExecStart=/usr/bin/node node_modules/meshcentral
Environment=NODE_ENV=production
WorkingDirectory={{ mesh_dir }}
User={{ user }}
Group={{ user }}
Restart=always
RestartSec=10s
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,14 @@
[Unit]
Description=TacticalRMM Nats Api
After=nats.service
[Service]
Type=simple
ExecStart=/usr/local/bin/nats-api -config {{ backend_dir }}/api/tacticalrmm/nats-api.conf
User={{ user }}
Group={{ user }}
Restart=always
RestartSec=5s
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,18 @@
[Unit]
Description=NATS Server
After=network.target
[Service]
PrivateTmp=true
Type=simple
ExecStart=/usr/local/bin/nats-server -c {{ backend_dir }}/api/tacticalrmm/nats-rmm.conf
ExecReload=/usr/bin/kill -s HUP $MAINPID
ExecStop=/usr/bin/kill -s SIGINT $MAINPID
User={{ user }}
Group={{ user }}
Restart=always
RestartSec=5s
LimitNOFILE=1000000
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,2 @@
deb https://nginx.org/packages/debian/ {{ ansible_distribution_release }} nginx
deb-src https://nginx.org/packages/debian/ {{ ansible_distribution_release }} nginx

View File

@@ -0,0 +1,4 @@
DEV_URL = "http://{{ api }}:8000"
DEV_HOST = "0.0.0.0"
DEV_PORT = "8080"
USE_HTTPS = false

View File

@@ -0,0 +1,22 @@
---
- hosts: "{{ target }}"
vars:
ansible_user: tactical
fullchain_src: /path/to/fullchain.pem
privkey_src: /path/to/privkey.pem
api: "api.example.com"
rmm: "rmm.example.com"
mesh: "mesh.example.com"
github_username: "changeme"
github_email: "changeme@example.com"
mesh_user: "changeme"
mesh_password: "changeme"
db_user: "changeme"
db_passwd: "changeme"
mesh_db_user: "changeme"
mesh_db_passwd: "changeme"
django_secret: "changeme"
django_user: "changeme"
django_password: "changeme"
roles:
- trmm_dev

View File

@@ -1,26 +1,15 @@
[run]
source = .
[report]
show_missing = True
include = *.py
omit =
tacticalrmm/asgi.py
tacticalrmm/wsgi.py
manage.py
*/__pycache__/*
*/env/*
*/management/*
*/migrations/*
*/static/*
manage.py
*/local_settings.py
*/apps.py
*/admin.py
*/celery.py
*/wsgi.py
*/settings.py
*/baker_recipes.py
*/urls.py
*/tests.py
*/test.py
checks/utils.py
*/asgi.py
*/demo_views.py
/usr/local/lib/*
**/migrations/*
**/test*.py
[report]
show_missing = True

12
api/tacticalrmm/.flake8 Normal file
View File

@@ -0,0 +1,12 @@
[flake8]
ignore = E501,W503,E722,E203
exclude =
.mypy*
.pytest*
.git
demo_data.py
manage.py
*/__pycache__/*
*/env/*
/usr/local/lib/*
**/migrations/*

View File

@@ -1,22 +1,24 @@
import uuid
from accounts.models import User
from django.core.management.base import BaseCommand
from accounts.models import User
from tacticalrmm.helpers import make_random_password
class Command(BaseCommand):
help = "Creates the installer user"
def handle(self, *args, **kwargs):
def handle(self, *args, **kwargs): # type: ignore
self.stdout.write("Checking if installer user has been created...")
if User.objects.filter(is_installer_user=True).exists():
self.stdout.write("Installer user already exists")
return
User.objects.create_user( # type: ignore
User.objects.create_user(
username=uuid.uuid4().hex,
is_installer_user=True,
password=User.objects.make_random_password(60), # type: ignore
password=make_random_password(len=60),
block_dashboard_login=True,
)
self.stdout.write("Installer user has been created")

View File

@@ -6,7 +6,7 @@ from knox.models import AuthToken
class Command(BaseCommand):
help = "Deletes all knox web tokens"
def handle(self, *args, **kwargs):
def handle(self, *args, **kwargs): # type: ignore
# only delete web tokens, not any generated by the installer or deployments
dont_delete = djangotime.now() + djangotime.timedelta(hours=23)
tokens = AuthToken.objects.exclude(deploytokens__isnull=False).filter(

View File

@@ -1,9 +1,10 @@
import subprocess
import pyotp
from accounts.models import User
from django.core.management.base import BaseCommand
from accounts.models import User
class Command(BaseCommand):
help = "Generates barcode for Authenticator and creates totp for user"

View File

@@ -1,10 +1,12 @@
import os
import subprocess
import pyotp
from accounts.models import User
from django.conf import settings
from django.core.management.base import BaseCommand
from accounts.models import User
from tacticalrmm.util_settings import get_webdomain
class Command(BaseCommand):
help = "Reset 2fa"
@@ -20,28 +22,13 @@ class Command(BaseCommand):
self.stdout.write(self.style.ERROR(f"User {username} doesn't exist"))
return
domain = "Tactical RMM"
nginx = "/etc/nginx/sites-available/frontend.conf"
found = None
if os.path.exists(nginx):
try:
with open(nginx, "r") as f:
for line in f:
if "server_name" in line:
found = line
break
if found:
rep = found.replace("server_name", "").replace(";", "")
domain = "".join(rep.split())
except:
pass
code = pyotp.random_base32()
user.totp_key = code
user.save(update_fields=["totp_key"])
url = pyotp.totp.TOTP(code).provisioning_uri(username, issuer_name=domain)
url = pyotp.totp.TOTP(code).provisioning_uri(
username, issuer_name=get_webdomain(settings.CORS_ORIGIN_WHITELIST[0])
)
subprocess.run(f'qr "{url}"', shell=True)
self.stdout.write(
self.style.WARNING("Scan the barcode above with your authenticator app")

View File

@@ -1,6 +1,9 @@
from accounts.models import User
from getpass import getpass
from django.core.management.base import BaseCommand
from accounts.models import User
class Command(BaseCommand):
help = "Reset password for user"
@@ -16,7 +19,13 @@ class Command(BaseCommand):
self.stdout.write(self.style.ERROR(f"User {username} doesn't exist"))
return
passwd = input("Enter new password: ")
user.set_password(passwd)
pass1, pass2 = "foo", "bar"
while pass1 != pass2:
pass1 = getpass()
pass2 = getpass(prompt="Confirm Password:")
if pass1 != pass2:
self.stdout.write(self.style.ERROR("Passwords don't match"))
user.set_password(pass1)
user.save()
self.stdout.write(self.style.SUCCESS(f"Password for {username} was reset!"))

View File

@@ -0,0 +1,25 @@
# Generated by Django 4.2.1 on 2023-05-17 07:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("accounts", "0031_user_date_format"),
]
operations = [
migrations.AlterField(
model_name="user",
name="default_agent_tbl_tab",
field=models.CharField(
choices=[
("server", "Servers"),
("workstation", "Workstations"),
("mixed", "Mixed"),
],
default="mixed",
max_length=50,
),
),
]

View File

@@ -0,0 +1,32 @@
# Generated by Django 4.2.1 on 2023-05-23 04:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("accounts", "0032_alter_user_default_agent_tbl_tab"),
]
operations = [
migrations.AddField(
model_name="user",
name="dash_info_color",
field=models.CharField(default="info", max_length=255),
),
migrations.AddField(
model_name="user",
name="dash_negative_color",
field=models.CharField(default="negative", max_length=255),
),
migrations.AddField(
model_name="user",
name="dash_positive_color",
field=models.CharField(default="positive", max_length=255),
),
migrations.AddField(
model_name="user",
name="dash_warning_color",
field=models.CharField(default="warning", max_length=255),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 4.1.9 on 2023-05-26 23:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("accounts", "0033_user_dash_info_color_user_dash_negative_color_and_more"),
]
operations = [
migrations.AddField(
model_name="role",
name="can_send_wol",
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 4.2.5 on 2023-10-08 22:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("accounts", "0034_role_can_send_wol"),
]
operations = [
migrations.AddField(
model_name="role",
name="can_manage_reports",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="role",
name="can_view_reports",
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,16 @@
# Generated by Django 4.2.7 on 2023-11-09 19:57
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("accounts", "0035_role_can_manage_reports_role_can_view_reports"),
]
operations = [
migrations.RemoveField(
model_name="role",
name="can_ping_agents",
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 4.2.13 on 2024-06-28 20:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("accounts", "0036_remove_role_can_ping_agents"),
]
operations = [
migrations.AddField(
model_name="role",
name="can_run_server_scripts",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="role",
name="can_use_webterm",
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 4.2.16 on 2024-10-06 05:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("accounts", "0037_role_can_run_server_scripts_role_can_use_webterm"),
]
operations = [
migrations.AddField(
model_name="role",
name="can_edit_global_keystore",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="role",
name="can_view_global_keystore",
field=models.BooleanField(default=False),
),
]

View File

@@ -1,30 +1,18 @@
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.db.models.fields import CharField, DateTimeField
from django.core.cache import cache
from logs.models import BaseAuditModel
from typing import Optional
from tacticalrmm.constants import ROLE_CACHE_PREFIX
from allauth.socialaccount.models import SocialAccount
from django.contrib.auth.models import AbstractUser
from django.core.cache import cache
from django.db import models
from django.db.models.fields import CharField, DateTimeField
AGENT_DBLCLICK_CHOICES = [
("editagent", "Edit Agent"),
("takecontrol", "Take Control"),
("remotebg", "Remote Background"),
("urlaction", "URL Action"),
]
AGENT_TBL_TAB_CHOICES = [
("server", "Servers"),
("workstation", "Workstations"),
("mixed", "Mixed"),
]
CLIENT_TREE_SORT_CHOICES = [
("alphafail", "Move failing clients to the top"),
("alpha", "Sort alphabetically"),
]
from logs.models import BaseAuditModel
from tacticalrmm.constants import (
ROLE_CACHE_PREFIX,
AgentDblClick,
AgentTableTabs,
ClientTreeSort,
)
class User(AbstractUser, BaseAuditModel):
@@ -33,8 +21,8 @@ class User(AbstractUser, BaseAuditModel):
totp_key = models.CharField(max_length=50, null=True, blank=True)
dark_mode = 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"
agent_dblclick_action: "AgentDblClick" = models.CharField(
max_length=50, choices=AgentDblClick.choices, default=AgentDblClick.EDIT_AGENT
)
url_action = models.ForeignKey(
"core.URLAction",
@@ -44,14 +32,18 @@ class User(AbstractUser, BaseAuditModel):
on_delete=models.SET_NULL,
)
default_agent_tbl_tab = models.CharField(
max_length=50, choices=AGENT_TBL_TAB_CHOICES, default="server"
max_length=50, choices=AgentTableTabs.choices, default=AgentTableTabs.MIXED
)
agents_per_page = models.PositiveIntegerField(default=50) # not currently used
client_tree_sort = models.CharField(
max_length=50, choices=CLIENT_TREE_SORT_CHOICES, default="alphafail"
max_length=50, choices=ClientTreeSort.choices, default=ClientTreeSort.ALPHA_FAIL
)
client_tree_splitter = models.PositiveIntegerField(default=11)
loading_bar_color = models.CharField(max_length=255, default="red")
dash_info_color = models.CharField(max_length=255, default="info")
dash_positive_color = models.CharField(max_length=255, default="positive")
dash_negative_color = models.CharField(max_length=255, default="negative")
dash_warning_color = models.CharField(max_length=255, default="warning")
clear_search_when_switching = models.BooleanField(default=True)
date_format = models.CharField(max_length=30, blank=True, null=True)
is_installer_user = models.BooleanField(default=False)
@@ -73,6 +65,19 @@ class User(AbstractUser, BaseAuditModel):
on_delete=models.SET_NULL,
)
@property
def mesh_user_id(self):
return f"user//{self.mesh_username}"
@property
def mesh_username(self):
# lower() needed for mesh api
return f"{self.username.replace(' ', '').lower()}___{self.pk}"
@property
def is_sso_user(self):
return SocialAccount.objects.filter(user_id=self.pk).exists()
@staticmethod
def serialize(user):
# serializes the task and returns json
@@ -104,7 +109,6 @@ class Role(BaseAuditModel):
# agents
can_list_agents = models.BooleanField(default=False)
can_ping_agents = models.BooleanField(default=False)
can_use_mesh = models.BooleanField(default=False)
can_uninstall_agents = models.BooleanField(default=False)
can_update_agents = models.BooleanField(default=False)
@@ -118,6 +122,7 @@ class Role(BaseAuditModel):
can_run_bulk = models.BooleanField(default=False)
can_recover_agents = models.BooleanField(default=False)
can_list_agent_history = models.BooleanField(default=False)
can_send_wol = models.BooleanField(default=False)
# core
can_list_notes = models.BooleanField(default=False)
@@ -129,6 +134,10 @@ class Role(BaseAuditModel):
can_run_urlactions = models.BooleanField(default=False)
can_view_customfields = models.BooleanField(default=False)
can_manage_customfields = models.BooleanField(default=False)
can_run_server_scripts = models.BooleanField(default=False)
can_use_webterm = models.BooleanField(default=False)
can_view_global_keystore = models.BooleanField(default=False)
can_edit_global_keystore = models.BooleanField(default=False)
# checks
can_list_checks = models.BooleanField(default=False)
@@ -194,14 +203,17 @@ class Role(BaseAuditModel):
can_list_api_keys = models.BooleanField(default=False)
can_manage_api_keys = models.BooleanField(default=False)
# reporting
can_view_reports = models.BooleanField(default=False)
can_manage_reports = models.BooleanField(default=False)
def __str__(self):
return self.name
def save(self, *args, **kwargs) -> None:
# delete cache on save
cache.delete(f"{ROLE_CACHE_PREFIX}{self.name}")
super(BaseAuditModel, self).save(*args, **kwargs)
super().save(*args, **kwargs)
@staticmethod
def serialize(role):

View File

@@ -1,38 +1,38 @@
from rest_framework import permissions
from tacticalrmm.permissions import _has_perm
from tacticalrmm.utils import get_core_settings
class AccountsPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool:
if r.method == "GET":
return _has_perm(r, "can_list_accounts")
else:
# allow users to reset their own password/2fa see issue #686
base_path = "/accounts/users/"
paths = ["reset/", "reset_totp/"]
# allow users to reset their own password/2fa see issue #686
base_path = "/accounts/users/"
paths = ("reset/", "reset_totp/")
if r.path in [base_path + i for i in paths]:
from accounts.models import User
if r.path in [base_path + i for i in paths]:
from accounts.models import User
try:
user = User.objects.get(pk=r.data["id"])
except User.DoesNotExist:
pass
else:
if user == r.user:
return True
try:
user = User.objects.get(pk=r.data["id"])
except User.DoesNotExist:
pass
else:
if user == r.user:
return True
return _has_perm(r, "can_manage_accounts")
return _has_perm(r, "can_manage_accounts")
class RolesPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool:
if r.method == "GET":
return _has_perm(r, "can_list_roles")
else:
return _has_perm(r, "can_manage_roles")
return _has_perm(r, "can_manage_roles")
class APIKeyPerms(permissions.BasePermission):
@@ -41,3 +41,14 @@ class APIKeyPerms(permissions.BasePermission):
return _has_perm(r, "can_list_api_keys")
return _has_perm(r, "can_manage_api_keys")
class LocalUserPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool:
settings = get_core_settings()
return not settings.block_local_user_logon
class SelfResetSSOPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool:
return not r.user.is_sso_user

View File

@@ -1,10 +1,13 @@
import pyotp
from django.conf import settings
from rest_framework.serializers import (
ModelSerializer,
ReadOnlyField,
SerializerMethodField,
)
from tacticalrmm.util_settings import get_webdomain
from .models import APIKey, Role, User
@@ -20,6 +23,10 @@ class UserUISerializer(ModelSerializer):
"client_tree_sort",
"client_tree_splitter",
"loading_bar_color",
"dash_info_color",
"dash_positive_color",
"dash_negative_color",
"dash_warning_color",
"clear_search_when_switching",
"block_dashboard_login",
"date_format",
@@ -45,7 +52,6 @@ class UserSerializer(ModelSerializer):
class TOTPSetupSerializer(ModelSerializer):
qr_url = SerializerMethodField()
class Meta:
@@ -58,7 +64,7 @@ class TOTPSetupSerializer(ModelSerializer):
def get_qr_url(self, obj):
return pyotp.totp.TOTP(obj.totp_key).provisioning_uri(
obj.username, issuer_name="Tactical RMM"
obj.username, issuer_name=get_webdomain(settings.CORS_ORIGIN_WHITELIST[0])
)
@@ -80,7 +86,6 @@ class RoleAuditSerializer(ModelSerializer):
class APIKeySerializer(ModelSerializer):
username = ReadOnlyField(source="user.username")
class Meta:

View File

@@ -1,28 +1,30 @@
from unittest.mock import patch
from accounts.models import APIKey, User
from accounts.serializers import APIKeySerializer
from django.test import override_settings
from model_bakery import baker, seq
from accounts.models import APIKey, User
from accounts.serializers import APIKeySerializer
from tacticalrmm.constants import AgentDblClick, AgentTableTabs, ClientTreeSort
from tacticalrmm.test import TacticalTestCase
class TestAccounts(TacticalTestCase):
def setUp(self):
self.setup_coresettings()
self.setup_client()
self.bob = User(username="bob")
self.bob.set_password("hunter2")
self.bob.save()
def test_check_creds(self):
url = "/checkcreds/"
url = "/v2/checkcreds/"
data = {"username": "bob", "password": "hunter2"}
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
self.assertIn("totp", r.data.keys())
self.assertEqual(r.data["totp"], "totp not set")
self.assertEqual(r.data["totp"], False)
data = {"username": "bob", "password": "a3asdsa2314"}
r = self.client.post(url, data, format="json")
@@ -39,7 +41,7 @@ class TestAccounts(TacticalTestCase):
data = {"username": "bob", "password": "hunter2"}
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data, "ok")
self.assertEqual(r.data["totp"], True)
# test user set to block dashboard logins
self.bob.block_dashboard_login = True
@@ -49,7 +51,7 @@ class TestAccounts(TacticalTestCase):
@patch("pyotp.TOTP.verify")
def test_login_view(self, mock_verify):
url = "/login/"
url = "/v2/login/"
mock_verify.return_value = True
data = {"username": "bob", "password": "hunter2", "twofactor": "123456"}
@@ -196,7 +198,7 @@ class GetUpdateDeleteUser(TacticalTestCase):
r = self.client.delete(url)
self.assertEqual(r.status_code, 200)
url = f"/accounts/893452/users/"
url = "/accounts/893452/users/"
r = self.client.delete(url)
self.assertEqual(r.status_code, 404)
@@ -283,9 +285,9 @@ class TestUserAction(TacticalTestCase):
data = {
"dark_mode": True,
"show_community_scripts": True,
"agent_dblclick_action": "editagent",
"default_agent_tbl_tab": "mixed",
"client_tree_sort": "alpha",
"agent_dblclick_action": AgentDblClick.EDIT_AGENT,
"default_agent_tbl_tab": AgentTableTabs.MIXED,
"client_tree_sort": ClientTreeSort.ALPHA,
"client_tree_splitter": 14,
"loading_bar_color": "green",
"clear_search_when_switching": False,
@@ -296,6 +298,27 @@ class TestUserAction(TacticalTestCase):
self.check_not_authenticated("patch", url)
class TestUserReset(TacticalTestCase):
def setUp(self):
self.authenticate()
self.setup_coresettings()
def test_reset_pw(self):
url = "/accounts/resetpw/"
data = {"password": "superSekret123456"}
r = self.client.put(url, data, format="json")
self.assertEqual(r.status_code, 200)
self.check_not_authenticated("put", url)
def test_reset_2fa(self):
url = "/accounts/reset2fa/"
r = self.client.put(url)
self.assertEqual(r.status_code, 200)
self.check_not_authenticated("put", url)
class TestAPIKeyViews(TacticalTestCase):
def setUp(self):
self.setup_coresettings()
@@ -382,7 +405,7 @@ class TestTOTPSetup(TacticalTestCase):
r = self.client.post(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data, "totp token already set")
self.assertEqual(r.data, False)
class TestAPIAuthentication(TacticalTestCase):

View File

@@ -5,6 +5,10 @@ from . import views
urlpatterns = [
path("users/", views.GetAddUsers.as_view()),
path("<int:pk>/users/", views.GetUpdateDeleteUser.as_view()),
path("sessions/<str:pk>/", views.DeleteActiveLoginSession.as_view()),
path(
"users/<int:pk>/sessions/", views.GetDeleteActiveLoginSessionsPerUser.as_view()
),
path("users/reset/", views.UserActions.as_view()),
path("users/reset_totp/", views.UserActions.as_view()),
path("users/setup_totp/", views.TOTPSetup.as_view()),
@@ -13,4 +17,6 @@ urlpatterns = [
path("roles/<int:pk>/", views.GetUpdateDeleteRole.as_view()),
path("apikeys/", views.GetAddAPIKeys.as_view()),
path("apikeys/<int:pk>/", views.GetUpdateDeleteAPIKey.as_view()),
path("resetpw/", views.ResetPass.as_view()),
path("reset2fa/", views.Reset2FA.as_view()),
]

View File

@@ -0,0 +1,24 @@
from typing import TYPE_CHECKING
from django.conf import settings
if TYPE_CHECKING:
from django.http import HttpRequest
from accounts.models import User
def is_root_user(*, request: "HttpRequest", user: "User") -> bool:
root = (
hasattr(settings, "ROOT_USER")
and request.user != user
and user.username == settings.ROOT_USER
)
demo = (
getattr(settings, "DEMO", False) and request.user.username == settings.ROOT_USER
)
return root or demo
def is_superuser(user: "User") -> bool:
return user.role and getattr(user.role, "is_superuser")

View File

@@ -1,20 +1,39 @@
import datetime
import pyotp
from allauth.socialaccount.models import SocialAccount, SocialApp
from django.conf import settings
from django.contrib.auth import login
from django.db import IntegrityError
from django.shortcuts import get_object_or_404
from ipware import get_client_ip
from django.utils import timezone as djangotime
from knox.models import AuthToken
from knox.views import LoginView as KnoxLoginView
from logs.models import AuditLog
from python_ipware import IpWare
from rest_framework.authtoken.serializers import AuthTokenSerializer
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
from rest_framework.serializers import (
ModelSerializer,
ReadOnlyField,
SerializerMethodField,
)
from rest_framework.views import APIView
from tacticalrmm.utils import notify_error
from accounts.utils import is_root_user
from core.tasks import sync_mesh_perms_task
from logs.models import AuditLog
from tacticalrmm.helpers import notify_error
from tacticalrmm.utils import get_core_settings
from .models import APIKey, Role, User
from .permissions import AccountsPerms, APIKeyPerms, RolesPerms
from .permissions import (
AccountsPerms,
APIKeyPerms,
LocalUserPerms,
RolesPerms,
SelfResetSSOPerms,
)
from .serializers import (
APIKeySerializer,
RoleSerializer,
@@ -24,24 +43,14 @@ from .serializers import (
)
def _is_root_user(request, user) -> bool:
root = (
hasattr(settings, "ROOT_USER")
and request.user != user
and user.username == settings.ROOT_USER
)
demo = (
getattr(settings, "DEMO", False) and request.user.username == settings.ROOT_USER
)
return root or demo
class CheckCreds(KnoxLoginView):
class CheckCredsV2(KnoxLoginView):
permission_classes = (AllowAny,)
def post(self, request, format=None):
# restrict time on tokens issued by this view to 3 min
def get_token_ttl(self):
return datetime.timedelta(seconds=180)
def post(self, request, format=None):
# check credentials
serializer = AuthTokenSerializer(data=request.data)
if not serializer.is_valid():
@@ -52,21 +61,25 @@ class CheckCreds(KnoxLoginView):
user = serializer.validated_data["user"]
if user.block_dashboard_login:
if user.block_dashboard_login or user.is_sso_user:
return notify_error("Bad credentials")
# block local logon if configured
core_settings = get_core_settings()
if not user.is_superuser and core_settings.block_local_user_logon:
return notify_error("Bad credentials")
# if totp token not set modify response to notify frontend
if not user.totp_key:
login(request, user)
response = super(CheckCreds, self).post(request, format=None)
response.data["totp"] = "totp not set"
response = super().post(request, format=None)
response.data["totp"] = False
return response
return Response("ok")
return Response({"totp": True})
class LoginView(KnoxLoginView):
class LoginViewV2(KnoxLoginView):
permission_classes = (AllowAny,)
def post(self, request, format=None):
@@ -79,6 +92,14 @@ class LoginView(KnoxLoginView):
if user.block_dashboard_login:
return notify_error("Bad credentials")
# block local logon if configured
core_settings = get_core_settings()
if not user.is_superuser and core_settings.block_local_user_logon:
return notify_error("Bad credentials")
if user.is_sso_user:
return notify_error("Bad credentials")
token = request.data["twofactor"]
totp = pyotp.TOTP(user.totp_key)
@@ -93,14 +114,20 @@ class LoginView(KnoxLoginView):
login(request, user)
# save ip information
client_ip, is_routable = get_client_ip(request)
user.last_login_ip = client_ip
user.save()
ipw = IpWare()
client_ip, _ = ipw.get_client_ip(request.META)
if client_ip:
user.last_login_ip = str(client_ip)
user.save()
AuditLog.audit_user_login_successful(
request.data["username"], debug_info={"ip": request._client_ip}
)
return super(LoginView, self).post(request, format=None)
response = super().post(request, format=None)
response.data["username"] = request.user.username
response.data["name"] = None
return Response(response.data)
else:
AuditLog.audit_user_failed_twofactor(
request.data["username"], debug_info={"ip": request._client_ip}
@@ -108,9 +135,100 @@ class LoginView(KnoxLoginView):
return notify_error("Bad credentials")
class GetDeleteActiveLoginSessionsPerUser(APIView):
permission_classes = [IsAuthenticated, AccountsPerms]
class TokenSerializer(ModelSerializer):
user = ReadOnlyField(source="user.username")
class Meta:
model = AuthToken
fields = (
"digest",
"user",
"created",
"expiry",
)
def get(self, request, pk):
tokens = get_object_or_404(User, pk=pk).auth_token_set.filter(
expiry__gt=djangotime.now()
)
return Response(self.TokenSerializer(tokens, many=True).data)
def delete(self, request, pk):
tokens = get_object_or_404(User, pk=pk).auth_token_set.filter(
expiry__gt=djangotime.now()
)
tokens.delete()
return Response("ok")
class DeleteActiveLoginSession(APIView):
permission_classes = [IsAuthenticated, AccountsPerms]
def delete(self, request, pk):
token = get_object_or_404(AuthToken, digest=pk)
token.delete()
return Response("ok")
class GetAddUsers(APIView):
permission_classes = [IsAuthenticated, AccountsPerms]
class UserSerializerSSO(ModelSerializer):
social_accounts = SerializerMethodField()
def get_social_accounts(self, obj):
accounts = SocialAccount.objects.filter(user_id=obj.pk)
if accounts:
social_accounts = []
for account in accounts:
try:
provider_account = account.get_provider_account()
display = provider_account.to_str()
except SocialApp.DoesNotExist:
display = "Orphaned Provider"
except Exception:
display = "Unknown"
social_accounts.append(
{
"uid": account.uid,
"provider": account.provider,
"display": display,
"last_login": account.last_login,
"date_joined": account.date_joined,
"extra_data": account.extra_data,
}
)
return social_accounts
return []
class Meta:
model = User
fields = [
"id",
"username",
"first_name",
"last_name",
"email",
"is_active",
"last_login",
"last_login_ip",
"role",
"block_dashboard_login",
"date_format",
"social_accounts",
]
def get(self, request):
search = request.GET.get("search", None)
@@ -121,7 +239,7 @@ class GetAddUsers(APIView):
else:
users = User.objects.filter(agent=None, is_installer_user=False)
return Response(UserSerializer(users, many=True).data)
return Response(self.UserSerializerSSO(users, many=True).data)
def post(self, request):
# add new user
@@ -145,6 +263,7 @@ class GetAddUsers(APIView):
user.role = role
user.save()
sync_mesh_perms_task.delay()
return Response(user.username)
@@ -159,31 +278,33 @@ class GetUpdateDeleteUser(APIView):
def put(self, request, pk):
user = get_object_or_404(User, pk=pk)
if _is_root_user(request, user):
if is_root_user(request=request, user=user):
return notify_error("The root user cannot be modified from the UI")
serializer = UserSerializer(instance=user, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
sync_mesh_perms_task.delay()
return Response("ok")
def delete(self, request, pk):
user = get_object_or_404(User, pk=pk)
if _is_root_user(request, user):
if is_root_user(request=request, user=user):
return notify_error("The root user cannot be deleted from the UI")
user.delete()
sync_mesh_perms_task.delay()
return Response("ok")
class UserActions(APIView):
permission_classes = [IsAuthenticated, AccountsPerms]
permission_classes = [IsAuthenticated, AccountsPerms, LocalUserPerms]
# reset password
def post(self, request):
user = get_object_or_404(User, pk=request.data["id"])
if _is_root_user(request, user):
if is_root_user(request=request, user=user):
return notify_error("The root user cannot be modified from the UI")
user.set_password(request.data["password"])
@@ -194,7 +315,7 @@ class UserActions(APIView):
# reset two factor token
def put(self, request):
user = get_object_or_404(User, pk=request.data["id"])
if _is_root_user(request, user):
if is_root_user(request=request, user=user):
return notify_error("The root user cannot be modified from the UI")
user.totp_key = ""
@@ -206,10 +327,8 @@ class UserActions(APIView):
class TOTPSetup(APIView):
# totp setup
def post(self, request):
user = request.user
if not user.totp_key:
code = pyotp.random_base32()
@@ -217,7 +336,7 @@ class TOTPSetup(APIView):
user.save(update_fields=["totp_key"])
return Response(TOTPSetupSerializer(user).data)
return Response("totp token already set")
return Response(False)
class UserUI(APIView):
@@ -256,11 +375,13 @@ class GetUpdateDeleteRole(APIView):
serializer = RoleSerializer(instance=role, data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
sync_mesh_perms_task.delay()
return Response("Role was edited")
def delete(self, request, pk):
role = get_object_or_404(Role, pk=pk)
role.delete()
sync_mesh_perms_task.delay()
return Response("Role was removed")
@@ -278,7 +399,7 @@ class GetAddAPIKeys(APIView):
request.data["key"] = get_random_string(length=32).upper()
serializer = APIKeySerializer(data=request.data)
serializer.is_valid(raise_exception=True)
obj = serializer.save()
serializer.save()
return Response("The API Key was added")
@@ -301,3 +422,23 @@ class GetUpdateDeleteAPIKey(APIView):
apikey = get_object_or_404(APIKey, pk=pk)
apikey.delete()
return Response("The API Key was deleted")
class ResetPass(APIView):
permission_classes = [IsAuthenticated, SelfResetSSOPerms]
def put(self, request):
user = request.user
user.set_password(request.data["password"])
user.save()
return Response("Password was reset.")
class Reset2FA(APIView):
permission_classes = [IsAuthenticated, SelfResetSSOPerms]
def put(self, request):
user = request.user
user.totp_key = ""
user.save()
return Response("2FA was reset. Log out and back in to setup.")

View File

@@ -1,6 +1,6 @@
import json
import os
import random
import secrets
import string
from itertools import cycle
@@ -8,10 +8,11 @@ from django.conf import settings
from django.utils import timezone as djangotime
from model_bakery.recipe import Recipe, foreign_key, seq
from tacticalrmm.constants import AgentMonType, AgentPlat
def generate_agent_id(hostname):
rand = "".join(random.choice(string.ascii_letters) for _ in range(35))
return f"{rand}-{hostname}"
def generate_agent_id() -> str:
return "".join(secrets.choice(string.ascii_letters) for i in range(39))
site = Recipe("clients.Site")
@@ -24,26 +25,34 @@ def get_wmi_data():
return json.load(f)
def get_win_svcs():
svcs = settings.BASE_DIR.joinpath("tacticalrmm/test_data/winsvcs.json")
with open(svcs) as f:
return json.load(f)
agent = Recipe(
"agents.Agent",
site=foreign_key(site),
hostname="DESKTOP-TEST123",
version="1.3.0",
monitoring_type=cycle(["workstation", "server"]),
agent_id=seq(generate_agent_id("DESKTOP-TEST123")),
monitoring_type=cycle(AgentMonType.values),
agent_id=seq(generate_agent_id()),
last_seen=djangotime.now() - djangotime.timedelta(days=5),
plat="windows",
plat=AgentPlat.WINDOWS,
)
server_agent = agent.extend(
monitoring_type="server",
monitoring_type=AgentMonType.SERVER,
)
workstation_agent = agent.extend(
monitoring_type="workstation",
monitoring_type=AgentMonType.WORKSTATION,
)
online_agent = agent.extend(last_seen=djangotime.now())
online_agent = agent.extend(
last_seen=djangotime.now(), services=get_win_svcs(), wmi_detail=get_wmi_data()
)
offline_agent = agent.extend(
last_seen=djangotime.now() - djangotime.timedelta(minutes=7)
@@ -78,4 +87,4 @@ agent_with_services = agent.extend(
],
)
agent_with_wmi = agent.extend(wmi=get_wmi_data())
agent_with_wmi = agent.extend(wmi_detail=get_wmi_data())

View File

@@ -0,0 +1,82 @@
from agents.models import Agent, AgentHistory
from channels.db import database_sync_to_async
from channels.generic.websocket import AsyncJsonWebsocketConsumer
from django.contrib.auth.models import AnonymousUser
from django.shortcuts import get_object_or_404
from tacticalrmm.constants import AGENT_DEFER, AgentHistoryType
from tacticalrmm.permissions import _has_perm_on_agent
class SendCMD(AsyncJsonWebsocketConsumer):
async def connect(self):
self.user = self.scope["user"]
if isinstance(self.user, AnonymousUser):
await self.close()
await self.accept()
async def receive_json(self, payload, **kwargs):
auth = await self.has_perm(payload["agent_id"])
if not auth:
await self.send_json(
{"ret": "You do not have permission to perform this action."}
)
return
agent = await self.get_agent(payload["agent_id"])
timeout = int(payload["timeout"])
if payload["shell"] == "custom" and payload["custom_shell"]:
shell = payload["custom_shell"]
else:
shell = payload["shell"]
hist_pk = await self.get_history_id(agent, payload["cmd"])
data = {
"func": "rawcmd",
"timeout": timeout,
"payload": {
"command": payload["cmd"],
"shell": shell,
},
"id": hist_pk,
}
ret = await agent.nats_cmd(data, timeout=timeout + 2)
await self.send_json({"ret": ret})
async def disconnect(self, _):
pass
def _has_perm(self, perm: str) -> bool:
if self.user.is_superuser or (
self.user.role and getattr(self.user.role, "is_superuser")
):
return True
# make sure non-superusers with empty roles aren't permitted
elif not self.user.role:
return False
return self.user.role and getattr(self.user.role, perm)
@database_sync_to_async # type: ignore
def get_agent(self, agent_id: str) -> "Agent":
return get_object_or_404(Agent.objects.defer(*AGENT_DEFER), agent_id=agent_id)
@database_sync_to_async # type: ignore
def get_history_id(self, agent: "Agent", cmd: str) -> int:
hist = AgentHistory.objects.create(
agent=agent,
type=AgentHistoryType.CMD_RUN,
command=cmd,
username=self.user.username[:50],
)
return hist.pk
@database_sync_to_async # type: ignore
def has_perm(self, agent_id: str) -> bool:
return self._has_perm("can_send_cmd") and _has_perm_on_agent(
self.user, agent_id
)

View File

@@ -1,6 +1,7 @@
from django.core.management.base import BaseCommand
from agents.models import Agent
from clients.models import Client, Site
from django.core.management.base import BaseCommand
class Command(BaseCommand):

View File

@@ -1,16 +1,16 @@
import asyncio
from agents.models import Agent
from django.core.management.base import BaseCommand
from django.utils import timezone as djangotime
from packaging import version as pyver
from agents.models import Agent
from tacticalrmm.constants import AGENT_DEFER
from tacticalrmm.utils import reload_nats
class Command(BaseCommand):
help = "Delete old agents"
help = "Delete multiple agents based on criteria"
def add_arguments(self, parser):
parser.add_argument(
@@ -23,6 +23,21 @@ class Command(BaseCommand):
type=str,
help="Delete agents that equal to or less than this version",
)
parser.add_argument(
"--site",
type=str,
help="Delete agents that belong to the specified site",
)
parser.add_argument(
"--client",
type=str,
help="Delete agents that belong to the specified client",
)
parser.add_argument(
"--hostname",
type=str,
help="Delete agents with hostname starting with argument",
)
parser.add_argument(
"--delete",
action="store_true",
@@ -32,25 +47,40 @@ class Command(BaseCommand):
def handle(self, *args, **kwargs):
days = kwargs["days"]
agentver = kwargs["agentver"]
site = kwargs["site"]
client = kwargs["client"]
hostname = kwargs["hostname"]
delete = kwargs["delete"]
if not days and not agentver:
if not days and not agentver and not site and not client and not hostname:
self.stdout.write(
self.style.ERROR("Must have at least one parameter: days or agentver")
self.style.ERROR(
"Must have at least one parameter: days, agentver, site, client or hostname"
)
)
return
q = Agent.objects.defer(*AGENT_DEFER)
agents = Agent.objects.select_related("site__client").defer(*AGENT_DEFER)
agents = []
if days:
overdue = djangotime.now() - djangotime.timedelta(days=days)
agents = [i for i in q if i.last_seen < overdue]
agents = agents.filter(last_seen__lt=overdue)
if site:
agents = agents.filter(site__name=site)
if client:
agents = agents.filter(site__client__name=client)
if hostname:
agents = agents.filter(hostname__istartswith=hostname)
if agentver:
agents = [i for i in q if pyver.parse(i.version) <= pyver.parse(agentver)]
agents = [
i for i in agents if pyver.parse(i.version) <= pyver.parse(agentver)
]
if not agents:
if len(agents) == 0:
self.stdout.write(self.style.ERROR("No agents matched"))
return
@@ -64,7 +94,7 @@ class Command(BaseCommand):
try:
agent.delete()
except Exception as e:
err = f"Failed to delete agent {agent.hostname}: {str(e)}"
err = f"Failed to delete agent {agent.hostname}: {e}"
self.stdout.write(self.style.ERROR(err))
else:
deleted_count += 1

View File

@@ -1,17 +1,17 @@
# import datetime as dt
import random
from agents.models import Agent
from core.tasks import cache_db_fields_task, handle_resolved_stuff
from django.core.management.base import BaseCommand
from django.utils import timezone as djangotime
from agents.models import Agent
from core.tasks import cache_db_fields_task
class Command(BaseCommand):
help = "stuff for demo site in cron"
def handle(self, *args, **kwargs):
random_dates = []
now = djangotime.now()
@@ -23,18 +23,9 @@ class Command(BaseCommand):
rand = now - djangotime.timedelta(minutes=random.randint(10, 20))
random_dates.append(rand)
""" for _ in range(5):
rand = djangotime.now() - djangotime.timedelta(hours=random.randint(1, 10))
random_dates.append(rand)
for _ in range(5):
rand = djangotime.now() - djangotime.timedelta(days=random.randint(40, 90))
random_dates.append(rand) """
agents = Agent.objects.only("last_seen")
for agent in agents:
agent.last_seen = random.choice(random_dates)
agent.save(update_fields=["last_seen"])
cache_db_fields_task()
handle_resolved_stuff()

View File

@@ -3,28 +3,55 @@ import json
import random
import string
from accounts.models import User
from agents.models import Agent, AgentHistory
from automation.models import Policy
from autotasks.models import AutomatedTask, TaskResult
from checks.models import Check, CheckResult, CheckHistory
from clients.models import Client, Site
from django.conf import settings
from django.core.management import call_command
from django.core.management.base import BaseCommand
from django.utils import timezone as djangotime
from accounts.models import User
from agents.models import Agent, AgentHistory
from automation.models import Policy
from autotasks.models import AutomatedTask, TaskResult
from checks.models import Check, CheckHistory, CheckResult
from clients.models import Client, Site
from logs.models import AuditLog, PendingAction
from scripts.models import Script
from software.models import InstalledSoftware
from winupdate.models import WinUpdate, WinUpdatePolicy
from tacticalrmm.constants import (
AgentHistoryType,
AgentMonType,
AgentPlat,
AlertSeverity,
CheckStatus,
CheckType,
EvtLogFailWhen,
EvtLogNames,
EvtLogTypes,
GoArch,
PAAction,
ScriptShell,
TaskSyncStatus,
TaskType,
)
from tacticalrmm.demo_data import (
check_network_loc_aware_ps1,
check_storage_pool_health_ps1,
clear_print_spool_bat,
disks,
disks_linux_deb,
disks_linux_pi,
ping_fail_output,
ping_success_output,
restart_nla_ps1,
show_temp_dir_py,
spooler_stdout,
temp_dir_stdout,
wmi_deb,
wmi_pi,
wmi_mac,
disks_mac,
)
from winupdate.models import WinUpdate, WinUpdatePolicy
AGENTS_TO_GENERATE = 250
@@ -43,19 +70,18 @@ EVT_LOG_FAIL = settings.BASE_DIR.joinpath(
class Command(BaseCommand):
help = "populate database with fake agents"
def rand_string(self, length):
def rand_string(self, length: int) -> str:
chars = string.ascii_letters
return "".join(random.choice(chars) for _ in range(length))
def handle(self, *args, **kwargs):
def handle(self, *args, **kwargs) -> None:
user = User.objects.first()
if user:
user.totp_key = "ABSA234234"
user.save(update_fields=["totp_key"])
Client.objects.all().delete()
Agent.objects.all().delete()
Client.objects.all().delete()
Check.objects.all().delete()
Script.objects.all().delete()
AutomatedTask.objects.all().delete()
@@ -65,6 +91,9 @@ class Command(BaseCommand):
PendingAction.objects.all().delete()
call_command("load_community_scripts")
call_command("initial_db_setup")
call_command("load_chocos")
call_command("create_installer_user")
# policies
check_policy = Policy()
@@ -95,27 +124,27 @@ class Command(BaseCommand):
update_policy.email_if_fail = True
update_policy.save()
clients = [
clients = (
"Company 1",
"Company 2",
"Company 3",
"Company 1",
"Company 4",
"Company 5",
"Company 6",
]
sites1 = ["HQ1", "LA Office 1", "NY Office 1"]
sites2 = ["HQ2", "LA Office 2", "NY Office 2"]
sites3 = ["HQ3", "LA Office 3", "NY Office 3"]
sites4 = ["HQ4", "LA Office 4", "NY Office 4"]
sites5 = ["HQ5", "LA Office 5", "NY Office 5"]
sites6 = ["HQ6", "LA Office 6", "NY Office 6"]
)
sites1 = ("HQ1", "LA Office 1", "NY Office 1")
sites2 = ("HQ2", "LA Office 2", "NY Office 2")
sites3 = ("HQ3", "LA Office 3", "NY Office 3")
sites4 = ("HQ4", "LA Office 4", "NY Office 4")
sites5 = ("HQ5", "LA Office 5", "NY Office 5")
sites6 = ("HQ6", "LA Office 6", "NY Office 6")
client1 = Client(name="Company 1")
client2 = Client(name="Company 2")
client3 = Client(name="Company 3")
client4 = Client(name="Company 4")
client5 = Client(name="Company 5")
client6 = Client(name="Company 6")
client1 = Client(name=clients[0])
client2 = Client(name=clients[1])
client3 = Client(name=clients[2])
client4 = Client(name=clients[3])
client5 = Client(name=clients[4])
client6 = Client(name=clients[5])
client1.save()
client2.save()
@@ -142,7 +171,7 @@ class Command(BaseCommand):
for site in sites6:
Site(client=client6, name=site).save()
hostnames = [
hostnames = (
"DC-1",
"DC-2",
"FSV-1",
@@ -150,26 +179,33 @@ class Command(BaseCommand):
"WSUS",
"DESKTOP-12345",
"LAPTOP-55443",
]
descriptions = ["Bob's computer", "Primary DC", "File Server", "Karen's Laptop"]
modes = ["server", "workstation"]
op_systems_servers = [
"db-aws-01",
"Karens-MacBook-Air.local",
)
descriptions = ("Bob's computer", "Primary DC", "File Server", "Karen's Laptop")
modes = AgentMonType.values
op_systems_servers = (
"Microsoft Windows Server 2016 Standard, 64bit (build 14393)",
"Microsoft Windows Server 2012 R2 Standard, 64bit (build 9600)",
"Microsoft Windows Server 2019 Standard, 64bit (build 17763)",
]
)
op_systems_workstations = [
op_systems_workstations = (
"Microsoft Windows 8.1 Pro, 64bit (build 9600)",
"Microsoft Windows 10 Pro for Workstations, 64bit (build 18363)",
"Microsoft Windows 10 Pro, 64bit (build 18363)",
]
)
public_ips = ["65.234.22.4", "74.123.43.5", "44.21.134.45"]
linux_deb_os = "Debian 11.2 x86_64 5.10.0-11-amd64"
linux_pi_os = "Raspbian 11.2 armv7l 5.10.92-v7+"
mac_os = "Darwin 12.5.1 arm64 21.6.0"
total_rams = [4, 8, 16, 32, 64, 128]
public_ips = ("65.234.22.4", "74.123.43.5", "44.21.134.45")
total_rams = (4, 8, 16, 32, 64, 128)
now = dt.datetime.now()
django_now = djangotime.now()
boot_times = []
@@ -181,7 +217,7 @@ class Command(BaseCommand):
rand_days = now - dt.timedelta(days=random.randint(2, 50))
boot_times.append(str(rand_days.timestamp()))
user_names = ["None", "Karen", "Steve", "jsmith", "jdoe"]
user_names = ("None", "Karen", "Steve", "jsmith", "jdoe")
with open(SVCS) as f:
services = json.load(f)
@@ -196,10 +232,7 @@ class Command(BaseCommand):
with open(WMI_3) as f:
wmi3 = json.load(f)
wmi_details = []
wmi_details.append(wmi1)
wmi_details.append(wmi2)
wmi_details.append(wmi3)
wmi_details = [i for i in (wmi1, wmi2, wmi3)]
# software
with open(SW_1) as f:
@@ -208,9 +241,7 @@ class Command(BaseCommand):
with open(SW_2) as f:
software2 = json.load(f)
softwares = []
softwares.append(software1)
softwares.append(software2)
softwares = [i for i in (software1, software2)]
# windows updates
with open(WIN_UPDATES) as f:
@@ -226,74 +257,103 @@ class Command(BaseCommand):
clear_spool.name = "Clear Print Spooler"
clear_spool.description = "clears the print spooler. Fuck printers"
clear_spool.filename = "clear_print_spool.bat"
clear_spool.shell = "cmd"
clear_spool.shell = ScriptShell.CMD
clear_spool.script_body = clear_print_spool_bat
clear_spool.save()
check_net_aware = Script()
check_net_aware.name = "Check Network Location Awareness"
check_net_aware.description = "Check's network location awareness on domain computers, should always be domain profile and not public or private. Sometimes happens when computer restarts before domain available. This script will return 0 if check passes or 1 if it fails."
check_net_aware.filename = "check_network_loc_aware.ps1"
check_net_aware.shell = "powershell"
check_net_aware.shell = ScriptShell.POWERSHELL
check_net_aware.script_body = check_network_loc_aware_ps1
check_net_aware.save()
check_pool_health = Script()
check_pool_health.name = "Check storage spool health"
check_pool_health.description = "loops through all storage pools and will fail if any of them are not healthy"
check_pool_health.filename = "check_storage_pool_health.ps1"
check_pool_health.shell = "powershell"
check_pool_health.shell = ScriptShell.POWERSHELL
check_pool_health.script_body = check_storage_pool_health_ps1
check_pool_health.save()
restart_nla = Script()
restart_nla.name = "Restart NLA Service"
restart_nla.description = "restarts the Network Location Awareness windows service to fix the nic profile. Run this after the check network service fails"
restart_nla.filename = "restart_nla.ps1"
restart_nla.shell = "powershell"
restart_nla.shell = ScriptShell.POWERSHELL
restart_nla.script_body = restart_nla_ps1
restart_nla.save()
show_tmp_dir_script = Script()
show_tmp_dir_script.name = "Check temp dir"
show_tmp_dir_script.description = "shows files in temp dir using python"
show_tmp_dir_script.filename = "show_temp_dir.py"
show_tmp_dir_script.shell = "python"
show_tmp_dir_script.shell = ScriptShell.PYTHON
show_tmp_dir_script.script_body = show_temp_dir_py
show_tmp_dir_script.save()
for count_agents in range(AGENTS_TO_GENERATE):
client = random.choice(clients)
if client == "Company 1":
if client == clients[0]:
site = random.choice(sites1)
elif client == "Company 2":
elif client == clients[1]:
site = random.choice(sites2)
elif client == "Company 3":
elif client == clients[2]:
site = random.choice(sites3)
elif client == "Company 4":
elif client == clients[3]:
site = random.choice(sites4)
elif client == "Company 5":
elif client == clients[4]:
site = random.choice(sites5)
elif client == "Company 6":
elif client == clients[5]:
site = random.choice(sites6)
else:
site = None
agent = Agent()
mode = random.choice(modes)
if mode == "server":
agent.operating_system = random.choice(op_systems_servers)
plat_pick = random.randint(1, 15)
if plat_pick in (7, 11):
agent.plat = AgentPlat.LINUX
mode = AgentMonType.SERVER
# pi arm
if plat_pick == 7:
agent.goarch = GoArch.ARM32
agent.wmi_detail = wmi_pi
agent.disks = disks_linux_pi
agent.operating_system = linux_pi_os
else:
agent.goarch = GoArch.AMD64
agent.wmi_detail = wmi_deb
agent.disks = disks_linux_deb
agent.operating_system = linux_deb_os
elif plat_pick in (4, 14):
agent.plat = AgentPlat.DARWIN
mode = random.choice([AgentMonType.SERVER, AgentMonType.WORKSTATION])
agent.goarch = GoArch.ARM64
agent.wmi_detail = wmi_mac
agent.disks = disks_mac
agent.operating_system = mac_os
else:
agent.operating_system = random.choice(op_systems_workstations)
agent.plat = AgentPlat.WINDOWS
agent.goarch = GoArch.AMD64
mode = random.choice(modes)
agent.wmi_detail = random.choice(wmi_details)
agent.services = services
agent.disks = random.choice(disks)
if mode == AgentMonType.SERVER:
agent.operating_system = random.choice(op_systems_servers)
else:
agent.operating_system = random.choice(op_systems_workstations)
agent.hostname = random.choice(hostnames)
agent.version = settings.LATEST_AGENT_VER
agent.hostname = random.choice(hostnames)
agent.site = Site.objects.get(name=site)
agent.agent_id = self.rand_string(25)
agent.agent_id = self.rand_string(40)
agent.description = random.choice(descriptions)
agent.monitoring_type = mode
agent.public_ip = random.choice(public_ips)
agent.last_seen = djangotime.now()
agent.plat = "windows"
agent.plat_release = "windows-2019Server"
agent.last_seen = django_now
agent.total_ram = random.choice(total_rams)
agent.boot_time = random.choice(boot_times)
agent.logged_in_username = random.choice(user_names)
@@ -303,40 +363,36 @@ class Command(BaseCommand):
agent.overdue_email_alert = random.choice([True, False])
agent.overdue_text_alert = random.choice([True, False])
agent.needs_reboot = random.choice([True, False])
agent.wmi_detail = random.choice(wmi_details)
agent.services = services
agent.disks = random.choice(disks)
agent.save()
InstalledSoftware(agent=agent, software=random.choice(softwares)).save()
if agent.plat == AgentPlat.WINDOWS:
InstalledSoftware(agent=agent, software=random.choice(softwares)).save()
if mode == "workstation":
if mode == AgentMonType.WORKSTATION:
WinUpdatePolicy(agent=agent, run_time_days=[5, 6]).save()
else:
WinUpdatePolicy(agent=agent).save()
# windows updates load
guids = []
for k in windows_updates.keys():
guids.append(k)
for i in guids:
WinUpdate(
agent=agent,
guid=i,
kb=windows_updates[i]["KBs"][0],
title=windows_updates[i]["Title"],
installed=windows_updates[i]["Installed"],
downloaded=windows_updates[i]["Downloaded"],
description=windows_updates[i]["Description"],
severity=windows_updates[i]["Severity"],
).save()
if agent.plat == AgentPlat.WINDOWS:
# windows updates load
guids = [i for i in windows_updates.keys()]
for i in guids:
WinUpdate(
agent=agent,
guid=i,
kb=windows_updates[i]["KBs"][0],
title=windows_updates[i]["Title"],
installed=windows_updates[i]["Installed"],
downloaded=windows_updates[i]["Downloaded"],
description=windows_updates[i]["Description"],
severity=windows_updates[i]["Severity"],
).save()
# agent histories
hist = AgentHistory()
hist.agent = agent
hist.type = "cmd_run"
hist.type = AgentHistoryType.CMD_RUN
hist.command = "ping google.com"
hist.username = "demo"
hist.results = ping_success_output
@@ -344,7 +400,7 @@ class Command(BaseCommand):
hist1 = AgentHistory()
hist1.agent = agent
hist1.type = "script_run"
hist1.type = AgentHistoryType.SCRIPT_RUN
hist1.script = clear_spool
hist1.script_results = {
"id": 1,
@@ -355,50 +411,57 @@ class Command(BaseCommand):
}
hist1.save()
# disk space check
check1 = Check()
check_result1 = CheckResult(assigned_check=check1, agent=agent)
check1.agent = agent
check1.check_type = "diskspace"
check_result1.status = "passing"
check_result1.last_run = djangotime.now()
check_result1.more_info = "Total: 498.7GB, Free: 287.4GB"
check_result1.save()
if agent.plat == AgentPlat.WINDOWS:
# disk space check
check1 = Check()
check1.agent = agent
check1.check_type = CheckType.DISK_SPACE
check1.warning_threshold = 25
check1.error_threshold = 10
check1.disk = "C:"
check1.email_alert = random.choice([True, False])
check1.text_alert = random.choice([True, False])
check1.save()
check1.warning_threshold = 25
check1.error_threshold = 10
check1.disk = "C:"
check1.email_alert = random.choice([True, False])
check1.text_alert = random.choice([True, False])
check1.save()
check_result1 = CheckResult()
check_result1.agent = agent
check_result1.assigned_check = check1
check_result1.status = CheckStatus.PASSING
check_result1.last_run = django_now
check_result1.more_info = "Total: 498.7GB, Free: 287.4GB"
check_result1.save()
for i in range(30):
check1_history = CheckHistory()
check1_history.check_id = check1.pk
check1_history.agent_id = agent.agent_id
check1_history.x = djangotime.now() - djangotime.timedelta(
minutes=i * 2
)
check1_history.y = random.randint(13, 40)
check1_history.save()
for i in range(30):
check1_history = CheckHistory()
check1_history.check_id = check1.pk
check1_history.agent_id = agent.agent_id
check1_history.x = django_now - djangotime.timedelta(minutes=i * 2)
check1_history.y = random.randint(13, 40)
check1_history.save()
# ping check
check2 = Check()
check_result2 = CheckResult(assigned_check=check2, agent=agent)
check_result2 = CheckResult()
check2.agent = agent
check2.check_type = "ping"
check_result2.last_run = djangotime.now()
check2.check_type = CheckType.PING
check2.email_alert = random.choice([True, False])
check2.text_alert = random.choice([True, False])
check_result2.agent = agent
check_result2.assigned_check = check2
check_result2.last_run = django_now
if site in sites5:
check2.name = "Synology NAS"
check_result2.status = "failing"
check2.alert_severity = AlertSeverity.ERROR
check_result2.status = CheckStatus.FAILING
check2.ip = "172.17.14.26"
check_result2.more_info = ping_fail_output
else:
check2.name = "Google"
check_result2.status = "passing"
check_result2.status = CheckStatus.PASSING
check2.ip = "8.8.8.8"
check_result2.more_info = ping_success_output
@@ -409,9 +472,7 @@ class Command(BaseCommand):
check2_history = CheckHistory()
check2_history.check_id = check2.pk
check2_history.agent_id = agent.agent_id
check2_history.x = djangotime.now() - djangotime.timedelta(
minutes=i * 2
)
check2_history.x = django_now - djangotime.timedelta(minutes=i * 2)
if site in sites5:
check2_history.y = 1
check2_history.results = ping_fail_output
@@ -422,13 +483,19 @@ class Command(BaseCommand):
# cpu load check
check3 = Check()
check_result3 = CheckResult(assigned_check=check3, agent=agent)
check3.agent = agent
check3.check_type = "cpuload"
check_result3.status = "passing"
check_result3.last_run = djangotime.now()
check3.check_type = CheckType.CPU_LOAD
check3.warning_threshold = 70
check3.error_threshold = 90
check3.email_alert = random.choice([True, False])
check3.text_alert = random.choice([True, False])
check3.save()
check_result3 = CheckResult()
check_result3.agent = agent
check_result3.assigned_check = check3
check_result3.status = CheckStatus.PASSING
check_result3.last_run = django_now
check_result3.history = [
15,
23,
@@ -445,101 +512,116 @@ class Command(BaseCommand):
13,
34,
]
check3.email_alert = random.choice([True, False])
check3.text_alert = random.choice([True, False])
check3.save()
check_result3.save()
for i in range(30):
check3_history = CheckHistory()
check3_history.check_id = check3.pk
check3_history.agent_id = agent.agent_id
check3_history.x = djangotime.now() - djangotime.timedelta(
minutes=i * 2
)
check3_history.x = django_now - djangotime.timedelta(minutes=i * 2)
check3_history.y = random.randint(2, 79)
check3_history.save()
# memory check
check4 = Check()
check_result4 = CheckResult(assigned_check=check4, agent=agent)
check4.agent = agent
check4.check_type = "memory"
check_result4.status = "passing"
check4.check_type = CheckType.MEMORY
check4.warning_threshold = 70
check4.error_threshold = 85
check_result4.history = [34, 34, 35, 36, 34, 34, 34, 34, 34, 34]
check4.email_alert = random.choice([True, False])
check4.text_alert = random.choice([True, False])
check4.save()
check_result4 = CheckResult()
check_result4.agent = agent
check_result4.assigned_check = check4
check_result4.status = CheckStatus.PASSING
check_result4.last_run = django_now
check_result4.history = [34, 34, 35, 36, 34, 34, 34, 34, 34, 34]
check_result4.save()
for i in range(30):
check4_history = CheckHistory()
check4_history.check_id = check4.pk
check4_history.agent_id = agent.agent_id
check4_history.x = djangotime.now() - djangotime.timedelta(
minutes=i * 2
)
check4_history.x = django_now - djangotime.timedelta(minutes=i * 2)
check4_history.y = random.randint(2, 79)
check4_history.save()
# script check storage pool
check5 = Check()
check_result5 = CheckResult(assigned_check=check5, agent=agent)
check5.agent = agent
check5.check_type = "script"
check_result5.status = "passing"
check_result5.last_run = djangotime.now()
check5.check_type = CheckType.SCRIPT
check5.email_alert = random.choice([True, False])
check5.text_alert = random.choice([True, False])
check5.timeout = 120
check_result5.retcode = 0
check_result5.execution_time = "4.0000"
check5.script = check_pool_health
check5.save()
check_result5 = CheckResult()
check_result5.agent = agent
check_result5.assigned_check = check5
check_result5.status = CheckStatus.PASSING
check_result5.last_run = django_now
check_result5.retcode = 0
check_result5.execution_time = "4.0000"
check_result5.save()
for i in range(30):
check5_history = CheckHistory()
check5_history.check_id = check5.pk
check5_history.agent_id = agent.agent_id
check5_history.x = djangotime.now() - djangotime.timedelta(
minutes=i * 2
)
check5_history.x = django_now - djangotime.timedelta(minutes=i * 2)
if i == 10 or i == 18:
check5_history.y = 1
else:
check5_history.y = 0
check5_history.results = {
"retcode": 0,
"stdout": None,
"stderr": None,
"execution_time": "4.0000",
}
check5_history.save()
check6 = Check()
check_result6 = CheckResult(assigned_check=check6, agent=agent)
check6.agent = agent
check6.check_type = "script"
check_result6.status = "passing"
check_result6.last_run = djangotime.now()
check6.check_type = CheckType.SCRIPT
check6.email_alert = random.choice([True, False])
check6.text_alert = random.choice([True, False])
check6.timeout = 120
check_result6.retcode = 0
check_result6.execution_time = "4.0000"
check6.script = check_net_aware
check6.save()
check_result6 = CheckResult()
check_result6.agent = agent
check_result6.assigned_check = check6
check_result6.status = CheckStatus.PASSING
check_result6.last_run = django_now
check_result6.retcode = 0
check_result6.execution_time = "4.0000"
check_result6.save()
for i in range(30):
check6_history = CheckHistory()
check6_history.check_id = check6.pk
check6_history.agent_id = agent.agent_id
check6_history.x = djangotime.now() - djangotime.timedelta(
minutes=i * 2
)
check6_history.x = django_now - djangotime.timedelta(minutes=i * 2)
check6_history.y = 0
check6_history.results = {
"retcode": 0,
"stdout": None,
"stderr": None,
"execution_time": "4.0000",
}
check6_history.save()
nla_task = AutomatedTask()
nla_task_result = TaskResult(task=nla_task, agent=agent)
nla_task.agent = agent
actions = [
{
@@ -553,17 +635,21 @@ class Command(BaseCommand):
nla_task.actions = actions
nla_task.assigned_check = check6
nla_task.name = "Restart NLA"
nla_task.task_type = "checkfailure"
nla_task.task_type = TaskType.CHECK_FAILURE
nla_task.save()
nla_task_result = TaskResult()
nla_task_result.task = nla_task
nla_task_result.agent = agent
nla_task_result.execution_time = "1.8443"
nla_task_result.last_run = djangotime.now()
nla_task_result.last_run = django_now
nla_task_result.stdout = "no stdout"
nla_task_result.retcode = 0
nla_task_result.sync_status = "synced"
nla_task.save()
nla_task_result.sync_status = TaskSyncStatus.SYNCED
nla_task_result.save()
spool_task = AutomatedTask()
spool_task_result = TaskResult(task=spool_task, agent=agent)
spool_task.agent = agent
actions = [
{
@@ -576,25 +662,26 @@ class Command(BaseCommand):
]
spool_task.actions = actions
spool_task.name = "Clear the print spooler"
spool_task.task_type = "daily"
spool_task.run_time_date = djangotime.now() + djangotime.timedelta(
minutes=10
)
spool_task.expire_date = djangotime.now() + djangotime.timedelta(days=753)
spool_task.task_type = TaskType.DAILY
spool_task.run_time_date = django_now + djangotime.timedelta(minutes=10)
spool_task.expire_date = django_now + djangotime.timedelta(days=753)
spool_task.daily_interval = 1
spool_task.weekly_interval = 1
spool_task.task_repetition_duration = "2h"
spool_task.task_repetition_interval = "25m"
spool_task.random_task_delay = "3m"
spool_task_result.last_run = djangotime.now()
spool_task.save()
spool_task_result = TaskResult()
spool_task_result.task = spool_task
spool_task_result.agent = agent
spool_task_result.last_run = django_now
spool_task_result.retcode = 0
spool_task_result.stdout = spooler_stdout
spool_task_result.sync_status = "synced"
spool_task.save()
spool_task_result.sync_status = TaskSyncStatus.SYNCED
spool_task_result.save()
tmp_dir_task = AutomatedTask()
tmp_dir_task_result = TaskResult(task=tmp_dir_task, agent=agent)
tmp_dir_task.agent = agent
tmp_dir_task.name = "show temp dir files"
actions = [
@@ -607,139 +694,153 @@ class Command(BaseCommand):
}
]
tmp_dir_task.actions = actions
tmp_dir_task.task_type = "manual"
tmp_dir_task_result.last_run = djangotime.now()
tmp_dir_task.task_type = TaskType.MANUAL
tmp_dir_task.save()
tmp_dir_task_result = TaskResult()
tmp_dir_task_result.task = tmp_dir_task
tmp_dir_task_result.agent = agent
tmp_dir_task_result.last_run = django_now
tmp_dir_task_result.stdout = temp_dir_stdout
tmp_dir_task_result.retcode = 0
tmp_dir_task_result.sync_status = "synced"
tmp_dir_task.save()
tmp_dir_task_result.sync_status = TaskSyncStatus.SYNCED
tmp_dir_task_result.save()
check7 = Check()
check_result7 = CheckResult(assigned_check=check7, agent=agent)
check7.agent = agent
check7.check_type = "script"
check_result7.status = "passing"
check_result7.last_run = djangotime.now()
check7.check_type = CheckType.SCRIPT
check7.email_alert = random.choice([True, False])
check7.text_alert = random.choice([True, False])
check7.timeout = 120
check7.script = clear_spool
check7.save()
check_result7 = CheckResult()
check_result7.assigned_check = check7
check_result7.agent = agent
check_result7.status = CheckStatus.PASSING
check_result7.last_run = django_now
check_result7.retcode = 0
check_result7.execution_time = "3.1337"
check7.script = clear_spool
check_result7.stdout = spooler_stdout
check7.save()
check_result7.save()
for i in range(30):
check7_history = CheckHistory()
check7_history.check_id = check7.pk
check7_history.agent_id = agent.agent_id
check7_history.x = djangotime.now() - djangotime.timedelta(
minutes=i * 2
)
check7_history.x = django_now - djangotime.timedelta(minutes=i * 2)
check7_history.y = 0
check7_history.results = {
"retcode": 0,
"stdout": spooler_stdout,
"stderr": None,
"execution_time": "3.1337",
}
check7_history.save()
check8 = Check()
check_result8 = CheckResult(assigned_check=check8, agent=agent)
check8.agent = agent
check8.check_type = "winsvc"
check_result8.status = "passing"
check_result8.last_run = djangotime.now()
check8.email_alert = random.choice([True, False])
check8.text_alert = random.choice([True, False])
check_result8.more_info = "Status RUNNING"
check8.fails_b4_alert = 4
check8.svc_name = "Spooler"
check8.svc_display_name = "Print Spooler"
check8.pass_if_start_pending = False
check8.restart_if_stopped = True
check8.save()
check_result8.save()
if agent.plat == AgentPlat.WINDOWS:
check8 = Check()
check8.agent = agent
check8.check_type = CheckType.WINSVC
check8.email_alert = random.choice([True, False])
check8.text_alert = random.choice([True, False])
check8.fails_b4_alert = 4
check8.svc_name = "Spooler"
check8.svc_display_name = "Print Spooler"
check8.pass_if_start_pending = False
check8.restart_if_stopped = True
check8.save()
for i in range(30):
check8_history = CheckHistory()
check8_history.check_id = check8.pk
check8_history.agent_id = agent.agent_id
check8_history.x = djangotime.now() - djangotime.timedelta(
minutes=i * 2
)
if i == 10 or i == 18:
check8_history.y = 1
check8_history.results = "Status STOPPED"
check_result8 = CheckResult()
check_result8.assigned_check = check8
check_result8.agent = agent
check_result8.status = CheckStatus.PASSING
check_result8.last_run = django_now
check_result8.more_info = "Status RUNNING"
check_result8.save()
for i in range(30):
check8_history = CheckHistory()
check8_history.check_id = check8.pk
check8_history.agent_id = agent.agent_id
check8_history.x = django_now - djangotime.timedelta(minutes=i * 2)
if i == 10 or i == 18:
check8_history.y = 1
check8_history.results = "Status STOPPED"
else:
check8_history.y = 0
check8_history.results = "Status RUNNING"
check8_history.save()
check9 = Check()
check9.agent = agent
check9.check_type = CheckType.EVENT_LOG
check9.name = "unexpected shutdown"
check9.email_alert = random.choice([True, False])
check9.text_alert = random.choice([True, False])
check9.fails_b4_alert = 2
check9.log_name = EvtLogNames.APPLICATION
check9.event_id = 1001
check9.event_type = EvtLogTypes.INFO
check9.fail_when = EvtLogFailWhen.CONTAINS
check9.search_last_days = 30
check_result9 = CheckResult()
check_result9.agent = agent
check_result9.assigned_check = check9
check_result9.last_run = django_now
if site in sites5:
check_result9.extra_details = eventlog_check_fail_data
check_result9.status = CheckStatus.FAILING
else:
check8_history.y = 0
check8_history.results = "Status RUNNING"
check8_history.save()
check_result9.extra_details = {"log": []}
check_result9.status = CheckStatus.PASSING
check9 = Check()
check_result9 = CheckResult(assigned_check=check9, agent=agent)
check9.agent = agent
check9.check_type = "eventlog"
check9.name = "unexpected shutdown"
check9.save()
check_result9.save()
check_result9.last_run = djangotime.now()
check9.email_alert = random.choice([True, False])
check9.text_alert = random.choice([True, False])
check9.fails_b4_alert = 2
for i in range(30):
check9_history = CheckHistory()
check9_history.check_id = check9.pk
check9_history.agent_id = agent.agent_id
check9_history.x = django_now - djangotime.timedelta(minutes=i * 2)
if i == 10 or i == 18:
check9_history.y = 1
check9_history.results = "Events Found: 16"
else:
check9_history.y = 0
check9_history.results = "Events Found: 0"
check9_history.save()
if site in sites5:
check_result9.extra_details = eventlog_check_fail_data
check_result9.status = "failing"
else:
check_result9.extra_details = {"log": []}
check_result9.status = "passing"
pick = random.randint(1, 10)
check9.log_name = "Application"
check9.event_id = 1001
check9.event_type = "INFO"
check9.fail_when = "contains"
check9.search_last_days = 30
if pick == 5 or pick == 3:
reboot_time = django_now + djangotime.timedelta(
minutes=random.randint(1000, 500000)
)
date_obj = dt.datetime.strftime(reboot_time, "%Y-%m-%d %H:%M")
check9.save()
check_result9.save()
obj = dt.datetime.strptime(date_obj, "%Y-%m-%d %H:%M")
for i in range(30):
check9_history = CheckHistory()
check9_history.check_id = check9.pk
check9_history.agent_id = agent.agent_id
check9_history.x = djangotime.now() - djangotime.timedelta(
minutes=i * 2
)
if i == 10 or i == 18:
check9_history.y = 1
check9_history.results = "Events Found: 16"
else:
check9_history.y = 0
check9_history.results = "Events Found: 0"
check9_history.save()
task_name = "TacticalRMM_SchedReboot_" + "".join(
random.choice(string.ascii_letters) for _ in range(10)
)
pick = random.randint(1, 10)
if pick == 5 or pick == 3:
reboot_time = djangotime.now() + djangotime.timedelta(
minutes=random.randint(1000, 500000)
)
date_obj = dt.datetime.strftime(reboot_time, "%Y-%m-%d %H:%M")
obj = dt.datetime.strptime(date_obj, "%Y-%m-%d %H:%M")
task_name = "TacticalRMM_SchedReboot_" + "".join(
random.choice(string.ascii_letters) for _ in range(10)
)
sched_reboot = PendingAction()
sched_reboot.agent = agent
sched_reboot.action_type = "schedreboot"
sched_reboot.details = {
"time": str(obj),
"taskname": task_name,
}
sched_reboot.save()
sched_reboot = PendingAction()
sched_reboot.agent = agent
sched_reboot.action_type = PAAction.SCHED_REBOOT
sched_reboot.details = {
"time": str(obj),
"taskname": task_name,
}
sched_reboot.save()
self.stdout.write(self.style.SUCCESS(f"Added agent # {count_agents + 1}"))
call_command("load_demo_scripts")
self.stdout.write("done")

View File

@@ -0,0 +1,30 @@
from django.core.management.base import BaseCommand
from agents.models import Agent
from tacticalrmm.constants import AGENT_DEFER
class Command(BaseCommand):
help = "Find all agents that have a certain service installed"
def add_arguments(self, parser):
parser.add_argument("name", type=str)
def handle(self, *args, **kwargs):
search = kwargs["name"].lower()
agents = Agent.objects.defer(*AGENT_DEFER)
for agent in agents:
try:
for svc in agent.services:
if (
search in svc["name"].lower()
or search in svc["display_name"].lower()
):
self.stdout.write(
self.style.SUCCESS(
f"{agent.hostname} - {svc['name']} ({svc['display_name']}) - {svc['status']}"
)
)
except:
continue

View File

@@ -0,0 +1,24 @@
from django.core.management.base import BaseCommand
from agents.models import Agent
from tacticalrmm.constants import AGENT_DEFER
class Command(BaseCommand):
def find_duplicates(self, lst):
return list(set([item for item in lst if lst.count(item) > 1]))
def handle(self, *args, **kwargs):
for agent in Agent.objects.defer(*AGENT_DEFER).prefetch_related(
"custom_fields__field"
):
if dupes := self.find_duplicates(
[i.field.name for i in agent.custom_fields.all()]
):
for dupe in dupes:
cf = list(
agent.custom_fields.filter(field__name=dupe).order_by("id")
)
to_delete = cf[:-1]
for i in to_delete:
i.delete()

View File

@@ -1,16 +1,17 @@
from agents.models import Agent
from django.conf import settings
from django.core.management.base import BaseCommand
from agents.models import Agent
from tacticalrmm.constants import AGENT_STATUS_ONLINE, ONLINE_AGENTS
class Command(BaseCommand):
help = "Shows online agents that are not on the latest version"
def handle(self, *args, **kwargs):
q = Agent.objects.exclude(version=settings.LATEST_AGENT_VER).only(
"pk", "version", "last_seen", "overdue_time", "offline_time"
)
agents = [i for i in q if i.status == "online"]
only = ONLINE_AGENTS + ("hostname",)
q = Agent.objects.exclude(version=settings.LATEST_AGENT_VER).only(*only)
agents = [i for i in q if i.status == AGENT_STATUS_ONLINE]
for agent in agents:
self.stdout.write(
self.style.SUCCESS(f"{agent.hostname} - v{agent.version}")

View File

@@ -1,10 +1,10 @@
from agents.models import Agent
from agents.tasks import send_agent_update_task
from core.utils import get_core_settings
from django.conf import settings
from django.core.management.base import BaseCommand
from packaging import version as pyver
from agents.models import Agent
from agents.tasks import send_agent_update_task
from core.utils import get_core_settings, token_is_valid
from tacticalrmm.constants import AGENT_DEFER
@@ -22,4 +22,5 @@ class Command(BaseCommand):
for i in q
if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
]
send_agent_update_task.delay(agent_ids=agent_ids)
token, _ = token_is_valid()
send_agent_update_task.delay(agent_ids=agent_ids, token=token, force=False)

View File

@@ -1,7 +1,7 @@
# Generated by Django 4.0.3 on 2022-04-07 17:28
from django.db import migrations, models
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):

View File

@@ -0,0 +1,17 @@
# Generated by Django 4.0.4 on 2022-04-25 06:51
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('agents', '0049_agent_agents_agen_monitor_df8816_idx'),
]
operations = [
migrations.RemoveField(
model_name='agent',
name='plat_release',
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.0.4 on 2022-05-18 03:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agents', '0050_remove_agent_plat_release'),
]
operations = [
migrations.AlterField(
model_name='agent',
name='plat',
field=models.CharField(choices=[('windows', 'Windows'), ('linux', 'Linux'), ('darwin', 'macOS')], default='windows', max_length=255),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.0.4 on 2022-05-18 05:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agents', '0051_alter_agent_plat'),
]
operations = [
migrations.AlterField(
model_name='agent',
name='monitoring_type',
field=models.CharField(choices=[('server', 'Server'), ('workstation', 'Workstation')], default='server', max_length=30),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 4.0.4 on 2022-05-18 06:10
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('agents', '0052_alter_agent_monitoring_type'),
]
operations = [
migrations.RemoveField(
model_name='agenthistory',
name='status',
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.0.4 on 2022-06-06 04:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agents', '0053_remove_agenthistory_status'),
]
operations = [
migrations.AlterField(
model_name='agent',
name='goarch',
field=models.CharField(blank=True, choices=[('amd64', 'amd64'), ('386', '386'), ('arm64', 'arm64'), ('arm', 'arm')], max_length=255, null=True),
),
]

View File

@@ -0,0 +1,631 @@
# Generated by Django 4.1 on 2022-08-24 07:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("agents", "0054_alter_agent_goarch"),
]
operations = [
migrations.AlterField(
model_name="agent",
name="time_zone",
field=models.CharField(
blank=True,
choices=[
("Africa/Abidjan", "Africa/Abidjan"),
("Africa/Accra", "Africa/Accra"),
("Africa/Addis_Ababa", "Africa/Addis_Ababa"),
("Africa/Algiers", "Africa/Algiers"),
("Africa/Asmara", "Africa/Asmara"),
("Africa/Asmera", "Africa/Asmera"),
("Africa/Bamako", "Africa/Bamako"),
("Africa/Bangui", "Africa/Bangui"),
("Africa/Banjul", "Africa/Banjul"),
("Africa/Bissau", "Africa/Bissau"),
("Africa/Blantyre", "Africa/Blantyre"),
("Africa/Brazzaville", "Africa/Brazzaville"),
("Africa/Bujumbura", "Africa/Bujumbura"),
("Africa/Cairo", "Africa/Cairo"),
("Africa/Casablanca", "Africa/Casablanca"),
("Africa/Ceuta", "Africa/Ceuta"),
("Africa/Conakry", "Africa/Conakry"),
("Africa/Dakar", "Africa/Dakar"),
("Africa/Dar_es_Salaam", "Africa/Dar_es_Salaam"),
("Africa/Djibouti", "Africa/Djibouti"),
("Africa/Douala", "Africa/Douala"),
("Africa/El_Aaiun", "Africa/El_Aaiun"),
("Africa/Freetown", "Africa/Freetown"),
("Africa/Gaborone", "Africa/Gaborone"),
("Africa/Harare", "Africa/Harare"),
("Africa/Johannesburg", "Africa/Johannesburg"),
("Africa/Juba", "Africa/Juba"),
("Africa/Kampala", "Africa/Kampala"),
("Africa/Khartoum", "Africa/Khartoum"),
("Africa/Kigali", "Africa/Kigali"),
("Africa/Kinshasa", "Africa/Kinshasa"),
("Africa/Lagos", "Africa/Lagos"),
("Africa/Libreville", "Africa/Libreville"),
("Africa/Lome", "Africa/Lome"),
("Africa/Luanda", "Africa/Luanda"),
("Africa/Lubumbashi", "Africa/Lubumbashi"),
("Africa/Lusaka", "Africa/Lusaka"),
("Africa/Malabo", "Africa/Malabo"),
("Africa/Maputo", "Africa/Maputo"),
("Africa/Maseru", "Africa/Maseru"),
("Africa/Mbabane", "Africa/Mbabane"),
("Africa/Mogadishu", "Africa/Mogadishu"),
("Africa/Monrovia", "Africa/Monrovia"),
("Africa/Nairobi", "Africa/Nairobi"),
("Africa/Ndjamena", "Africa/Ndjamena"),
("Africa/Niamey", "Africa/Niamey"),
("Africa/Nouakchott", "Africa/Nouakchott"),
("Africa/Ouagadougou", "Africa/Ouagadougou"),
("Africa/Porto-Novo", "Africa/Porto-Novo"),
("Africa/Sao_Tome", "Africa/Sao_Tome"),
("Africa/Timbuktu", "Africa/Timbuktu"),
("Africa/Tripoli", "Africa/Tripoli"),
("Africa/Tunis", "Africa/Tunis"),
("Africa/Windhoek", "Africa/Windhoek"),
("America/Adak", "America/Adak"),
("America/Anchorage", "America/Anchorage"),
("America/Anguilla", "America/Anguilla"),
("America/Antigua", "America/Antigua"),
("America/Araguaina", "America/Araguaina"),
(
"America/Argentina/Buenos_Aires",
"America/Argentina/Buenos_Aires",
),
("America/Argentina/Catamarca", "America/Argentina/Catamarca"),
(
"America/Argentina/ComodRivadavia",
"America/Argentina/ComodRivadavia",
),
("America/Argentina/Cordoba", "America/Argentina/Cordoba"),
("America/Argentina/Jujuy", "America/Argentina/Jujuy"),
("America/Argentina/La_Rioja", "America/Argentina/La_Rioja"),
("America/Argentina/Mendoza", "America/Argentina/Mendoza"),
(
"America/Argentina/Rio_Gallegos",
"America/Argentina/Rio_Gallegos",
),
("America/Argentina/Salta", "America/Argentina/Salta"),
("America/Argentina/San_Juan", "America/Argentina/San_Juan"),
("America/Argentina/San_Luis", "America/Argentina/San_Luis"),
("America/Argentina/Tucuman", "America/Argentina/Tucuman"),
("America/Argentina/Ushuaia", "America/Argentina/Ushuaia"),
("America/Aruba", "America/Aruba"),
("America/Asuncion", "America/Asuncion"),
("America/Atikokan", "America/Atikokan"),
("America/Atka", "America/Atka"),
("America/Bahia", "America/Bahia"),
("America/Bahia_Banderas", "America/Bahia_Banderas"),
("America/Barbados", "America/Barbados"),
("America/Belem", "America/Belem"),
("America/Belize", "America/Belize"),
("America/Blanc-Sablon", "America/Blanc-Sablon"),
("America/Boa_Vista", "America/Boa_Vista"),
("America/Bogota", "America/Bogota"),
("America/Boise", "America/Boise"),
("America/Buenos_Aires", "America/Buenos_Aires"),
("America/Cambridge_Bay", "America/Cambridge_Bay"),
("America/Campo_Grande", "America/Campo_Grande"),
("America/Cancun", "America/Cancun"),
("America/Caracas", "America/Caracas"),
("America/Catamarca", "America/Catamarca"),
("America/Cayenne", "America/Cayenne"),
("America/Cayman", "America/Cayman"),
("America/Chicago", "America/Chicago"),
("America/Chihuahua", "America/Chihuahua"),
("America/Coral_Harbour", "America/Coral_Harbour"),
("America/Cordoba", "America/Cordoba"),
("America/Costa_Rica", "America/Costa_Rica"),
("America/Creston", "America/Creston"),
("America/Cuiaba", "America/Cuiaba"),
("America/Curacao", "America/Curacao"),
("America/Danmarkshavn", "America/Danmarkshavn"),
("America/Dawson", "America/Dawson"),
("America/Dawson_Creek", "America/Dawson_Creek"),
("America/Denver", "America/Denver"),
("America/Detroit", "America/Detroit"),
("America/Dominica", "America/Dominica"),
("America/Edmonton", "America/Edmonton"),
("America/Eirunepe", "America/Eirunepe"),
("America/El_Salvador", "America/El_Salvador"),
("America/Ensenada", "America/Ensenada"),
("America/Fort_Nelson", "America/Fort_Nelson"),
("America/Fort_Wayne", "America/Fort_Wayne"),
("America/Fortaleza", "America/Fortaleza"),
("America/Glace_Bay", "America/Glace_Bay"),
("America/Godthab", "America/Godthab"),
("America/Goose_Bay", "America/Goose_Bay"),
("America/Grand_Turk", "America/Grand_Turk"),
("America/Grenada", "America/Grenada"),
("America/Guadeloupe", "America/Guadeloupe"),
("America/Guatemala", "America/Guatemala"),
("America/Guayaquil", "America/Guayaquil"),
("America/Guyana", "America/Guyana"),
("America/Halifax", "America/Halifax"),
("America/Havana", "America/Havana"),
("America/Hermosillo", "America/Hermosillo"),
("America/Indiana/Indianapolis", "America/Indiana/Indianapolis"),
("America/Indiana/Knox", "America/Indiana/Knox"),
("America/Indiana/Marengo", "America/Indiana/Marengo"),
("America/Indiana/Petersburg", "America/Indiana/Petersburg"),
("America/Indiana/Tell_City", "America/Indiana/Tell_City"),
("America/Indiana/Vevay", "America/Indiana/Vevay"),
("America/Indiana/Vincennes", "America/Indiana/Vincennes"),
("America/Indiana/Winamac", "America/Indiana/Winamac"),
("America/Indianapolis", "America/Indianapolis"),
("America/Inuvik", "America/Inuvik"),
("America/Iqaluit", "America/Iqaluit"),
("America/Jamaica", "America/Jamaica"),
("America/Jujuy", "America/Jujuy"),
("America/Juneau", "America/Juneau"),
("America/Kentucky/Louisville", "America/Kentucky/Louisville"),
("America/Kentucky/Monticello", "America/Kentucky/Monticello"),
("America/Knox_IN", "America/Knox_IN"),
("America/Kralendijk", "America/Kralendijk"),
("America/La_Paz", "America/La_Paz"),
("America/Lima", "America/Lima"),
("America/Los_Angeles", "America/Los_Angeles"),
("America/Louisville", "America/Louisville"),
("America/Lower_Princes", "America/Lower_Princes"),
("America/Maceio", "America/Maceio"),
("America/Managua", "America/Managua"),
("America/Manaus", "America/Manaus"),
("America/Marigot", "America/Marigot"),
("America/Martinique", "America/Martinique"),
("America/Matamoros", "America/Matamoros"),
("America/Mazatlan", "America/Mazatlan"),
("America/Mendoza", "America/Mendoza"),
("America/Menominee", "America/Menominee"),
("America/Merida", "America/Merida"),
("America/Metlakatla", "America/Metlakatla"),
("America/Mexico_City", "America/Mexico_City"),
("America/Miquelon", "America/Miquelon"),
("America/Moncton", "America/Moncton"),
("America/Monterrey", "America/Monterrey"),
("America/Montevideo", "America/Montevideo"),
("America/Montreal", "America/Montreal"),
("America/Montserrat", "America/Montserrat"),
("America/Nassau", "America/Nassau"),
("America/New_York", "America/New_York"),
("America/Nipigon", "America/Nipigon"),
("America/Nome", "America/Nome"),
("America/Noronha", "America/Noronha"),
("America/North_Dakota/Beulah", "America/North_Dakota/Beulah"),
("America/North_Dakota/Center", "America/North_Dakota/Center"),
(
"America/North_Dakota/New_Salem",
"America/North_Dakota/New_Salem",
),
("America/Nuuk", "America/Nuuk"),
("America/Ojinaga", "America/Ojinaga"),
("America/Panama", "America/Panama"),
("America/Pangnirtung", "America/Pangnirtung"),
("America/Paramaribo", "America/Paramaribo"),
("America/Phoenix", "America/Phoenix"),
("America/Port-au-Prince", "America/Port-au-Prince"),
("America/Port_of_Spain", "America/Port_of_Spain"),
("America/Porto_Acre", "America/Porto_Acre"),
("America/Porto_Velho", "America/Porto_Velho"),
("America/Puerto_Rico", "America/Puerto_Rico"),
("America/Punta_Arenas", "America/Punta_Arenas"),
("America/Rainy_River", "America/Rainy_River"),
("America/Rankin_Inlet", "America/Rankin_Inlet"),
("America/Recife", "America/Recife"),
("America/Regina", "America/Regina"),
("America/Resolute", "America/Resolute"),
("America/Rio_Branco", "America/Rio_Branco"),
("America/Rosario", "America/Rosario"),
("America/Santa_Isabel", "America/Santa_Isabel"),
("America/Santarem", "America/Santarem"),
("America/Santiago", "America/Santiago"),
("America/Santo_Domingo", "America/Santo_Domingo"),
("America/Sao_Paulo", "America/Sao_Paulo"),
("America/Scoresbysund", "America/Scoresbysund"),
("America/Shiprock", "America/Shiprock"),
("America/Sitka", "America/Sitka"),
("America/St_Barthelemy", "America/St_Barthelemy"),
("America/St_Johns", "America/St_Johns"),
("America/St_Kitts", "America/St_Kitts"),
("America/St_Lucia", "America/St_Lucia"),
("America/St_Thomas", "America/St_Thomas"),
("America/St_Vincent", "America/St_Vincent"),
("America/Swift_Current", "America/Swift_Current"),
("America/Tegucigalpa", "America/Tegucigalpa"),
("America/Thule", "America/Thule"),
("America/Thunder_Bay", "America/Thunder_Bay"),
("America/Tijuana", "America/Tijuana"),
("America/Toronto", "America/Toronto"),
("America/Tortola", "America/Tortola"),
("America/Vancouver", "America/Vancouver"),
("America/Virgin", "America/Virgin"),
("America/Whitehorse", "America/Whitehorse"),
("America/Winnipeg", "America/Winnipeg"),
("America/Yakutat", "America/Yakutat"),
("America/Yellowknife", "America/Yellowknife"),
("Antarctica/Casey", "Antarctica/Casey"),
("Antarctica/Davis", "Antarctica/Davis"),
("Antarctica/DumontDUrville", "Antarctica/DumontDUrville"),
("Antarctica/Macquarie", "Antarctica/Macquarie"),
("Antarctica/Mawson", "Antarctica/Mawson"),
("Antarctica/McMurdo", "Antarctica/McMurdo"),
("Antarctica/Palmer", "Antarctica/Palmer"),
("Antarctica/Rothera", "Antarctica/Rothera"),
("Antarctica/South_Pole", "Antarctica/South_Pole"),
("Antarctica/Syowa", "Antarctica/Syowa"),
("Antarctica/Troll", "Antarctica/Troll"),
("Antarctica/Vostok", "Antarctica/Vostok"),
("Arctic/Longyearbyen", "Arctic/Longyearbyen"),
("Asia/Aden", "Asia/Aden"),
("Asia/Almaty", "Asia/Almaty"),
("Asia/Amman", "Asia/Amman"),
("Asia/Anadyr", "Asia/Anadyr"),
("Asia/Aqtau", "Asia/Aqtau"),
("Asia/Aqtobe", "Asia/Aqtobe"),
("Asia/Ashgabat", "Asia/Ashgabat"),
("Asia/Ashkhabad", "Asia/Ashkhabad"),
("Asia/Atyrau", "Asia/Atyrau"),
("Asia/Baghdad", "Asia/Baghdad"),
("Asia/Bahrain", "Asia/Bahrain"),
("Asia/Baku", "Asia/Baku"),
("Asia/Bangkok", "Asia/Bangkok"),
("Asia/Barnaul", "Asia/Barnaul"),
("Asia/Beirut", "Asia/Beirut"),
("Asia/Bishkek", "Asia/Bishkek"),
("Asia/Brunei", "Asia/Brunei"),
("Asia/Calcutta", "Asia/Calcutta"),
("Asia/Chita", "Asia/Chita"),
("Asia/Choibalsan", "Asia/Choibalsan"),
("Asia/Chongqing", "Asia/Chongqing"),
("Asia/Chungking", "Asia/Chungking"),
("Asia/Colombo", "Asia/Colombo"),
("Asia/Dacca", "Asia/Dacca"),
("Asia/Damascus", "Asia/Damascus"),
("Asia/Dhaka", "Asia/Dhaka"),
("Asia/Dili", "Asia/Dili"),
("Asia/Dubai", "Asia/Dubai"),
("Asia/Dushanbe", "Asia/Dushanbe"),
("Asia/Famagusta", "Asia/Famagusta"),
("Asia/Gaza", "Asia/Gaza"),
("Asia/Harbin", "Asia/Harbin"),
("Asia/Hebron", "Asia/Hebron"),
("Asia/Ho_Chi_Minh", "Asia/Ho_Chi_Minh"),
("Asia/Hong_Kong", "Asia/Hong_Kong"),
("Asia/Hovd", "Asia/Hovd"),
("Asia/Irkutsk", "Asia/Irkutsk"),
("Asia/Istanbul", "Asia/Istanbul"),
("Asia/Jakarta", "Asia/Jakarta"),
("Asia/Jayapura", "Asia/Jayapura"),
("Asia/Jerusalem", "Asia/Jerusalem"),
("Asia/Kabul", "Asia/Kabul"),
("Asia/Kamchatka", "Asia/Kamchatka"),
("Asia/Karachi", "Asia/Karachi"),
("Asia/Kashgar", "Asia/Kashgar"),
("Asia/Kathmandu", "Asia/Kathmandu"),
("Asia/Katmandu", "Asia/Katmandu"),
("Asia/Khandyga", "Asia/Khandyga"),
("Asia/Kolkata", "Asia/Kolkata"),
("Asia/Krasnoyarsk", "Asia/Krasnoyarsk"),
("Asia/Kuala_Lumpur", "Asia/Kuala_Lumpur"),
("Asia/Kuching", "Asia/Kuching"),
("Asia/Kuwait", "Asia/Kuwait"),
("Asia/Macao", "Asia/Macao"),
("Asia/Macau", "Asia/Macau"),
("Asia/Magadan", "Asia/Magadan"),
("Asia/Makassar", "Asia/Makassar"),
("Asia/Manila", "Asia/Manila"),
("Asia/Muscat", "Asia/Muscat"),
("Asia/Nicosia", "Asia/Nicosia"),
("Asia/Novokuznetsk", "Asia/Novokuznetsk"),
("Asia/Novosibirsk", "Asia/Novosibirsk"),
("Asia/Omsk", "Asia/Omsk"),
("Asia/Oral", "Asia/Oral"),
("Asia/Phnom_Penh", "Asia/Phnom_Penh"),
("Asia/Pontianak", "Asia/Pontianak"),
("Asia/Pyongyang", "Asia/Pyongyang"),
("Asia/Qatar", "Asia/Qatar"),
("Asia/Qostanay", "Asia/Qostanay"),
("Asia/Qyzylorda", "Asia/Qyzylorda"),
("Asia/Rangoon", "Asia/Rangoon"),
("Asia/Riyadh", "Asia/Riyadh"),
("Asia/Saigon", "Asia/Saigon"),
("Asia/Sakhalin", "Asia/Sakhalin"),
("Asia/Samarkand", "Asia/Samarkand"),
("Asia/Seoul", "Asia/Seoul"),
("Asia/Shanghai", "Asia/Shanghai"),
("Asia/Singapore", "Asia/Singapore"),
("Asia/Srednekolymsk", "Asia/Srednekolymsk"),
("Asia/Taipei", "Asia/Taipei"),
("Asia/Tashkent", "Asia/Tashkent"),
("Asia/Tbilisi", "Asia/Tbilisi"),
("Asia/Tehran", "Asia/Tehran"),
("Asia/Tel_Aviv", "Asia/Tel_Aviv"),
("Asia/Thimbu", "Asia/Thimbu"),
("Asia/Thimphu", "Asia/Thimphu"),
("Asia/Tokyo", "Asia/Tokyo"),
("Asia/Tomsk", "Asia/Tomsk"),
("Asia/Ujung_Pandang", "Asia/Ujung_Pandang"),
("Asia/Ulaanbaatar", "Asia/Ulaanbaatar"),
("Asia/Ulan_Bator", "Asia/Ulan_Bator"),
("Asia/Urumqi", "Asia/Urumqi"),
("Asia/Ust-Nera", "Asia/Ust-Nera"),
("Asia/Vientiane", "Asia/Vientiane"),
("Asia/Vladivostok", "Asia/Vladivostok"),
("Asia/Yakutsk", "Asia/Yakutsk"),
("Asia/Yangon", "Asia/Yangon"),
("Asia/Yekaterinburg", "Asia/Yekaterinburg"),
("Asia/Yerevan", "Asia/Yerevan"),
("Atlantic/Azores", "Atlantic/Azores"),
("Atlantic/Bermuda", "Atlantic/Bermuda"),
("Atlantic/Canary", "Atlantic/Canary"),
("Atlantic/Cape_Verde", "Atlantic/Cape_Verde"),
("Atlantic/Faeroe", "Atlantic/Faeroe"),
("Atlantic/Faroe", "Atlantic/Faroe"),
("Atlantic/Jan_Mayen", "Atlantic/Jan_Mayen"),
("Atlantic/Madeira", "Atlantic/Madeira"),
("Atlantic/Reykjavik", "Atlantic/Reykjavik"),
("Atlantic/South_Georgia", "Atlantic/South_Georgia"),
("Atlantic/St_Helena", "Atlantic/St_Helena"),
("Atlantic/Stanley", "Atlantic/Stanley"),
("Australia/ACT", "Australia/ACT"),
("Australia/Adelaide", "Australia/Adelaide"),
("Australia/Brisbane", "Australia/Brisbane"),
("Australia/Broken_Hill", "Australia/Broken_Hill"),
("Australia/Canberra", "Australia/Canberra"),
("Australia/Currie", "Australia/Currie"),
("Australia/Darwin", "Australia/Darwin"),
("Australia/Eucla", "Australia/Eucla"),
("Australia/Hobart", "Australia/Hobart"),
("Australia/LHI", "Australia/LHI"),
("Australia/Lindeman", "Australia/Lindeman"),
("Australia/Lord_Howe", "Australia/Lord_Howe"),
("Australia/Melbourne", "Australia/Melbourne"),
("Australia/NSW", "Australia/NSW"),
("Australia/North", "Australia/North"),
("Australia/Perth", "Australia/Perth"),
("Australia/Queensland", "Australia/Queensland"),
("Australia/South", "Australia/South"),
("Australia/Sydney", "Australia/Sydney"),
("Australia/Tasmania", "Australia/Tasmania"),
("Australia/Victoria", "Australia/Victoria"),
("Australia/West", "Australia/West"),
("Australia/Yancowinna", "Australia/Yancowinna"),
("Brazil/Acre", "Brazil/Acre"),
("Brazil/DeNoronha", "Brazil/DeNoronha"),
("Brazil/East", "Brazil/East"),
("Brazil/West", "Brazil/West"),
("CET", "CET"),
("CST6CDT", "CST6CDT"),
("Canada/Atlantic", "Canada/Atlantic"),
("Canada/Central", "Canada/Central"),
("Canada/Eastern", "Canada/Eastern"),
("Canada/Mountain", "Canada/Mountain"),
("Canada/Newfoundland", "Canada/Newfoundland"),
("Canada/Pacific", "Canada/Pacific"),
("Canada/Saskatchewan", "Canada/Saskatchewan"),
("Canada/Yukon", "Canada/Yukon"),
("Chile/Continental", "Chile/Continental"),
("Chile/EasterIsland", "Chile/EasterIsland"),
("Cuba", "Cuba"),
("EET", "EET"),
("EST", "EST"),
("EST5EDT", "EST5EDT"),
("Egypt", "Egypt"),
("Eire", "Eire"),
("Etc/GMT", "Etc/GMT"),
("Etc/GMT+0", "Etc/GMT+0"),
("Etc/GMT+1", "Etc/GMT+1"),
("Etc/GMT+10", "Etc/GMT+10"),
("Etc/GMT+11", "Etc/GMT+11"),
("Etc/GMT+12", "Etc/GMT+12"),
("Etc/GMT+2", "Etc/GMT+2"),
("Etc/GMT+3", "Etc/GMT+3"),
("Etc/GMT+4", "Etc/GMT+4"),
("Etc/GMT+5", "Etc/GMT+5"),
("Etc/GMT+6", "Etc/GMT+6"),
("Etc/GMT+7", "Etc/GMT+7"),
("Etc/GMT+8", "Etc/GMT+8"),
("Etc/GMT+9", "Etc/GMT+9"),
("Etc/GMT-0", "Etc/GMT-0"),
("Etc/GMT-1", "Etc/GMT-1"),
("Etc/GMT-10", "Etc/GMT-10"),
("Etc/GMT-11", "Etc/GMT-11"),
("Etc/GMT-12", "Etc/GMT-12"),
("Etc/GMT-13", "Etc/GMT-13"),
("Etc/GMT-14", "Etc/GMT-14"),
("Etc/GMT-2", "Etc/GMT-2"),
("Etc/GMT-3", "Etc/GMT-3"),
("Etc/GMT-4", "Etc/GMT-4"),
("Etc/GMT-5", "Etc/GMT-5"),
("Etc/GMT-6", "Etc/GMT-6"),
("Etc/GMT-7", "Etc/GMT-7"),
("Etc/GMT-8", "Etc/GMT-8"),
("Etc/GMT-9", "Etc/GMT-9"),
("Etc/GMT0", "Etc/GMT0"),
("Etc/Greenwich", "Etc/Greenwich"),
("Etc/UCT", "Etc/UCT"),
("Etc/UTC", "Etc/UTC"),
("Etc/Universal", "Etc/Universal"),
("Etc/Zulu", "Etc/Zulu"),
("Europe/Amsterdam", "Europe/Amsterdam"),
("Europe/Andorra", "Europe/Andorra"),
("Europe/Astrakhan", "Europe/Astrakhan"),
("Europe/Athens", "Europe/Athens"),
("Europe/Belfast", "Europe/Belfast"),
("Europe/Belgrade", "Europe/Belgrade"),
("Europe/Berlin", "Europe/Berlin"),
("Europe/Bratislava", "Europe/Bratislava"),
("Europe/Brussels", "Europe/Brussels"),
("Europe/Bucharest", "Europe/Bucharest"),
("Europe/Budapest", "Europe/Budapest"),
("Europe/Busingen", "Europe/Busingen"),
("Europe/Chisinau", "Europe/Chisinau"),
("Europe/Copenhagen", "Europe/Copenhagen"),
("Europe/Dublin", "Europe/Dublin"),
("Europe/Gibraltar", "Europe/Gibraltar"),
("Europe/Guernsey", "Europe/Guernsey"),
("Europe/Helsinki", "Europe/Helsinki"),
("Europe/Isle_of_Man", "Europe/Isle_of_Man"),
("Europe/Istanbul", "Europe/Istanbul"),
("Europe/Jersey", "Europe/Jersey"),
("Europe/Kaliningrad", "Europe/Kaliningrad"),
("Europe/Kiev", "Europe/Kiev"),
("Europe/Kirov", "Europe/Kirov"),
("Europe/Kyiv", "Europe/Kyiv"),
("Europe/Lisbon", "Europe/Lisbon"),
("Europe/Ljubljana", "Europe/Ljubljana"),
("Europe/London", "Europe/London"),
("Europe/Luxembourg", "Europe/Luxembourg"),
("Europe/Madrid", "Europe/Madrid"),
("Europe/Malta", "Europe/Malta"),
("Europe/Mariehamn", "Europe/Mariehamn"),
("Europe/Minsk", "Europe/Minsk"),
("Europe/Monaco", "Europe/Monaco"),
("Europe/Moscow", "Europe/Moscow"),
("Europe/Nicosia", "Europe/Nicosia"),
("Europe/Oslo", "Europe/Oslo"),
("Europe/Paris", "Europe/Paris"),
("Europe/Podgorica", "Europe/Podgorica"),
("Europe/Prague", "Europe/Prague"),
("Europe/Riga", "Europe/Riga"),
("Europe/Rome", "Europe/Rome"),
("Europe/Samara", "Europe/Samara"),
("Europe/San_Marino", "Europe/San_Marino"),
("Europe/Sarajevo", "Europe/Sarajevo"),
("Europe/Saratov", "Europe/Saratov"),
("Europe/Simferopol", "Europe/Simferopol"),
("Europe/Skopje", "Europe/Skopje"),
("Europe/Sofia", "Europe/Sofia"),
("Europe/Stockholm", "Europe/Stockholm"),
("Europe/Tallinn", "Europe/Tallinn"),
("Europe/Tirane", "Europe/Tirane"),
("Europe/Tiraspol", "Europe/Tiraspol"),
("Europe/Ulyanovsk", "Europe/Ulyanovsk"),
("Europe/Uzhgorod", "Europe/Uzhgorod"),
("Europe/Vaduz", "Europe/Vaduz"),
("Europe/Vatican", "Europe/Vatican"),
("Europe/Vienna", "Europe/Vienna"),
("Europe/Vilnius", "Europe/Vilnius"),
("Europe/Volgograd", "Europe/Volgograd"),
("Europe/Warsaw", "Europe/Warsaw"),
("Europe/Zagreb", "Europe/Zagreb"),
("Europe/Zaporozhye", "Europe/Zaporozhye"),
("Europe/Zurich", "Europe/Zurich"),
("GB", "GB"),
("GB-Eire", "GB-Eire"),
("GMT", "GMT"),
("GMT+0", "GMT+0"),
("GMT-0", "GMT-0"),
("GMT0", "GMT0"),
("Greenwich", "Greenwich"),
("HST", "HST"),
("Hongkong", "Hongkong"),
("Iceland", "Iceland"),
("Indian/Antananarivo", "Indian/Antananarivo"),
("Indian/Chagos", "Indian/Chagos"),
("Indian/Christmas", "Indian/Christmas"),
("Indian/Cocos", "Indian/Cocos"),
("Indian/Comoro", "Indian/Comoro"),
("Indian/Kerguelen", "Indian/Kerguelen"),
("Indian/Mahe", "Indian/Mahe"),
("Indian/Maldives", "Indian/Maldives"),
("Indian/Mauritius", "Indian/Mauritius"),
("Indian/Mayotte", "Indian/Mayotte"),
("Indian/Reunion", "Indian/Reunion"),
("Iran", "Iran"),
("Israel", "Israel"),
("Jamaica", "Jamaica"),
("Japan", "Japan"),
("Kwajalein", "Kwajalein"),
("Libya", "Libya"),
("MET", "MET"),
("MST", "MST"),
("MST7MDT", "MST7MDT"),
("Mexico/BajaNorte", "Mexico/BajaNorte"),
("Mexico/BajaSur", "Mexico/BajaSur"),
("Mexico/General", "Mexico/General"),
("NZ", "NZ"),
("NZ-CHAT", "NZ-CHAT"),
("Navajo", "Navajo"),
("PRC", "PRC"),
("PST8PDT", "PST8PDT"),
("Pacific/Apia", "Pacific/Apia"),
("Pacific/Auckland", "Pacific/Auckland"),
("Pacific/Bougainville", "Pacific/Bougainville"),
("Pacific/Chatham", "Pacific/Chatham"),
("Pacific/Chuuk", "Pacific/Chuuk"),
("Pacific/Easter", "Pacific/Easter"),
("Pacific/Efate", "Pacific/Efate"),
("Pacific/Enderbury", "Pacific/Enderbury"),
("Pacific/Fakaofo", "Pacific/Fakaofo"),
("Pacific/Fiji", "Pacific/Fiji"),
("Pacific/Funafuti", "Pacific/Funafuti"),
("Pacific/Galapagos", "Pacific/Galapagos"),
("Pacific/Gambier", "Pacific/Gambier"),
("Pacific/Guadalcanal", "Pacific/Guadalcanal"),
("Pacific/Guam", "Pacific/Guam"),
("Pacific/Honolulu", "Pacific/Honolulu"),
("Pacific/Johnston", "Pacific/Johnston"),
("Pacific/Kanton", "Pacific/Kanton"),
("Pacific/Kiritimati", "Pacific/Kiritimati"),
("Pacific/Kosrae", "Pacific/Kosrae"),
("Pacific/Kwajalein", "Pacific/Kwajalein"),
("Pacific/Majuro", "Pacific/Majuro"),
("Pacific/Marquesas", "Pacific/Marquesas"),
("Pacific/Midway", "Pacific/Midway"),
("Pacific/Nauru", "Pacific/Nauru"),
("Pacific/Niue", "Pacific/Niue"),
("Pacific/Norfolk", "Pacific/Norfolk"),
("Pacific/Noumea", "Pacific/Noumea"),
("Pacific/Pago_Pago", "Pacific/Pago_Pago"),
("Pacific/Palau", "Pacific/Palau"),
("Pacific/Pitcairn", "Pacific/Pitcairn"),
("Pacific/Pohnpei", "Pacific/Pohnpei"),
("Pacific/Ponape", "Pacific/Ponape"),
("Pacific/Port_Moresby", "Pacific/Port_Moresby"),
("Pacific/Rarotonga", "Pacific/Rarotonga"),
("Pacific/Saipan", "Pacific/Saipan"),
("Pacific/Samoa", "Pacific/Samoa"),
("Pacific/Tahiti", "Pacific/Tahiti"),
("Pacific/Tarawa", "Pacific/Tarawa"),
("Pacific/Tongatapu", "Pacific/Tongatapu"),
("Pacific/Truk", "Pacific/Truk"),
("Pacific/Wake", "Pacific/Wake"),
("Pacific/Wallis", "Pacific/Wallis"),
("Pacific/Yap", "Pacific/Yap"),
("Poland", "Poland"),
("Portugal", "Portugal"),
("ROC", "ROC"),
("ROK", "ROK"),
("Singapore", "Singapore"),
("Turkey", "Turkey"),
("UCT", "UCT"),
("US/Alaska", "US/Alaska"),
("US/Aleutian", "US/Aleutian"),
("US/Arizona", "US/Arizona"),
("US/Central", "US/Central"),
("US/East-Indiana", "US/East-Indiana"),
("US/Eastern", "US/Eastern"),
("US/Hawaii", "US/Hawaii"),
("US/Indiana-Starke", "US/Indiana-Starke"),
("US/Michigan", "US/Michigan"),
("US/Mountain", "US/Mountain"),
("US/Pacific", "US/Pacific"),
("US/Samoa", "US/Samoa"),
("UTC", "UTC"),
("Universal", "Universal"),
("W-SU", "W-SU"),
("WET", "WET"),
("Zulu", "Zulu"),
],
max_length=255,
null=True,
),
),
]

View File

@@ -0,0 +1,631 @@
# Generated by Django 4.1.7 on 2023-02-28 22:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("agents", "0055_alter_agent_time_zone"),
]
operations = [
migrations.AlterField(
model_name="agent",
name="time_zone",
field=models.CharField(
blank=True,
choices=[
("Africa/Abidjan", "Africa/Abidjan"),
("Africa/Accra", "Africa/Accra"),
("Africa/Addis_Ababa", "Africa/Addis_Ababa"),
("Africa/Algiers", "Africa/Algiers"),
("Africa/Asmara", "Africa/Asmara"),
("Africa/Asmera", "Africa/Asmera"),
("Africa/Bamako", "Africa/Bamako"),
("Africa/Bangui", "Africa/Bangui"),
("Africa/Banjul", "Africa/Banjul"),
("Africa/Bissau", "Africa/Bissau"),
("Africa/Blantyre", "Africa/Blantyre"),
("Africa/Brazzaville", "Africa/Brazzaville"),
("Africa/Bujumbura", "Africa/Bujumbura"),
("Africa/Cairo", "Africa/Cairo"),
("Africa/Casablanca", "Africa/Casablanca"),
("Africa/Ceuta", "Africa/Ceuta"),
("Africa/Conakry", "Africa/Conakry"),
("Africa/Dakar", "Africa/Dakar"),
("Africa/Dar_es_Salaam", "Africa/Dar_es_Salaam"),
("Africa/Djibouti", "Africa/Djibouti"),
("Africa/Douala", "Africa/Douala"),
("Africa/El_Aaiun", "Africa/El_Aaiun"),
("Africa/Freetown", "Africa/Freetown"),
("Africa/Gaborone", "Africa/Gaborone"),
("Africa/Harare", "Africa/Harare"),
("Africa/Johannesburg", "Africa/Johannesburg"),
("Africa/Juba", "Africa/Juba"),
("Africa/Kampala", "Africa/Kampala"),
("Africa/Khartoum", "Africa/Khartoum"),
("Africa/Kigali", "Africa/Kigali"),
("Africa/Kinshasa", "Africa/Kinshasa"),
("Africa/Lagos", "Africa/Lagos"),
("Africa/Libreville", "Africa/Libreville"),
("Africa/Lome", "Africa/Lome"),
("Africa/Luanda", "Africa/Luanda"),
("Africa/Lubumbashi", "Africa/Lubumbashi"),
("Africa/Lusaka", "Africa/Lusaka"),
("Africa/Malabo", "Africa/Malabo"),
("Africa/Maputo", "Africa/Maputo"),
("Africa/Maseru", "Africa/Maseru"),
("Africa/Mbabane", "Africa/Mbabane"),
("Africa/Mogadishu", "Africa/Mogadishu"),
("Africa/Monrovia", "Africa/Monrovia"),
("Africa/Nairobi", "Africa/Nairobi"),
("Africa/Ndjamena", "Africa/Ndjamena"),
("Africa/Niamey", "Africa/Niamey"),
("Africa/Nouakchott", "Africa/Nouakchott"),
("Africa/Ouagadougou", "Africa/Ouagadougou"),
("Africa/Porto-Novo", "Africa/Porto-Novo"),
("Africa/Sao_Tome", "Africa/Sao_Tome"),
("Africa/Timbuktu", "Africa/Timbuktu"),
("Africa/Tripoli", "Africa/Tripoli"),
("Africa/Tunis", "Africa/Tunis"),
("Africa/Windhoek", "Africa/Windhoek"),
("America/Adak", "America/Adak"),
("America/Anchorage", "America/Anchorage"),
("America/Anguilla", "America/Anguilla"),
("America/Antigua", "America/Antigua"),
("America/Araguaina", "America/Araguaina"),
(
"America/Argentina/Buenos_Aires",
"America/Argentina/Buenos_Aires",
),
("America/Argentina/Catamarca", "America/Argentina/Catamarca"),
(
"America/Argentina/ComodRivadavia",
"America/Argentina/ComodRivadavia",
),
("America/Argentina/Cordoba", "America/Argentina/Cordoba"),
("America/Argentina/Jujuy", "America/Argentina/Jujuy"),
("America/Argentina/La_Rioja", "America/Argentina/La_Rioja"),
("America/Argentina/Mendoza", "America/Argentina/Mendoza"),
(
"America/Argentina/Rio_Gallegos",
"America/Argentina/Rio_Gallegos",
),
("America/Argentina/Salta", "America/Argentina/Salta"),
("America/Argentina/San_Juan", "America/Argentina/San_Juan"),
("America/Argentina/San_Luis", "America/Argentina/San_Luis"),
("America/Argentina/Tucuman", "America/Argentina/Tucuman"),
("America/Argentina/Ushuaia", "America/Argentina/Ushuaia"),
("America/Aruba", "America/Aruba"),
("America/Asuncion", "America/Asuncion"),
("America/Atikokan", "America/Atikokan"),
("America/Atka", "America/Atka"),
("America/Bahia", "America/Bahia"),
("America/Bahia_Banderas", "America/Bahia_Banderas"),
("America/Barbados", "America/Barbados"),
("America/Belem", "America/Belem"),
("America/Belize", "America/Belize"),
("America/Blanc-Sablon", "America/Blanc-Sablon"),
("America/Boa_Vista", "America/Boa_Vista"),
("America/Bogota", "America/Bogota"),
("America/Boise", "America/Boise"),
("America/Buenos_Aires", "America/Buenos_Aires"),
("America/Cambridge_Bay", "America/Cambridge_Bay"),
("America/Campo_Grande", "America/Campo_Grande"),
("America/Cancun", "America/Cancun"),
("America/Caracas", "America/Caracas"),
("America/Catamarca", "America/Catamarca"),
("America/Cayenne", "America/Cayenne"),
("America/Cayman", "America/Cayman"),
("America/Chicago", "America/Chicago"),
("America/Chihuahua", "America/Chihuahua"),
("America/Ciudad_Juarez", "America/Ciudad_Juarez"),
("America/Coral_Harbour", "America/Coral_Harbour"),
("America/Cordoba", "America/Cordoba"),
("America/Costa_Rica", "America/Costa_Rica"),
("America/Creston", "America/Creston"),
("America/Cuiaba", "America/Cuiaba"),
("America/Curacao", "America/Curacao"),
("America/Danmarkshavn", "America/Danmarkshavn"),
("America/Dawson", "America/Dawson"),
("America/Dawson_Creek", "America/Dawson_Creek"),
("America/Denver", "America/Denver"),
("America/Detroit", "America/Detroit"),
("America/Dominica", "America/Dominica"),
("America/Edmonton", "America/Edmonton"),
("America/Eirunepe", "America/Eirunepe"),
("America/El_Salvador", "America/El_Salvador"),
("America/Ensenada", "America/Ensenada"),
("America/Fort_Nelson", "America/Fort_Nelson"),
("America/Fort_Wayne", "America/Fort_Wayne"),
("America/Fortaleza", "America/Fortaleza"),
("America/Glace_Bay", "America/Glace_Bay"),
("America/Godthab", "America/Godthab"),
("America/Goose_Bay", "America/Goose_Bay"),
("America/Grand_Turk", "America/Grand_Turk"),
("America/Grenada", "America/Grenada"),
("America/Guadeloupe", "America/Guadeloupe"),
("America/Guatemala", "America/Guatemala"),
("America/Guayaquil", "America/Guayaquil"),
("America/Guyana", "America/Guyana"),
("America/Halifax", "America/Halifax"),
("America/Havana", "America/Havana"),
("America/Hermosillo", "America/Hermosillo"),
("America/Indiana/Indianapolis", "America/Indiana/Indianapolis"),
("America/Indiana/Knox", "America/Indiana/Knox"),
("America/Indiana/Marengo", "America/Indiana/Marengo"),
("America/Indiana/Petersburg", "America/Indiana/Petersburg"),
("America/Indiana/Tell_City", "America/Indiana/Tell_City"),
("America/Indiana/Vevay", "America/Indiana/Vevay"),
("America/Indiana/Vincennes", "America/Indiana/Vincennes"),
("America/Indiana/Winamac", "America/Indiana/Winamac"),
("America/Indianapolis", "America/Indianapolis"),
("America/Inuvik", "America/Inuvik"),
("America/Iqaluit", "America/Iqaluit"),
("America/Jamaica", "America/Jamaica"),
("America/Jujuy", "America/Jujuy"),
("America/Juneau", "America/Juneau"),
("America/Kentucky/Louisville", "America/Kentucky/Louisville"),
("America/Kentucky/Monticello", "America/Kentucky/Monticello"),
("America/Knox_IN", "America/Knox_IN"),
("America/Kralendijk", "America/Kralendijk"),
("America/La_Paz", "America/La_Paz"),
("America/Lima", "America/Lima"),
("America/Los_Angeles", "America/Los_Angeles"),
("America/Louisville", "America/Louisville"),
("America/Lower_Princes", "America/Lower_Princes"),
("America/Maceio", "America/Maceio"),
("America/Managua", "America/Managua"),
("America/Manaus", "America/Manaus"),
("America/Marigot", "America/Marigot"),
("America/Martinique", "America/Martinique"),
("America/Matamoros", "America/Matamoros"),
("America/Mazatlan", "America/Mazatlan"),
("America/Mendoza", "America/Mendoza"),
("America/Menominee", "America/Menominee"),
("America/Merida", "America/Merida"),
("America/Metlakatla", "America/Metlakatla"),
("America/Mexico_City", "America/Mexico_City"),
("America/Miquelon", "America/Miquelon"),
("America/Moncton", "America/Moncton"),
("America/Monterrey", "America/Monterrey"),
("America/Montevideo", "America/Montevideo"),
("America/Montreal", "America/Montreal"),
("America/Montserrat", "America/Montserrat"),
("America/Nassau", "America/Nassau"),
("America/New_York", "America/New_York"),
("America/Nipigon", "America/Nipigon"),
("America/Nome", "America/Nome"),
("America/Noronha", "America/Noronha"),
("America/North_Dakota/Beulah", "America/North_Dakota/Beulah"),
("America/North_Dakota/Center", "America/North_Dakota/Center"),
(
"America/North_Dakota/New_Salem",
"America/North_Dakota/New_Salem",
),
("America/Nuuk", "America/Nuuk"),
("America/Ojinaga", "America/Ojinaga"),
("America/Panama", "America/Panama"),
("America/Pangnirtung", "America/Pangnirtung"),
("America/Paramaribo", "America/Paramaribo"),
("America/Phoenix", "America/Phoenix"),
("America/Port-au-Prince", "America/Port-au-Prince"),
("America/Port_of_Spain", "America/Port_of_Spain"),
("America/Porto_Acre", "America/Porto_Acre"),
("America/Porto_Velho", "America/Porto_Velho"),
("America/Puerto_Rico", "America/Puerto_Rico"),
("America/Punta_Arenas", "America/Punta_Arenas"),
("America/Rainy_River", "America/Rainy_River"),
("America/Rankin_Inlet", "America/Rankin_Inlet"),
("America/Recife", "America/Recife"),
("America/Regina", "America/Regina"),
("America/Resolute", "America/Resolute"),
("America/Rio_Branco", "America/Rio_Branco"),
("America/Rosario", "America/Rosario"),
("America/Santa_Isabel", "America/Santa_Isabel"),
("America/Santarem", "America/Santarem"),
("America/Santiago", "America/Santiago"),
("America/Santo_Domingo", "America/Santo_Domingo"),
("America/Sao_Paulo", "America/Sao_Paulo"),
("America/Scoresbysund", "America/Scoresbysund"),
("America/Shiprock", "America/Shiprock"),
("America/Sitka", "America/Sitka"),
("America/St_Barthelemy", "America/St_Barthelemy"),
("America/St_Johns", "America/St_Johns"),
("America/St_Kitts", "America/St_Kitts"),
("America/St_Lucia", "America/St_Lucia"),
("America/St_Thomas", "America/St_Thomas"),
("America/St_Vincent", "America/St_Vincent"),
("America/Swift_Current", "America/Swift_Current"),
("America/Tegucigalpa", "America/Tegucigalpa"),
("America/Thule", "America/Thule"),
("America/Thunder_Bay", "America/Thunder_Bay"),
("America/Tijuana", "America/Tijuana"),
("America/Toronto", "America/Toronto"),
("America/Tortola", "America/Tortola"),
("America/Vancouver", "America/Vancouver"),
("America/Virgin", "America/Virgin"),
("America/Whitehorse", "America/Whitehorse"),
("America/Winnipeg", "America/Winnipeg"),
("America/Yakutat", "America/Yakutat"),
("America/Yellowknife", "America/Yellowknife"),
("Antarctica/Casey", "Antarctica/Casey"),
("Antarctica/Davis", "Antarctica/Davis"),
("Antarctica/DumontDUrville", "Antarctica/DumontDUrville"),
("Antarctica/Macquarie", "Antarctica/Macquarie"),
("Antarctica/Mawson", "Antarctica/Mawson"),
("Antarctica/McMurdo", "Antarctica/McMurdo"),
("Antarctica/Palmer", "Antarctica/Palmer"),
("Antarctica/Rothera", "Antarctica/Rothera"),
("Antarctica/South_Pole", "Antarctica/South_Pole"),
("Antarctica/Syowa", "Antarctica/Syowa"),
("Antarctica/Troll", "Antarctica/Troll"),
("Antarctica/Vostok", "Antarctica/Vostok"),
("Arctic/Longyearbyen", "Arctic/Longyearbyen"),
("Asia/Aden", "Asia/Aden"),
("Asia/Almaty", "Asia/Almaty"),
("Asia/Amman", "Asia/Amman"),
("Asia/Anadyr", "Asia/Anadyr"),
("Asia/Aqtau", "Asia/Aqtau"),
("Asia/Aqtobe", "Asia/Aqtobe"),
("Asia/Ashgabat", "Asia/Ashgabat"),
("Asia/Ashkhabad", "Asia/Ashkhabad"),
("Asia/Atyrau", "Asia/Atyrau"),
("Asia/Baghdad", "Asia/Baghdad"),
("Asia/Bahrain", "Asia/Bahrain"),
("Asia/Baku", "Asia/Baku"),
("Asia/Bangkok", "Asia/Bangkok"),
("Asia/Barnaul", "Asia/Barnaul"),
("Asia/Beirut", "Asia/Beirut"),
("Asia/Bishkek", "Asia/Bishkek"),
("Asia/Brunei", "Asia/Brunei"),
("Asia/Calcutta", "Asia/Calcutta"),
("Asia/Chita", "Asia/Chita"),
("Asia/Choibalsan", "Asia/Choibalsan"),
("Asia/Chongqing", "Asia/Chongqing"),
("Asia/Chungking", "Asia/Chungking"),
("Asia/Colombo", "Asia/Colombo"),
("Asia/Dacca", "Asia/Dacca"),
("Asia/Damascus", "Asia/Damascus"),
("Asia/Dhaka", "Asia/Dhaka"),
("Asia/Dili", "Asia/Dili"),
("Asia/Dubai", "Asia/Dubai"),
("Asia/Dushanbe", "Asia/Dushanbe"),
("Asia/Famagusta", "Asia/Famagusta"),
("Asia/Gaza", "Asia/Gaza"),
("Asia/Harbin", "Asia/Harbin"),
("Asia/Hebron", "Asia/Hebron"),
("Asia/Ho_Chi_Minh", "Asia/Ho_Chi_Minh"),
("Asia/Hong_Kong", "Asia/Hong_Kong"),
("Asia/Hovd", "Asia/Hovd"),
("Asia/Irkutsk", "Asia/Irkutsk"),
("Asia/Istanbul", "Asia/Istanbul"),
("Asia/Jakarta", "Asia/Jakarta"),
("Asia/Jayapura", "Asia/Jayapura"),
("Asia/Jerusalem", "Asia/Jerusalem"),
("Asia/Kabul", "Asia/Kabul"),
("Asia/Kamchatka", "Asia/Kamchatka"),
("Asia/Karachi", "Asia/Karachi"),
("Asia/Kashgar", "Asia/Kashgar"),
("Asia/Kathmandu", "Asia/Kathmandu"),
("Asia/Katmandu", "Asia/Katmandu"),
("Asia/Khandyga", "Asia/Khandyga"),
("Asia/Kolkata", "Asia/Kolkata"),
("Asia/Krasnoyarsk", "Asia/Krasnoyarsk"),
("Asia/Kuala_Lumpur", "Asia/Kuala_Lumpur"),
("Asia/Kuching", "Asia/Kuching"),
("Asia/Kuwait", "Asia/Kuwait"),
("Asia/Macao", "Asia/Macao"),
("Asia/Macau", "Asia/Macau"),
("Asia/Magadan", "Asia/Magadan"),
("Asia/Makassar", "Asia/Makassar"),
("Asia/Manila", "Asia/Manila"),
("Asia/Muscat", "Asia/Muscat"),
("Asia/Nicosia", "Asia/Nicosia"),
("Asia/Novokuznetsk", "Asia/Novokuznetsk"),
("Asia/Novosibirsk", "Asia/Novosibirsk"),
("Asia/Omsk", "Asia/Omsk"),
("Asia/Oral", "Asia/Oral"),
("Asia/Phnom_Penh", "Asia/Phnom_Penh"),
("Asia/Pontianak", "Asia/Pontianak"),
("Asia/Pyongyang", "Asia/Pyongyang"),
("Asia/Qatar", "Asia/Qatar"),
("Asia/Qostanay", "Asia/Qostanay"),
("Asia/Qyzylorda", "Asia/Qyzylorda"),
("Asia/Rangoon", "Asia/Rangoon"),
("Asia/Riyadh", "Asia/Riyadh"),
("Asia/Saigon", "Asia/Saigon"),
("Asia/Sakhalin", "Asia/Sakhalin"),
("Asia/Samarkand", "Asia/Samarkand"),
("Asia/Seoul", "Asia/Seoul"),
("Asia/Shanghai", "Asia/Shanghai"),
("Asia/Singapore", "Asia/Singapore"),
("Asia/Srednekolymsk", "Asia/Srednekolymsk"),
("Asia/Taipei", "Asia/Taipei"),
("Asia/Tashkent", "Asia/Tashkent"),
("Asia/Tbilisi", "Asia/Tbilisi"),
("Asia/Tehran", "Asia/Tehran"),
("Asia/Tel_Aviv", "Asia/Tel_Aviv"),
("Asia/Thimbu", "Asia/Thimbu"),
("Asia/Thimphu", "Asia/Thimphu"),
("Asia/Tokyo", "Asia/Tokyo"),
("Asia/Tomsk", "Asia/Tomsk"),
("Asia/Ujung_Pandang", "Asia/Ujung_Pandang"),
("Asia/Ulaanbaatar", "Asia/Ulaanbaatar"),
("Asia/Ulan_Bator", "Asia/Ulan_Bator"),
("Asia/Urumqi", "Asia/Urumqi"),
("Asia/Ust-Nera", "Asia/Ust-Nera"),
("Asia/Vientiane", "Asia/Vientiane"),
("Asia/Vladivostok", "Asia/Vladivostok"),
("Asia/Yakutsk", "Asia/Yakutsk"),
("Asia/Yangon", "Asia/Yangon"),
("Asia/Yekaterinburg", "Asia/Yekaterinburg"),
("Asia/Yerevan", "Asia/Yerevan"),
("Atlantic/Azores", "Atlantic/Azores"),
("Atlantic/Bermuda", "Atlantic/Bermuda"),
("Atlantic/Canary", "Atlantic/Canary"),
("Atlantic/Cape_Verde", "Atlantic/Cape_Verde"),
("Atlantic/Faeroe", "Atlantic/Faeroe"),
("Atlantic/Faroe", "Atlantic/Faroe"),
("Atlantic/Jan_Mayen", "Atlantic/Jan_Mayen"),
("Atlantic/Madeira", "Atlantic/Madeira"),
("Atlantic/Reykjavik", "Atlantic/Reykjavik"),
("Atlantic/South_Georgia", "Atlantic/South_Georgia"),
("Atlantic/St_Helena", "Atlantic/St_Helena"),
("Atlantic/Stanley", "Atlantic/Stanley"),
("Australia/ACT", "Australia/ACT"),
("Australia/Adelaide", "Australia/Adelaide"),
("Australia/Brisbane", "Australia/Brisbane"),
("Australia/Broken_Hill", "Australia/Broken_Hill"),
("Australia/Canberra", "Australia/Canberra"),
("Australia/Currie", "Australia/Currie"),
("Australia/Darwin", "Australia/Darwin"),
("Australia/Eucla", "Australia/Eucla"),
("Australia/Hobart", "Australia/Hobart"),
("Australia/LHI", "Australia/LHI"),
("Australia/Lindeman", "Australia/Lindeman"),
("Australia/Lord_Howe", "Australia/Lord_Howe"),
("Australia/Melbourne", "Australia/Melbourne"),
("Australia/NSW", "Australia/NSW"),
("Australia/North", "Australia/North"),
("Australia/Perth", "Australia/Perth"),
("Australia/Queensland", "Australia/Queensland"),
("Australia/South", "Australia/South"),
("Australia/Sydney", "Australia/Sydney"),
("Australia/Tasmania", "Australia/Tasmania"),
("Australia/Victoria", "Australia/Victoria"),
("Australia/West", "Australia/West"),
("Australia/Yancowinna", "Australia/Yancowinna"),
("Brazil/Acre", "Brazil/Acre"),
("Brazil/DeNoronha", "Brazil/DeNoronha"),
("Brazil/East", "Brazil/East"),
("Brazil/West", "Brazil/West"),
("CET", "CET"),
("CST6CDT", "CST6CDT"),
("Canada/Atlantic", "Canada/Atlantic"),
("Canada/Central", "Canada/Central"),
("Canada/Eastern", "Canada/Eastern"),
("Canada/Mountain", "Canada/Mountain"),
("Canada/Newfoundland", "Canada/Newfoundland"),
("Canada/Pacific", "Canada/Pacific"),
("Canada/Saskatchewan", "Canada/Saskatchewan"),
("Canada/Yukon", "Canada/Yukon"),
("Chile/Continental", "Chile/Continental"),
("Chile/EasterIsland", "Chile/EasterIsland"),
("Cuba", "Cuba"),
("EET", "EET"),
("EST", "EST"),
("EST5EDT", "EST5EDT"),
("Egypt", "Egypt"),
("Eire", "Eire"),
("Etc/GMT", "Etc/GMT"),
("Etc/GMT+0", "Etc/GMT+0"),
("Etc/GMT+1", "Etc/GMT+1"),
("Etc/GMT+10", "Etc/GMT+10"),
("Etc/GMT+11", "Etc/GMT+11"),
("Etc/GMT+12", "Etc/GMT+12"),
("Etc/GMT+2", "Etc/GMT+2"),
("Etc/GMT+3", "Etc/GMT+3"),
("Etc/GMT+4", "Etc/GMT+4"),
("Etc/GMT+5", "Etc/GMT+5"),
("Etc/GMT+6", "Etc/GMT+6"),
("Etc/GMT+7", "Etc/GMT+7"),
("Etc/GMT+8", "Etc/GMT+8"),
("Etc/GMT+9", "Etc/GMT+9"),
("Etc/GMT-0", "Etc/GMT-0"),
("Etc/GMT-1", "Etc/GMT-1"),
("Etc/GMT-10", "Etc/GMT-10"),
("Etc/GMT-11", "Etc/GMT-11"),
("Etc/GMT-12", "Etc/GMT-12"),
("Etc/GMT-13", "Etc/GMT-13"),
("Etc/GMT-14", "Etc/GMT-14"),
("Etc/GMT-2", "Etc/GMT-2"),
("Etc/GMT-3", "Etc/GMT-3"),
("Etc/GMT-4", "Etc/GMT-4"),
("Etc/GMT-5", "Etc/GMT-5"),
("Etc/GMT-6", "Etc/GMT-6"),
("Etc/GMT-7", "Etc/GMT-7"),
("Etc/GMT-8", "Etc/GMT-8"),
("Etc/GMT-9", "Etc/GMT-9"),
("Etc/GMT0", "Etc/GMT0"),
("Etc/Greenwich", "Etc/Greenwich"),
("Etc/UCT", "Etc/UCT"),
("Etc/UTC", "Etc/UTC"),
("Etc/Universal", "Etc/Universal"),
("Etc/Zulu", "Etc/Zulu"),
("Europe/Amsterdam", "Europe/Amsterdam"),
("Europe/Andorra", "Europe/Andorra"),
("Europe/Astrakhan", "Europe/Astrakhan"),
("Europe/Athens", "Europe/Athens"),
("Europe/Belfast", "Europe/Belfast"),
("Europe/Belgrade", "Europe/Belgrade"),
("Europe/Berlin", "Europe/Berlin"),
("Europe/Bratislava", "Europe/Bratislava"),
("Europe/Brussels", "Europe/Brussels"),
("Europe/Bucharest", "Europe/Bucharest"),
("Europe/Budapest", "Europe/Budapest"),
("Europe/Busingen", "Europe/Busingen"),
("Europe/Chisinau", "Europe/Chisinau"),
("Europe/Copenhagen", "Europe/Copenhagen"),
("Europe/Dublin", "Europe/Dublin"),
("Europe/Gibraltar", "Europe/Gibraltar"),
("Europe/Guernsey", "Europe/Guernsey"),
("Europe/Helsinki", "Europe/Helsinki"),
("Europe/Isle_of_Man", "Europe/Isle_of_Man"),
("Europe/Istanbul", "Europe/Istanbul"),
("Europe/Jersey", "Europe/Jersey"),
("Europe/Kaliningrad", "Europe/Kaliningrad"),
("Europe/Kiev", "Europe/Kiev"),
("Europe/Kirov", "Europe/Kirov"),
("Europe/Kyiv", "Europe/Kyiv"),
("Europe/Lisbon", "Europe/Lisbon"),
("Europe/Ljubljana", "Europe/Ljubljana"),
("Europe/London", "Europe/London"),
("Europe/Luxembourg", "Europe/Luxembourg"),
("Europe/Madrid", "Europe/Madrid"),
("Europe/Malta", "Europe/Malta"),
("Europe/Mariehamn", "Europe/Mariehamn"),
("Europe/Minsk", "Europe/Minsk"),
("Europe/Monaco", "Europe/Monaco"),
("Europe/Moscow", "Europe/Moscow"),
("Europe/Nicosia", "Europe/Nicosia"),
("Europe/Oslo", "Europe/Oslo"),
("Europe/Paris", "Europe/Paris"),
("Europe/Podgorica", "Europe/Podgorica"),
("Europe/Prague", "Europe/Prague"),
("Europe/Riga", "Europe/Riga"),
("Europe/Rome", "Europe/Rome"),
("Europe/Samara", "Europe/Samara"),
("Europe/San_Marino", "Europe/San_Marino"),
("Europe/Sarajevo", "Europe/Sarajevo"),
("Europe/Saratov", "Europe/Saratov"),
("Europe/Simferopol", "Europe/Simferopol"),
("Europe/Skopje", "Europe/Skopje"),
("Europe/Sofia", "Europe/Sofia"),
("Europe/Stockholm", "Europe/Stockholm"),
("Europe/Tallinn", "Europe/Tallinn"),
("Europe/Tirane", "Europe/Tirane"),
("Europe/Tiraspol", "Europe/Tiraspol"),
("Europe/Ulyanovsk", "Europe/Ulyanovsk"),
("Europe/Uzhgorod", "Europe/Uzhgorod"),
("Europe/Vaduz", "Europe/Vaduz"),
("Europe/Vatican", "Europe/Vatican"),
("Europe/Vienna", "Europe/Vienna"),
("Europe/Vilnius", "Europe/Vilnius"),
("Europe/Volgograd", "Europe/Volgograd"),
("Europe/Warsaw", "Europe/Warsaw"),
("Europe/Zagreb", "Europe/Zagreb"),
("Europe/Zaporozhye", "Europe/Zaporozhye"),
("Europe/Zurich", "Europe/Zurich"),
("GB", "GB"),
("GB-Eire", "GB-Eire"),
("GMT", "GMT"),
("GMT+0", "GMT+0"),
("GMT-0", "GMT-0"),
("GMT0", "GMT0"),
("Greenwich", "Greenwich"),
("HST", "HST"),
("Hongkong", "Hongkong"),
("Iceland", "Iceland"),
("Indian/Antananarivo", "Indian/Antananarivo"),
("Indian/Chagos", "Indian/Chagos"),
("Indian/Christmas", "Indian/Christmas"),
("Indian/Cocos", "Indian/Cocos"),
("Indian/Comoro", "Indian/Comoro"),
("Indian/Kerguelen", "Indian/Kerguelen"),
("Indian/Mahe", "Indian/Mahe"),
("Indian/Maldives", "Indian/Maldives"),
("Indian/Mauritius", "Indian/Mauritius"),
("Indian/Mayotte", "Indian/Mayotte"),
("Indian/Reunion", "Indian/Reunion"),
("Iran", "Iran"),
("Israel", "Israel"),
("Jamaica", "Jamaica"),
("Japan", "Japan"),
("Kwajalein", "Kwajalein"),
("Libya", "Libya"),
("MET", "MET"),
("MST", "MST"),
("MST7MDT", "MST7MDT"),
("Mexico/BajaNorte", "Mexico/BajaNorte"),
("Mexico/BajaSur", "Mexico/BajaSur"),
("Mexico/General", "Mexico/General"),
("NZ", "NZ"),
("NZ-CHAT", "NZ-CHAT"),
("Navajo", "Navajo"),
("PRC", "PRC"),
("PST8PDT", "PST8PDT"),
("Pacific/Apia", "Pacific/Apia"),
("Pacific/Auckland", "Pacific/Auckland"),
("Pacific/Bougainville", "Pacific/Bougainville"),
("Pacific/Chatham", "Pacific/Chatham"),
("Pacific/Chuuk", "Pacific/Chuuk"),
("Pacific/Easter", "Pacific/Easter"),
("Pacific/Efate", "Pacific/Efate"),
("Pacific/Enderbury", "Pacific/Enderbury"),
("Pacific/Fakaofo", "Pacific/Fakaofo"),
("Pacific/Fiji", "Pacific/Fiji"),
("Pacific/Funafuti", "Pacific/Funafuti"),
("Pacific/Galapagos", "Pacific/Galapagos"),
("Pacific/Gambier", "Pacific/Gambier"),
("Pacific/Guadalcanal", "Pacific/Guadalcanal"),
("Pacific/Guam", "Pacific/Guam"),
("Pacific/Honolulu", "Pacific/Honolulu"),
("Pacific/Johnston", "Pacific/Johnston"),
("Pacific/Kanton", "Pacific/Kanton"),
("Pacific/Kiritimati", "Pacific/Kiritimati"),
("Pacific/Kosrae", "Pacific/Kosrae"),
("Pacific/Kwajalein", "Pacific/Kwajalein"),
("Pacific/Majuro", "Pacific/Majuro"),
("Pacific/Marquesas", "Pacific/Marquesas"),
("Pacific/Midway", "Pacific/Midway"),
("Pacific/Nauru", "Pacific/Nauru"),
("Pacific/Niue", "Pacific/Niue"),
("Pacific/Norfolk", "Pacific/Norfolk"),
("Pacific/Noumea", "Pacific/Noumea"),
("Pacific/Pago_Pago", "Pacific/Pago_Pago"),
("Pacific/Palau", "Pacific/Palau"),
("Pacific/Pitcairn", "Pacific/Pitcairn"),
("Pacific/Pohnpei", "Pacific/Pohnpei"),
("Pacific/Ponape", "Pacific/Ponape"),
("Pacific/Port_Moresby", "Pacific/Port_Moresby"),
("Pacific/Rarotonga", "Pacific/Rarotonga"),
("Pacific/Saipan", "Pacific/Saipan"),
("Pacific/Samoa", "Pacific/Samoa"),
("Pacific/Tahiti", "Pacific/Tahiti"),
("Pacific/Tarawa", "Pacific/Tarawa"),
("Pacific/Tongatapu", "Pacific/Tongatapu"),
("Pacific/Truk", "Pacific/Truk"),
("Pacific/Wake", "Pacific/Wake"),
("Pacific/Wallis", "Pacific/Wallis"),
("Pacific/Yap", "Pacific/Yap"),
("Poland", "Poland"),
("Portugal", "Portugal"),
("ROC", "ROC"),
("ROK", "ROK"),
("Singapore", "Singapore"),
("Turkey", "Turkey"),
("UCT", "UCT"),
("US/Alaska", "US/Alaska"),
("US/Aleutian", "US/Aleutian"),
("US/Arizona", "US/Arizona"),
("US/Central", "US/Central"),
("US/East-Indiana", "US/East-Indiana"),
("US/Eastern", "US/Eastern"),
("US/Hawaii", "US/Hawaii"),
("US/Indiana-Starke", "US/Indiana-Starke"),
("US/Michigan", "US/Michigan"),
("US/Mountain", "US/Mountain"),
("US/Pacific", "US/Pacific"),
("US/Samoa", "US/Samoa"),
("UTC", "UTC"),
("Universal", "Universal"),
("W-SU", "W-SU"),
("WET", "WET"),
("Zulu", "Zulu"),
],
max_length=255,
null=True,
),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 4.2.3 on 2023-07-18 01:15
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("core", "0037_coresettings_open_ai_model_and_more"),
("agents", "0056_alter_agent_time_zone"),
]
operations = [
migrations.AlterUniqueTogether(
name="agentcustomfield",
unique_together={("agent", "field")},
),
]

View File

@@ -0,0 +1,633 @@
# Generated by Django 4.2.7 on 2023-11-09 19:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("agents", "0057_alter_agentcustomfield_unique_together"),
]
operations = [
migrations.AlterField(
model_name="agent",
name="time_zone",
field=models.CharField(
blank=True,
choices=[
("Africa/Abidjan", "Africa/Abidjan"),
("Africa/Accra", "Africa/Accra"),
("Africa/Addis_Ababa", "Africa/Addis_Ababa"),
("Africa/Algiers", "Africa/Algiers"),
("Africa/Asmara", "Africa/Asmara"),
("Africa/Asmera", "Africa/Asmera"),
("Africa/Bamako", "Africa/Bamako"),
("Africa/Bangui", "Africa/Bangui"),
("Africa/Banjul", "Africa/Banjul"),
("Africa/Bissau", "Africa/Bissau"),
("Africa/Blantyre", "Africa/Blantyre"),
("Africa/Brazzaville", "Africa/Brazzaville"),
("Africa/Bujumbura", "Africa/Bujumbura"),
("Africa/Cairo", "Africa/Cairo"),
("Africa/Casablanca", "Africa/Casablanca"),
("Africa/Ceuta", "Africa/Ceuta"),
("Africa/Conakry", "Africa/Conakry"),
("Africa/Dakar", "Africa/Dakar"),
("Africa/Dar_es_Salaam", "Africa/Dar_es_Salaam"),
("Africa/Djibouti", "Africa/Djibouti"),
("Africa/Douala", "Africa/Douala"),
("Africa/El_Aaiun", "Africa/El_Aaiun"),
("Africa/Freetown", "Africa/Freetown"),
("Africa/Gaborone", "Africa/Gaborone"),
("Africa/Harare", "Africa/Harare"),
("Africa/Johannesburg", "Africa/Johannesburg"),
("Africa/Juba", "Africa/Juba"),
("Africa/Kampala", "Africa/Kampala"),
("Africa/Khartoum", "Africa/Khartoum"),
("Africa/Kigali", "Africa/Kigali"),
("Africa/Kinshasa", "Africa/Kinshasa"),
("Africa/Lagos", "Africa/Lagos"),
("Africa/Libreville", "Africa/Libreville"),
("Africa/Lome", "Africa/Lome"),
("Africa/Luanda", "Africa/Luanda"),
("Africa/Lubumbashi", "Africa/Lubumbashi"),
("Africa/Lusaka", "Africa/Lusaka"),
("Africa/Malabo", "Africa/Malabo"),
("Africa/Maputo", "Africa/Maputo"),
("Africa/Maseru", "Africa/Maseru"),
("Africa/Mbabane", "Africa/Mbabane"),
("Africa/Mogadishu", "Africa/Mogadishu"),
("Africa/Monrovia", "Africa/Monrovia"),
("Africa/Nairobi", "Africa/Nairobi"),
("Africa/Ndjamena", "Africa/Ndjamena"),
("Africa/Niamey", "Africa/Niamey"),
("Africa/Nouakchott", "Africa/Nouakchott"),
("Africa/Ouagadougou", "Africa/Ouagadougou"),
("Africa/Porto-Novo", "Africa/Porto-Novo"),
("Africa/Sao_Tome", "Africa/Sao_Tome"),
("Africa/Timbuktu", "Africa/Timbuktu"),
("Africa/Tripoli", "Africa/Tripoli"),
("Africa/Tunis", "Africa/Tunis"),
("Africa/Windhoek", "Africa/Windhoek"),
("America/Adak", "America/Adak"),
("America/Anchorage", "America/Anchorage"),
("America/Anguilla", "America/Anguilla"),
("America/Antigua", "America/Antigua"),
("America/Araguaina", "America/Araguaina"),
(
"America/Argentina/Buenos_Aires",
"America/Argentina/Buenos_Aires",
),
("America/Argentina/Catamarca", "America/Argentina/Catamarca"),
(
"America/Argentina/ComodRivadavia",
"America/Argentina/ComodRivadavia",
),
("America/Argentina/Cordoba", "America/Argentina/Cordoba"),
("America/Argentina/Jujuy", "America/Argentina/Jujuy"),
("America/Argentina/La_Rioja", "America/Argentina/La_Rioja"),
("America/Argentina/Mendoza", "America/Argentina/Mendoza"),
(
"America/Argentina/Rio_Gallegos",
"America/Argentina/Rio_Gallegos",
),
("America/Argentina/Salta", "America/Argentina/Salta"),
("America/Argentina/San_Juan", "America/Argentina/San_Juan"),
("America/Argentina/San_Luis", "America/Argentina/San_Luis"),
("America/Argentina/Tucuman", "America/Argentina/Tucuman"),
("America/Argentina/Ushuaia", "America/Argentina/Ushuaia"),
("America/Aruba", "America/Aruba"),
("America/Asuncion", "America/Asuncion"),
("America/Atikokan", "America/Atikokan"),
("America/Atka", "America/Atka"),
("America/Bahia", "America/Bahia"),
("America/Bahia_Banderas", "America/Bahia_Banderas"),
("America/Barbados", "America/Barbados"),
("America/Belem", "America/Belem"),
("America/Belize", "America/Belize"),
("America/Blanc-Sablon", "America/Blanc-Sablon"),
("America/Boa_Vista", "America/Boa_Vista"),
("America/Bogota", "America/Bogota"),
("America/Boise", "America/Boise"),
("America/Buenos_Aires", "America/Buenos_Aires"),
("America/Cambridge_Bay", "America/Cambridge_Bay"),
("America/Campo_Grande", "America/Campo_Grande"),
("America/Cancun", "America/Cancun"),
("America/Caracas", "America/Caracas"),
("America/Catamarca", "America/Catamarca"),
("America/Cayenne", "America/Cayenne"),
("America/Cayman", "America/Cayman"),
("America/Chicago", "America/Chicago"),
("America/Chihuahua", "America/Chihuahua"),
("America/Ciudad_Juarez", "America/Ciudad_Juarez"),
("America/Coral_Harbour", "America/Coral_Harbour"),
("America/Cordoba", "America/Cordoba"),
("America/Costa_Rica", "America/Costa_Rica"),
("America/Creston", "America/Creston"),
("America/Cuiaba", "America/Cuiaba"),
("America/Curacao", "America/Curacao"),
("America/Danmarkshavn", "America/Danmarkshavn"),
("America/Dawson", "America/Dawson"),
("America/Dawson_Creek", "America/Dawson_Creek"),
("America/Denver", "America/Denver"),
("America/Detroit", "America/Detroit"),
("America/Dominica", "America/Dominica"),
("America/Edmonton", "America/Edmonton"),
("America/Eirunepe", "America/Eirunepe"),
("America/El_Salvador", "America/El_Salvador"),
("America/Ensenada", "America/Ensenada"),
("America/Fort_Nelson", "America/Fort_Nelson"),
("America/Fort_Wayne", "America/Fort_Wayne"),
("America/Fortaleza", "America/Fortaleza"),
("America/Glace_Bay", "America/Glace_Bay"),
("America/Godthab", "America/Godthab"),
("America/Goose_Bay", "America/Goose_Bay"),
("America/Grand_Turk", "America/Grand_Turk"),
("America/Grenada", "America/Grenada"),
("America/Guadeloupe", "America/Guadeloupe"),
("America/Guatemala", "America/Guatemala"),
("America/Guayaquil", "America/Guayaquil"),
("America/Guyana", "America/Guyana"),
("America/Halifax", "America/Halifax"),
("America/Havana", "America/Havana"),
("America/Hermosillo", "America/Hermosillo"),
("America/Indiana/Indianapolis", "America/Indiana/Indianapolis"),
("America/Indiana/Knox", "America/Indiana/Knox"),
("America/Indiana/Marengo", "America/Indiana/Marengo"),
("America/Indiana/Petersburg", "America/Indiana/Petersburg"),
("America/Indiana/Tell_City", "America/Indiana/Tell_City"),
("America/Indiana/Vevay", "America/Indiana/Vevay"),
("America/Indiana/Vincennes", "America/Indiana/Vincennes"),
("America/Indiana/Winamac", "America/Indiana/Winamac"),
("America/Indianapolis", "America/Indianapolis"),
("America/Inuvik", "America/Inuvik"),
("America/Iqaluit", "America/Iqaluit"),
("America/Jamaica", "America/Jamaica"),
("America/Jujuy", "America/Jujuy"),
("America/Juneau", "America/Juneau"),
("America/Kentucky/Louisville", "America/Kentucky/Louisville"),
("America/Kentucky/Monticello", "America/Kentucky/Monticello"),
("America/Knox_IN", "America/Knox_IN"),
("America/Kralendijk", "America/Kralendijk"),
("America/La_Paz", "America/La_Paz"),
("America/Lima", "America/Lima"),
("America/Los_Angeles", "America/Los_Angeles"),
("America/Louisville", "America/Louisville"),
("America/Lower_Princes", "America/Lower_Princes"),
("America/Maceio", "America/Maceio"),
("America/Managua", "America/Managua"),
("America/Manaus", "America/Manaus"),
("America/Marigot", "America/Marigot"),
("America/Martinique", "America/Martinique"),
("America/Matamoros", "America/Matamoros"),
("America/Mazatlan", "America/Mazatlan"),
("America/Mendoza", "America/Mendoza"),
("America/Menominee", "America/Menominee"),
("America/Merida", "America/Merida"),
("America/Metlakatla", "America/Metlakatla"),
("America/Mexico_City", "America/Mexico_City"),
("America/Miquelon", "America/Miquelon"),
("America/Moncton", "America/Moncton"),
("America/Monterrey", "America/Monterrey"),
("America/Montevideo", "America/Montevideo"),
("America/Montreal", "America/Montreal"),
("America/Montserrat", "America/Montserrat"),
("America/Nassau", "America/Nassau"),
("America/New_York", "America/New_York"),
("America/Nipigon", "America/Nipigon"),
("America/Nome", "America/Nome"),
("America/Noronha", "America/Noronha"),
("America/North_Dakota/Beulah", "America/North_Dakota/Beulah"),
("America/North_Dakota/Center", "America/North_Dakota/Center"),
(
"America/North_Dakota/New_Salem",
"America/North_Dakota/New_Salem",
),
("America/Nuuk", "America/Nuuk"),
("America/Ojinaga", "America/Ojinaga"),
("America/Panama", "America/Panama"),
("America/Pangnirtung", "America/Pangnirtung"),
("America/Paramaribo", "America/Paramaribo"),
("America/Phoenix", "America/Phoenix"),
("America/Port-au-Prince", "America/Port-au-Prince"),
("America/Port_of_Spain", "America/Port_of_Spain"),
("America/Porto_Acre", "America/Porto_Acre"),
("America/Porto_Velho", "America/Porto_Velho"),
("America/Puerto_Rico", "America/Puerto_Rico"),
("America/Punta_Arenas", "America/Punta_Arenas"),
("America/Rainy_River", "America/Rainy_River"),
("America/Rankin_Inlet", "America/Rankin_Inlet"),
("America/Recife", "America/Recife"),
("America/Regina", "America/Regina"),
("America/Resolute", "America/Resolute"),
("America/Rio_Branco", "America/Rio_Branco"),
("America/Rosario", "America/Rosario"),
("America/Santa_Isabel", "America/Santa_Isabel"),
("America/Santarem", "America/Santarem"),
("America/Santiago", "America/Santiago"),
("America/Santo_Domingo", "America/Santo_Domingo"),
("America/Sao_Paulo", "America/Sao_Paulo"),
("America/Scoresbysund", "America/Scoresbysund"),
("America/Shiprock", "America/Shiprock"),
("America/Sitka", "America/Sitka"),
("America/St_Barthelemy", "America/St_Barthelemy"),
("America/St_Johns", "America/St_Johns"),
("America/St_Kitts", "America/St_Kitts"),
("America/St_Lucia", "America/St_Lucia"),
("America/St_Thomas", "America/St_Thomas"),
("America/St_Vincent", "America/St_Vincent"),
("America/Swift_Current", "America/Swift_Current"),
("America/Tegucigalpa", "America/Tegucigalpa"),
("America/Thule", "America/Thule"),
("America/Thunder_Bay", "America/Thunder_Bay"),
("America/Tijuana", "America/Tijuana"),
("America/Toronto", "America/Toronto"),
("America/Tortola", "America/Tortola"),
("America/Vancouver", "America/Vancouver"),
("America/Virgin", "America/Virgin"),
("America/Whitehorse", "America/Whitehorse"),
("America/Winnipeg", "America/Winnipeg"),
("America/Yakutat", "America/Yakutat"),
("America/Yellowknife", "America/Yellowknife"),
("Antarctica/Casey", "Antarctica/Casey"),
("Antarctica/Davis", "Antarctica/Davis"),
("Antarctica/DumontDUrville", "Antarctica/DumontDUrville"),
("Antarctica/Macquarie", "Antarctica/Macquarie"),
("Antarctica/Mawson", "Antarctica/Mawson"),
("Antarctica/McMurdo", "Antarctica/McMurdo"),
("Antarctica/Palmer", "Antarctica/Palmer"),
("Antarctica/Rothera", "Antarctica/Rothera"),
("Antarctica/South_Pole", "Antarctica/South_Pole"),
("Antarctica/Syowa", "Antarctica/Syowa"),
("Antarctica/Troll", "Antarctica/Troll"),
("Antarctica/Vostok", "Antarctica/Vostok"),
("Arctic/Longyearbyen", "Arctic/Longyearbyen"),
("Asia/Aden", "Asia/Aden"),
("Asia/Almaty", "Asia/Almaty"),
("Asia/Amman", "Asia/Amman"),
("Asia/Anadyr", "Asia/Anadyr"),
("Asia/Aqtau", "Asia/Aqtau"),
("Asia/Aqtobe", "Asia/Aqtobe"),
("Asia/Ashgabat", "Asia/Ashgabat"),
("Asia/Ashkhabad", "Asia/Ashkhabad"),
("Asia/Atyrau", "Asia/Atyrau"),
("Asia/Baghdad", "Asia/Baghdad"),
("Asia/Bahrain", "Asia/Bahrain"),
("Asia/Baku", "Asia/Baku"),
("Asia/Bangkok", "Asia/Bangkok"),
("Asia/Barnaul", "Asia/Barnaul"),
("Asia/Beirut", "Asia/Beirut"),
("Asia/Bishkek", "Asia/Bishkek"),
("Asia/Brunei", "Asia/Brunei"),
("Asia/Calcutta", "Asia/Calcutta"),
("Asia/Chita", "Asia/Chita"),
("Asia/Choibalsan", "Asia/Choibalsan"),
("Asia/Chongqing", "Asia/Chongqing"),
("Asia/Chungking", "Asia/Chungking"),
("Asia/Colombo", "Asia/Colombo"),
("Asia/Dacca", "Asia/Dacca"),
("Asia/Damascus", "Asia/Damascus"),
("Asia/Dhaka", "Asia/Dhaka"),
("Asia/Dili", "Asia/Dili"),
("Asia/Dubai", "Asia/Dubai"),
("Asia/Dushanbe", "Asia/Dushanbe"),
("Asia/Famagusta", "Asia/Famagusta"),
("Asia/Gaza", "Asia/Gaza"),
("Asia/Harbin", "Asia/Harbin"),
("Asia/Hebron", "Asia/Hebron"),
("Asia/Ho_Chi_Minh", "Asia/Ho_Chi_Minh"),
("Asia/Hong_Kong", "Asia/Hong_Kong"),
("Asia/Hovd", "Asia/Hovd"),
("Asia/Irkutsk", "Asia/Irkutsk"),
("Asia/Istanbul", "Asia/Istanbul"),
("Asia/Jakarta", "Asia/Jakarta"),
("Asia/Jayapura", "Asia/Jayapura"),
("Asia/Jerusalem", "Asia/Jerusalem"),
("Asia/Kabul", "Asia/Kabul"),
("Asia/Kamchatka", "Asia/Kamchatka"),
("Asia/Karachi", "Asia/Karachi"),
("Asia/Kashgar", "Asia/Kashgar"),
("Asia/Kathmandu", "Asia/Kathmandu"),
("Asia/Katmandu", "Asia/Katmandu"),
("Asia/Khandyga", "Asia/Khandyga"),
("Asia/Kolkata", "Asia/Kolkata"),
("Asia/Krasnoyarsk", "Asia/Krasnoyarsk"),
("Asia/Kuala_Lumpur", "Asia/Kuala_Lumpur"),
("Asia/Kuching", "Asia/Kuching"),
("Asia/Kuwait", "Asia/Kuwait"),
("Asia/Macao", "Asia/Macao"),
("Asia/Macau", "Asia/Macau"),
("Asia/Magadan", "Asia/Magadan"),
("Asia/Makassar", "Asia/Makassar"),
("Asia/Manila", "Asia/Manila"),
("Asia/Muscat", "Asia/Muscat"),
("Asia/Nicosia", "Asia/Nicosia"),
("Asia/Novokuznetsk", "Asia/Novokuznetsk"),
("Asia/Novosibirsk", "Asia/Novosibirsk"),
("Asia/Omsk", "Asia/Omsk"),
("Asia/Oral", "Asia/Oral"),
("Asia/Phnom_Penh", "Asia/Phnom_Penh"),
("Asia/Pontianak", "Asia/Pontianak"),
("Asia/Pyongyang", "Asia/Pyongyang"),
("Asia/Qatar", "Asia/Qatar"),
("Asia/Qostanay", "Asia/Qostanay"),
("Asia/Qyzylorda", "Asia/Qyzylorda"),
("Asia/Rangoon", "Asia/Rangoon"),
("Asia/Riyadh", "Asia/Riyadh"),
("Asia/Saigon", "Asia/Saigon"),
("Asia/Sakhalin", "Asia/Sakhalin"),
("Asia/Samarkand", "Asia/Samarkand"),
("Asia/Seoul", "Asia/Seoul"),
("Asia/Shanghai", "Asia/Shanghai"),
("Asia/Singapore", "Asia/Singapore"),
("Asia/Srednekolymsk", "Asia/Srednekolymsk"),
("Asia/Taipei", "Asia/Taipei"),
("Asia/Tashkent", "Asia/Tashkent"),
("Asia/Tbilisi", "Asia/Tbilisi"),
("Asia/Tehran", "Asia/Tehran"),
("Asia/Tel_Aviv", "Asia/Tel_Aviv"),
("Asia/Thimbu", "Asia/Thimbu"),
("Asia/Thimphu", "Asia/Thimphu"),
("Asia/Tokyo", "Asia/Tokyo"),
("Asia/Tomsk", "Asia/Tomsk"),
("Asia/Ujung_Pandang", "Asia/Ujung_Pandang"),
("Asia/Ulaanbaatar", "Asia/Ulaanbaatar"),
("Asia/Ulan_Bator", "Asia/Ulan_Bator"),
("Asia/Urumqi", "Asia/Urumqi"),
("Asia/Ust-Nera", "Asia/Ust-Nera"),
("Asia/Vientiane", "Asia/Vientiane"),
("Asia/Vladivostok", "Asia/Vladivostok"),
("Asia/Yakutsk", "Asia/Yakutsk"),
("Asia/Yangon", "Asia/Yangon"),
("Asia/Yekaterinburg", "Asia/Yekaterinburg"),
("Asia/Yerevan", "Asia/Yerevan"),
("Atlantic/Azores", "Atlantic/Azores"),
("Atlantic/Bermuda", "Atlantic/Bermuda"),
("Atlantic/Canary", "Atlantic/Canary"),
("Atlantic/Cape_Verde", "Atlantic/Cape_Verde"),
("Atlantic/Faeroe", "Atlantic/Faeroe"),
("Atlantic/Faroe", "Atlantic/Faroe"),
("Atlantic/Jan_Mayen", "Atlantic/Jan_Mayen"),
("Atlantic/Madeira", "Atlantic/Madeira"),
("Atlantic/Reykjavik", "Atlantic/Reykjavik"),
("Atlantic/South_Georgia", "Atlantic/South_Georgia"),
("Atlantic/St_Helena", "Atlantic/St_Helena"),
("Atlantic/Stanley", "Atlantic/Stanley"),
("Australia/ACT", "Australia/ACT"),
("Australia/Adelaide", "Australia/Adelaide"),
("Australia/Brisbane", "Australia/Brisbane"),
("Australia/Broken_Hill", "Australia/Broken_Hill"),
("Australia/Canberra", "Australia/Canberra"),
("Australia/Currie", "Australia/Currie"),
("Australia/Darwin", "Australia/Darwin"),
("Australia/Eucla", "Australia/Eucla"),
("Australia/Hobart", "Australia/Hobart"),
("Australia/LHI", "Australia/LHI"),
("Australia/Lindeman", "Australia/Lindeman"),
("Australia/Lord_Howe", "Australia/Lord_Howe"),
("Australia/Melbourne", "Australia/Melbourne"),
("Australia/NSW", "Australia/NSW"),
("Australia/North", "Australia/North"),
("Australia/Perth", "Australia/Perth"),
("Australia/Queensland", "Australia/Queensland"),
("Australia/South", "Australia/South"),
("Australia/Sydney", "Australia/Sydney"),
("Australia/Tasmania", "Australia/Tasmania"),
("Australia/Victoria", "Australia/Victoria"),
("Australia/West", "Australia/West"),
("Australia/Yancowinna", "Australia/Yancowinna"),
("Brazil/Acre", "Brazil/Acre"),
("Brazil/DeNoronha", "Brazil/DeNoronha"),
("Brazil/East", "Brazil/East"),
("Brazil/West", "Brazil/West"),
("CET", "CET"),
("CST6CDT", "CST6CDT"),
("Canada/Atlantic", "Canada/Atlantic"),
("Canada/Central", "Canada/Central"),
("Canada/Eastern", "Canada/Eastern"),
("Canada/Mountain", "Canada/Mountain"),
("Canada/Newfoundland", "Canada/Newfoundland"),
("Canada/Pacific", "Canada/Pacific"),
("Canada/Saskatchewan", "Canada/Saskatchewan"),
("Canada/Yukon", "Canada/Yukon"),
("Chile/Continental", "Chile/Continental"),
("Chile/EasterIsland", "Chile/EasterIsland"),
("Cuba", "Cuba"),
("EET", "EET"),
("EST", "EST"),
("EST5EDT", "EST5EDT"),
("Egypt", "Egypt"),
("Eire", "Eire"),
("Etc/GMT", "Etc/GMT"),
("Etc/GMT+0", "Etc/GMT+0"),
("Etc/GMT+1", "Etc/GMT+1"),
("Etc/GMT+10", "Etc/GMT+10"),
("Etc/GMT+11", "Etc/GMT+11"),
("Etc/GMT+12", "Etc/GMT+12"),
("Etc/GMT+2", "Etc/GMT+2"),
("Etc/GMT+3", "Etc/GMT+3"),
("Etc/GMT+4", "Etc/GMT+4"),
("Etc/GMT+5", "Etc/GMT+5"),
("Etc/GMT+6", "Etc/GMT+6"),
("Etc/GMT+7", "Etc/GMT+7"),
("Etc/GMT+8", "Etc/GMT+8"),
("Etc/GMT+9", "Etc/GMT+9"),
("Etc/GMT-0", "Etc/GMT-0"),
("Etc/GMT-1", "Etc/GMT-1"),
("Etc/GMT-10", "Etc/GMT-10"),
("Etc/GMT-11", "Etc/GMT-11"),
("Etc/GMT-12", "Etc/GMT-12"),
("Etc/GMT-13", "Etc/GMT-13"),
("Etc/GMT-14", "Etc/GMT-14"),
("Etc/GMT-2", "Etc/GMT-2"),
("Etc/GMT-3", "Etc/GMT-3"),
("Etc/GMT-4", "Etc/GMT-4"),
("Etc/GMT-5", "Etc/GMT-5"),
("Etc/GMT-6", "Etc/GMT-6"),
("Etc/GMT-7", "Etc/GMT-7"),
("Etc/GMT-8", "Etc/GMT-8"),
("Etc/GMT-9", "Etc/GMT-9"),
("Etc/GMT0", "Etc/GMT0"),
("Etc/Greenwich", "Etc/Greenwich"),
("Etc/UCT", "Etc/UCT"),
("Etc/UTC", "Etc/UTC"),
("Etc/Universal", "Etc/Universal"),
("Etc/Zulu", "Etc/Zulu"),
("Europe/Amsterdam", "Europe/Amsterdam"),
("Europe/Andorra", "Europe/Andorra"),
("Europe/Astrakhan", "Europe/Astrakhan"),
("Europe/Athens", "Europe/Athens"),
("Europe/Belfast", "Europe/Belfast"),
("Europe/Belgrade", "Europe/Belgrade"),
("Europe/Berlin", "Europe/Berlin"),
("Europe/Bratislava", "Europe/Bratislava"),
("Europe/Brussels", "Europe/Brussels"),
("Europe/Bucharest", "Europe/Bucharest"),
("Europe/Budapest", "Europe/Budapest"),
("Europe/Busingen", "Europe/Busingen"),
("Europe/Chisinau", "Europe/Chisinau"),
("Europe/Copenhagen", "Europe/Copenhagen"),
("Europe/Dublin", "Europe/Dublin"),
("Europe/Gibraltar", "Europe/Gibraltar"),
("Europe/Guernsey", "Europe/Guernsey"),
("Europe/Helsinki", "Europe/Helsinki"),
("Europe/Isle_of_Man", "Europe/Isle_of_Man"),
("Europe/Istanbul", "Europe/Istanbul"),
("Europe/Jersey", "Europe/Jersey"),
("Europe/Kaliningrad", "Europe/Kaliningrad"),
("Europe/Kiev", "Europe/Kiev"),
("Europe/Kirov", "Europe/Kirov"),
("Europe/Kyiv", "Europe/Kyiv"),
("Europe/Lisbon", "Europe/Lisbon"),
("Europe/Ljubljana", "Europe/Ljubljana"),
("Europe/London", "Europe/London"),
("Europe/Luxembourg", "Europe/Luxembourg"),
("Europe/Madrid", "Europe/Madrid"),
("Europe/Malta", "Europe/Malta"),
("Europe/Mariehamn", "Europe/Mariehamn"),
("Europe/Minsk", "Europe/Minsk"),
("Europe/Monaco", "Europe/Monaco"),
("Europe/Moscow", "Europe/Moscow"),
("Europe/Nicosia", "Europe/Nicosia"),
("Europe/Oslo", "Europe/Oslo"),
("Europe/Paris", "Europe/Paris"),
("Europe/Podgorica", "Europe/Podgorica"),
("Europe/Prague", "Europe/Prague"),
("Europe/Riga", "Europe/Riga"),
("Europe/Rome", "Europe/Rome"),
("Europe/Samara", "Europe/Samara"),
("Europe/San_Marino", "Europe/San_Marino"),
("Europe/Sarajevo", "Europe/Sarajevo"),
("Europe/Saratov", "Europe/Saratov"),
("Europe/Simferopol", "Europe/Simferopol"),
("Europe/Skopje", "Europe/Skopje"),
("Europe/Sofia", "Europe/Sofia"),
("Europe/Stockholm", "Europe/Stockholm"),
("Europe/Tallinn", "Europe/Tallinn"),
("Europe/Tirane", "Europe/Tirane"),
("Europe/Tiraspol", "Europe/Tiraspol"),
("Europe/Ulyanovsk", "Europe/Ulyanovsk"),
("Europe/Uzhgorod", "Europe/Uzhgorod"),
("Europe/Vaduz", "Europe/Vaduz"),
("Europe/Vatican", "Europe/Vatican"),
("Europe/Vienna", "Europe/Vienna"),
("Europe/Vilnius", "Europe/Vilnius"),
("Europe/Volgograd", "Europe/Volgograd"),
("Europe/Warsaw", "Europe/Warsaw"),
("Europe/Zagreb", "Europe/Zagreb"),
("Europe/Zaporozhye", "Europe/Zaporozhye"),
("Europe/Zurich", "Europe/Zurich"),
("Factory", "Factory"),
("GB", "GB"),
("GB-Eire", "GB-Eire"),
("GMT", "GMT"),
("GMT+0", "GMT+0"),
("GMT-0", "GMT-0"),
("GMT0", "GMT0"),
("Greenwich", "Greenwich"),
("HST", "HST"),
("Hongkong", "Hongkong"),
("Iceland", "Iceland"),
("Indian/Antananarivo", "Indian/Antananarivo"),
("Indian/Chagos", "Indian/Chagos"),
("Indian/Christmas", "Indian/Christmas"),
("Indian/Cocos", "Indian/Cocos"),
("Indian/Comoro", "Indian/Comoro"),
("Indian/Kerguelen", "Indian/Kerguelen"),
("Indian/Mahe", "Indian/Mahe"),
("Indian/Maldives", "Indian/Maldives"),
("Indian/Mauritius", "Indian/Mauritius"),
("Indian/Mayotte", "Indian/Mayotte"),
("Indian/Reunion", "Indian/Reunion"),
("Iran", "Iran"),
("Israel", "Israel"),
("Jamaica", "Jamaica"),
("Japan", "Japan"),
("Kwajalein", "Kwajalein"),
("Libya", "Libya"),
("MET", "MET"),
("MST", "MST"),
("MST7MDT", "MST7MDT"),
("Mexico/BajaNorte", "Mexico/BajaNorte"),
("Mexico/BajaSur", "Mexico/BajaSur"),
("Mexico/General", "Mexico/General"),
("NZ", "NZ"),
("NZ-CHAT", "NZ-CHAT"),
("Navajo", "Navajo"),
("PRC", "PRC"),
("PST8PDT", "PST8PDT"),
("Pacific/Apia", "Pacific/Apia"),
("Pacific/Auckland", "Pacific/Auckland"),
("Pacific/Bougainville", "Pacific/Bougainville"),
("Pacific/Chatham", "Pacific/Chatham"),
("Pacific/Chuuk", "Pacific/Chuuk"),
("Pacific/Easter", "Pacific/Easter"),
("Pacific/Efate", "Pacific/Efate"),
("Pacific/Enderbury", "Pacific/Enderbury"),
("Pacific/Fakaofo", "Pacific/Fakaofo"),
("Pacific/Fiji", "Pacific/Fiji"),
("Pacific/Funafuti", "Pacific/Funafuti"),
("Pacific/Galapagos", "Pacific/Galapagos"),
("Pacific/Gambier", "Pacific/Gambier"),
("Pacific/Guadalcanal", "Pacific/Guadalcanal"),
("Pacific/Guam", "Pacific/Guam"),
("Pacific/Honolulu", "Pacific/Honolulu"),
("Pacific/Johnston", "Pacific/Johnston"),
("Pacific/Kanton", "Pacific/Kanton"),
("Pacific/Kiritimati", "Pacific/Kiritimati"),
("Pacific/Kosrae", "Pacific/Kosrae"),
("Pacific/Kwajalein", "Pacific/Kwajalein"),
("Pacific/Majuro", "Pacific/Majuro"),
("Pacific/Marquesas", "Pacific/Marquesas"),
("Pacific/Midway", "Pacific/Midway"),
("Pacific/Nauru", "Pacific/Nauru"),
("Pacific/Niue", "Pacific/Niue"),
("Pacific/Norfolk", "Pacific/Norfolk"),
("Pacific/Noumea", "Pacific/Noumea"),
("Pacific/Pago_Pago", "Pacific/Pago_Pago"),
("Pacific/Palau", "Pacific/Palau"),
("Pacific/Pitcairn", "Pacific/Pitcairn"),
("Pacific/Pohnpei", "Pacific/Pohnpei"),
("Pacific/Ponape", "Pacific/Ponape"),
("Pacific/Port_Moresby", "Pacific/Port_Moresby"),
("Pacific/Rarotonga", "Pacific/Rarotonga"),
("Pacific/Saipan", "Pacific/Saipan"),
("Pacific/Samoa", "Pacific/Samoa"),
("Pacific/Tahiti", "Pacific/Tahiti"),
("Pacific/Tarawa", "Pacific/Tarawa"),
("Pacific/Tongatapu", "Pacific/Tongatapu"),
("Pacific/Truk", "Pacific/Truk"),
("Pacific/Wake", "Pacific/Wake"),
("Pacific/Wallis", "Pacific/Wallis"),
("Pacific/Yap", "Pacific/Yap"),
("Poland", "Poland"),
("Portugal", "Portugal"),
("ROC", "ROC"),
("ROK", "ROK"),
("Singapore", "Singapore"),
("Turkey", "Turkey"),
("UCT", "UCT"),
("US/Alaska", "US/Alaska"),
("US/Aleutian", "US/Aleutian"),
("US/Arizona", "US/Arizona"),
("US/Central", "US/Central"),
("US/East-Indiana", "US/East-Indiana"),
("US/Eastern", "US/Eastern"),
("US/Hawaii", "US/Hawaii"),
("US/Indiana-Starke", "US/Indiana-Starke"),
("US/Michigan", "US/Michigan"),
("US/Mountain", "US/Mountain"),
("US/Pacific", "US/Pacific"),
("US/Samoa", "US/Samoa"),
("UTC", "UTC"),
("Universal", "Universal"),
("W-SU", "W-SU"),
("WET", "WET"),
("Zulu", "Zulu"),
("localtime", "localtime"),
],
max_length=255,
null=True,
),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.2.10 on 2024-02-19 05:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("agents", "0058_alter_agent_time_zone"),
]
operations = [
migrations.AlterField(
model_name="agenthistory",
name="id",
field=models.BigAutoField(primary_key=True, serialize=False),
),
]

View File

@@ -0,0 +1,36 @@
# Generated by Django 4.2.16 on 2024-10-05 20:39
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("core", "0047_alter_coresettings_notify_on_warning_alerts"),
("agents", "0059_alter_agenthistory_id"),
]
operations = [
migrations.AddField(
model_name="agenthistory",
name="collector_all_output",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="agenthistory",
name="custom_field",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="history",
to="core.customfield",
),
),
migrations.AddField(
model_name="agenthistory",
name="save_to_agent_note",
field=models.BooleanField(default=False),
),
]

View File

@@ -1,30 +1,51 @@
import asyncio
import logging
import re
from collections import Counter
from distutils.version import LooseVersion
from typing import Any, Optional, List, Dict, Union, Sequence, cast, TYPE_CHECKING
from django.core.cache import cache
from packaging import version as pyver
from contextlib import suppress
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Union, cast
import msgpack
import nats
import validators
from asgiref.sync import sync_to_async
from core.models import TZ_CHOICES
from django.conf import settings
from django.contrib.postgres.fields import ArrayField
from django.core.cache import cache
from django.db import models
from django.utils import timezone as djangotime
from logs.models import BaseAuditModel, DebugLog
from nats.errors import TimeoutError
from packaging import version as pyver
from packaging.version import Version as LooseVersion
from core.utils import get_core_settings
from agents.utils import get_agent_url
from checks.models import CheckResult
from core.models import TZ_CHOICES
from core.utils import _b64_to_hex, get_core_settings, send_command_with_mesh
from logs.models import BaseAuditModel, DebugLog, PendingAction
from tacticalrmm.constants import (
AGENT_STATUS_OFFLINE,
AGENT_STATUS_ONLINE,
AGENT_STATUS_OVERDUE,
AGENT_TBL_PEND_ACTION_CNT_CACHE_PREFIX,
ONLINE_AGENTS,
AgentHistoryType,
AgentMonType,
AgentPlat,
AlertSeverity,
CheckStatus,
CheckType,
CustomFieldType,
DebugLogType,
GoArch,
PAAction,
PAStatus,
)
from tacticalrmm.helpers import has_script_actions, has_webhook, setup_nats_options
from tacticalrmm.models import PermissionQuerySet
from tacticalrmm.constants import ONLINE_AGENTS
if TYPE_CHECKING:
from alerts.models import Alert, AlertTemplate
from automation.models import Policy
from alerts.models import AlertTemplate, Alert
from autotasks.models import AutomatedTask
from checks.models import Check
from clients.models import Client
@@ -33,6 +54,8 @@ if TYPE_CHECKING:
# type helpers
Disk = Union[Dict[str, Any], str]
logger = logging.getLogger("trmm")
class Agent(BaseAuditModel):
class Meta:
@@ -44,9 +67,12 @@ class Agent(BaseAuditModel):
version = models.CharField(default="0.1.0", max_length=255)
operating_system = models.CharField(null=True, blank=True, max_length=255)
plat = models.CharField(max_length=255, default="windows")
goarch = models.CharField(max_length=255, null=True, blank=True)
plat_release = models.CharField(max_length=255, null=True, blank=True)
plat: "AgentPlat" = models.CharField( # type: ignore
max_length=255, choices=AgentPlat.choices, default=AgentPlat.WINDOWS
)
goarch: "GoArch" = models.CharField( # type: ignore
max_length=255, choices=GoArch.choices, null=True, blank=True
)
hostname = models.CharField(max_length=255)
agent_id = models.CharField(max_length=200, unique=True)
last_seen = models.DateTimeField(null=True, blank=True)
@@ -57,7 +83,9 @@ class Agent(BaseAuditModel):
boot_time = models.FloatField(null=True, blank=True)
logged_in_username = models.CharField(null=True, blank=True, max_length=255)
last_logged_in_user = models.CharField(null=True, blank=True, max_length=255)
monitoring_type = models.CharField(max_length=30)
monitoring_type = models.CharField(
max_length=30, choices=AgentMonType.choices, default=AgentMonType.SERVER
)
description = 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)
@@ -98,6 +126,27 @@ class Agent(BaseAuditModel):
def __str__(self) -> str:
return self.hostname
def save(self, *args, **kwargs):
# prevent recursion since calling set_alert_template() also calls save()
if not hasattr(self, "_processing_set_alert_template"):
self._processing_set_alert_template = False
if self.pk and not self._processing_set_alert_template:
orig = Agent.objects.get(pk=self.pk)
mon_type_changed = self.monitoring_type != orig.monitoring_type
site_changed = self.site_id != orig.site_id
policy_changed = self.policy != orig.policy
block_inherit = (
self.block_policy_inheritance != orig.block_policy_inheritance
)
if mon_type_changed or site_changed or policy_changed or block_inherit:
self._processing_set_alert_template = True
self.set_alert_template()
self._processing_set_alert_template = False
super().save(*args, **kwargs)
@property
def client(self) -> "Client":
return self.site.client
@@ -107,13 +156,14 @@ class Agent(BaseAuditModel):
# return the default timezone unless the timezone is explicity set per agent
if self.time_zone:
return self.time_zone
else:
return get_core_settings().default_time_zone
return get_core_settings().default_time_zone
@property
def is_posix(self) -> bool:
return self.plat == "linux" or self.plat == "darwin"
return self.plat in {AgentPlat.LINUX, AgentPlat.DARWIN}
# DEPRECATED, use goarch instead
@property
def arch(self) -> Optional[str]:
if self.is_posix:
@@ -126,41 +176,70 @@ class Agent(BaseAuditModel):
return "32"
return None
@property
def winagent_dl(self) -> Optional[str]:
if self.arch == "64":
return settings.DL_64
elif self.arch == "32":
return settings.DL_32
return None
def do_update(self, *, token: str = "", force: bool = False) -> str:
ver = settings.LATEST_AGENT_VER
@property
def win_inno_exe(self) -> Optional[str]:
if self.arch == "64":
return f"winagent-v{settings.LATEST_AGENT_VER}.exe"
elif self.arch == "32":
return f"winagent-v{settings.LATEST_AGENT_VER}-x86.exe"
return None
if not self.goarch:
DebugLog.warning(
agent=self,
log_type=DebugLogType.AGENT_ISSUES,
message=f"Unable to determine arch on {self.hostname}({self.agent_id}). Skipping agent update.",
)
return "noarch"
if pyver.parse(self.version) <= pyver.parse("1.3.0"):
return "not supported"
url = get_agent_url(goarch=self.goarch, plat=self.plat, token=token)
bin = f"tacticalagent-v{ver}-{self.plat}-{self.goarch}.exe"
if not force:
if self.pendingactions.filter( # type: ignore
action_type=PAAction.AGENT_UPDATE, status=PAStatus.PENDING
).exists():
self.pendingactions.filter( # type: ignore
action_type=PAAction.AGENT_UPDATE, status=PAStatus.PENDING
).delete()
PendingAction.objects.create(
agent=self,
action_type=PAAction.AGENT_UPDATE,
details={
"url": url,
"version": ver,
"inno": bin,
},
)
nats_data = {
"func": "agentupdate",
"payload": {
"url": url,
"version": ver,
"inno": bin,
},
}
asyncio.run(self.nats_cmd(nats_data, wait=False))
return "created"
@property
def status(self) -> str:
offline = djangotime.now() - djangotime.timedelta(minutes=self.offline_time)
overdue = djangotime.now() - djangotime.timedelta(minutes=self.overdue_time)
now = djangotime.now()
offline = now - djangotime.timedelta(minutes=self.offline_time)
overdue = now - djangotime.timedelta(minutes=self.overdue_time)
if self.last_seen is not None:
if (self.last_seen < offline) and (self.last_seen > overdue):
return "offline"
return AGENT_STATUS_OFFLINE
elif (self.last_seen < offline) and (self.last_seen < overdue):
return "overdue"
return AGENT_STATUS_OVERDUE
else:
return "online"
return AGENT_STATUS_ONLINE
else:
return "offline"
return AGENT_STATUS_OFFLINE
@property
def checks(self) -> Dict[str, Any]:
from checks.models import CheckResult
total, passing, failing, warning, info = 0, 0, 0, 0, 0
for check in self.get_checks_with_policies(exclude_overridden=True):
@@ -168,23 +247,29 @@ class Agent(BaseAuditModel):
if (
not hasattr(check.check_result, "status")
or isinstance(check.check_result, CheckResult)
and check.check_result.status == "passing"
and check.check_result.status == CheckStatus.PASSING
):
passing += 1
elif (
isinstance(check.check_result, CheckResult)
and check.check_result.status == "failing"
and check.check_result.status == CheckStatus.FAILING
):
alert_severity = (
check.check_result.alert_severity
if check.check_type in ["memory", "cpuload", "diskspace", "script"]
if check.check_type
in (
CheckType.MEMORY,
CheckType.CPU_LOAD,
CheckType.DISK_SPACE,
CheckType.SCRIPT,
)
else check.alert_severity
)
if alert_severity == "error":
if alert_severity == AlertSeverity.ERROR:
failing += 1
elif alert_severity == "warning":
elif alert_severity == AlertSeverity.WARNING:
warning += 1
elif alert_severity == "info":
elif alert_severity == AlertSeverity.INFO:
info += 1
ret = {
@@ -197,6 +282,15 @@ class Agent(BaseAuditModel):
}
return ret
@property
def pending_actions_count(self) -> int:
ret = cache.get(f"{AGENT_TBL_PEND_ACTION_CNT_CACHE_PREFIX}{self.pk}")
if ret is None:
ret = self.pendingactions.filter(status=PAStatus.PENDING).count()
cache.set(f"{AGENT_TBL_PEND_ACTION_CNT_CACHE_PREFIX}{self.pk}", ret, 600)
return ret
@property
def cpu_model(self) -> List[str]:
if self.is_posix:
@@ -209,7 +303,20 @@ class Agent(BaseAuditModel):
try:
cpus = self.wmi_detail["cpu"]
for cpu in cpus:
ret.append([x["Name"] for x in cpu if "Name" in x][0])
name = [x["Name"] for x in cpu if "Name" in x][0]
lp, nc = "", ""
with suppress(Exception):
lp = [
x["NumberOfLogicalProcessors"]
for x in cpu
if "NumberOfCores" in x
][0]
nc = [x["NumberOfCores"] for x in cpu if "NumberOfCores" in x][0]
if lp and nc:
cpu_string = f"{name}, {nc}C/{lp}T"
else:
cpu_string = name
ret.append(cpu_string)
return ret
except:
return ["unknown cpu model"]
@@ -273,8 +380,8 @@ class Agent(BaseAuditModel):
if len(ret) == 1:
return cast(str, ret[0])
else:
return ", ".join(ret) if ret else "error getting local ips"
return ", ".join(ret) if ret else "error getting local ips"
@property
def make_model(self) -> str:
@@ -284,7 +391,7 @@ class Agent(BaseAuditModel):
except:
return "error getting make/model"
try:
with suppress(Exception):
comp_sys = self.wmi_detail["comp_sys"][0]
comp_sys_prod = self.wmi_detail["comp_sys_prod"][0]
make = [x["Vendor"] for x in comp_sys_prod if "Vendor" in x][0]
@@ -301,14 +408,10 @@ class Agent(BaseAuditModel):
model = sysfam
return f"{make} {model}"
except:
pass
try:
with suppress(Exception):
comp_sys_prod = self.wmi_detail["comp_sys_prod"][0]
return cast(str, [x["Version"] for x in comp_sys_prod if "Version" in x][0])
except:
pass
return "unknown make/model"
@@ -341,6 +444,23 @@ class Agent(BaseAuditModel):
except:
return ["unknown disk"]
@property
def serial_number(self) -> str:
if self.is_posix:
try:
return self.wmi_detail["serialnumber"]
except:
return ""
try:
return self.wmi_detail["bios"][0][0]["SerialNumber"]
except:
return ""
@property
def hex_mesh_node_id(self) -> str:
return _b64_to_hex(self.mesh_node_id)
@classmethod
def online_agents(cls, min_version: str = "") -> "List[Agent]":
if min_version:
@@ -348,10 +468,14 @@ class Agent(BaseAuditModel):
i
for i in cls.objects.only(*ONLINE_AGENTS)
if pyver.parse(i.version) >= pyver.parse(min_version)
and i.status == "online"
and i.status == AGENT_STATUS_ONLINE
]
return [i for i in cls.objects.only(*ONLINE_AGENTS) if i.status == "online"]
return [
i
for i in cls.objects.only(*ONLINE_AGENTS)
if i.status == AGENT_STATUS_ONLINE
]
def is_supported_script(self, platforms: List[str]) -> bool:
return self.plat.lower() in platforms if platforms else True
@@ -359,7 +483,6 @@ class Agent(BaseAuditModel):
def get_checks_with_policies(
self, exclude_overridden: bool = False
) -> "List[Check]":
if exclude_overridden:
checks = (
list(
@@ -374,12 +497,10 @@ class Agent(BaseAuditModel):
return self.add_check_results(checks)
def get_tasks_with_policies(self) -> "List[AutomatedTask]":
tasks = list(self.autotasks.all()) + self.get_tasks_from_policies()
return self.add_task_results(tasks)
def add_task_results(self, tasks: "List[AutomatedTask]") -> "List[AutomatedTask]":
results = self.taskresults.all() # type: ignore
for task in tasks:
@@ -391,7 +512,6 @@ class Agent(BaseAuditModel):
return tasks
def add_check_results(self, checks: "List[Check]") -> "List[Check]":
results = self.checkresults.all() # type: ignore
for check in checks:
@@ -415,7 +535,7 @@ class Agent(BaseAuditModel):
models.prefetch_related_objects(
[
policy
for policy in [self.policy, site_policy, client_policy, default_policy]
for policy in (self.policy, site_policy, client_policy, default_policy)
if policy
],
"excluded_agents",
@@ -428,24 +548,32 @@ class Agent(BaseAuditModel):
)
return {
"agent_policy": self.policy
if self.policy and not self.policy.is_agent_excluded(self)
else None,
"site_policy": site_policy
if (site_policy and not site_policy.is_agent_excluded(self))
and not self.block_policy_inheritance
else None,
"client_policy": client_policy
if (client_policy and not client_policy.is_agent_excluded(self))
and not self.block_policy_inheritance
and not self.site.block_policy_inheritance
else None,
"default_policy": default_policy
if (default_policy and not default_policy.is_agent_excluded(self))
and not self.block_policy_inheritance
and not self.site.block_policy_inheritance
and not self.client.block_policy_inheritance
else None,
"agent_policy": (
self.policy
if self.policy and not self.policy.is_agent_excluded(self)
else None
),
"site_policy": (
site_policy
if (site_policy and not site_policy.is_agent_excluded(self))
and not self.block_policy_inheritance
else None
),
"client_policy": (
client_policy
if (client_policy and not client_policy.is_agent_excluded(self))
and not self.block_policy_inheritance
and not self.site.block_policy_inheritance
else None
),
"default_policy": (
default_policy
if (default_policy and not default_policy.is_agent_excluded(self))
and not self.block_policy_inheritance
and not self.site.block_policy_inheritance
and not self.client.block_policy_inheritance
else None
),
}
def check_run_interval(self) -> int:
@@ -453,7 +581,6 @@ class Agent(BaseAuditModel):
# determine if any agent checks have a custom interval and set the lowest interval
for check in self.get_checks_with_policies():
if check.run_interval and check.run_interval < interval:
# don't allow check runs less than 15s
interval = 15 if check.run_interval < 15 else check.run_interval
@@ -468,13 +595,19 @@ class Agent(BaseAuditModel):
wait: bool = False,
run_on_any: bool = False,
history_pk: int = 0,
run_as_user: bool = False,
env_vars: list[str] = [],
) -> Any:
from scripts.models import Script
script = Script.objects.get(pk=scriptpk)
# always override if set on script model
if script.run_as_user:
run_as_user = True
parsed_args = script.parse_script_args(self, script.shell, args)
parsed_env_vars = script.parse_script_env_vars(self, script.shell, env_vars)
data = {
"func": "runscriptfull" if full else "runscript",
@@ -484,6 +617,10 @@ class Agent(BaseAuditModel):
"code": script.code,
"shell": script.shell,
},
"run_as_user": run_as_user,
"env_vars": parsed_env_vars,
"nushell_enable_config": settings.NUSHELL_ENABLE_CONFIG,
"deno_default_permissions": settings.DENO_DEFAULT_PERMISSIONS,
}
if history_pk != 0:
@@ -519,7 +656,7 @@ class Agent(BaseAuditModel):
def approve_updates(self) -> None:
patch_policy = self.get_patch_policy()
severity_list = list()
severity_list = []
if patch_policy.critical == "approve":
severity_list.append("Critical")
@@ -551,17 +688,14 @@ class Agent(BaseAuditModel):
if not agent_policy:
agent_policy = WinUpdatePolicy.objects.create(agent=self)
# Get the list of policies applied to the agent and select the
# highest priority one.
policies = self.get_agent_policies()
processed_policies: List[int] = list()
for _, policy in policies.items():
if (
policy
and policy.active
and policy.pk not in processed_policies
and policy.winupdatepolicy.exists()
):
if policy and policy.active and policy.winupdatepolicy.exists():
patch_policy = policy.winupdatepolicy.first()
break
# if policy still doesn't exist return the agent patch policy
if not patch_policy:
@@ -613,7 +747,7 @@ class Agent(BaseAuditModel):
policies = self.get_agent_policies()
# loop through all policies applied to agent and return an alert_template if found
processed_policies: List[int] = list()
processed_policies: List[int] = []
for key, policy in policies.items():
# default alert_template will override a default policy with alert template applied
if (
@@ -678,10 +812,10 @@ class Agent(BaseAuditModel):
cache_key = f"agent_{self.agent_id}_checks"
elif self.policy:
cache_key = f"site_{self.monitoring_type}_{self.site_id}_policy_{self.policy_id}_checks"
cache_key = f"site_{self.monitoring_type}_{self.plat}_{self.site_id}_policy_{self.policy_id}_checks"
else:
cache_key = f"site_{self.monitoring_type}_{self.site_id}_checks"
cache_key = f"site_{self.monitoring_type}_{self.plat}_{self.site_id}_checks"
cached_checks = cache.get(cache_key)
if isinstance(cached_checks, list):
@@ -703,10 +837,10 @@ class Agent(BaseAuditModel):
cache_key = f"agent_{self.agent_id}_tasks"
elif self.policy:
cache_key = f"site_{self.monitoring_type}_{self.site_id}_policy_{self.policy_id}_tasks"
cache_key = f"site_{self.monitoring_type}_{self.plat}_{self.site_id}_policy_{self.policy_id}_tasks"
else:
cache_key = f"site_{self.monitoring_type}_{self.site_id}_tasks"
cache_key = f"site_{self.monitoring_type}_{self.plat}_{self.site_id}_tasks"
cached_tasks = cache.get(cache_key)
if isinstance(cached_tasks, list):
@@ -714,25 +848,15 @@ class Agent(BaseAuditModel):
else:
# get agent tasks based on policies
tasks = Policy.get_policy_tasks(self)
cache.set(f"site_{self.site_id}_tasks", tasks, 600)
cache.set(cache_key, tasks, 600)
return tasks
def _do_nats_debug(self, agent: "Agent", message: str) -> None:
DebugLog.error(agent=agent, log_type="agent_issues", message=message)
async def nats_cmd(
self, data: Dict[Any, Any], timeout: int = 30, wait: bool = True
) -> Any:
options = {
"servers": f"tls://{settings.ALLOWED_HOSTS[0]}:4222",
"user": "tacticalrmm",
"password": settings.SECRET_KEY,
"connect_timeout": 3,
"max_reconnect_attempts": 2,
}
opts = setup_nats_options()
try:
nc = await nats.connect(**options)
nc = await nats.connect(**opts)
except:
return "natsdown"
@@ -748,9 +872,7 @@ class Agent(BaseAuditModel):
ret = msgpack.loads(msg.data)
except Exception as e:
ret = str(e)
await sync_to_async(self._do_nats_debug, thread_sensitive=False)(
agent=self, message=ret
)
logger.error(e)
await nc.close()
return ret
@@ -759,6 +881,41 @@ class Agent(BaseAuditModel):
await nc.flush()
await nc.close()
def recover(self, mode: str, mesh_uri: str, wait: bool = True) -> tuple[str, bool]:
"""
Return type: tuple(message: str, error: bool)
"""
if mode == "tacagent":
if self.plat == AgentPlat.LINUX:
cmd = "systemctl restart tacticalagent.service"
shell = 3
elif self.plat == AgentPlat.DARWIN:
cmd = "launchctl kickstart -k system/tacticalagent"
shell = 3
else:
cmd = "net stop tacticalrmm & taskkill /F /IM tacticalrmm.exe & net start tacticalrmm"
shell = 1
asyncio.run(
send_command_with_mesh(cmd, mesh_uri, self.mesh_node_id, shell, 0)
)
return "ok", False
elif mode == "mesh":
data = {"func": "recover", "payload": {"mode": mode}}
if wait:
r = asyncio.run(self.nats_cmd(data, timeout=20))
if r == "ok":
return "ok", False
else:
return str(r), True
else:
asyncio.run(self.nats_cmd(data, timeout=20, wait=False))
return "ok", False
return "invalid", True
@staticmethod
def serialize(agent: "Agent") -> Dict[str, Any]:
# serializes the agent and returns json
@@ -767,7 +924,7 @@ class Agent(BaseAuditModel):
return AgentAuditSerializer(agent).data
def delete_superseded_updates(self) -> None:
try:
with suppress(Exception):
pks = [] # list of pks to delete
kbs = list(self.winupdates.values_list("kb", flat=True))
d = Counter(kbs)
@@ -778,8 +935,10 @@ class Agent(BaseAuditModel):
# extract the version from the title and sort from oldest to newest
# skip if no version info is available therefore nothing to parse
try:
matches = r"(Version|Versão)"
pattern = r"\(" + matches + r"(.*?)\)"
vers = [
re.search(r"\(Version(.*?)\)", i).group(1).strip()
re.search(pattern, i, flags=re.IGNORECASE).group(2).strip()
for i in titles
]
sorted_vers = sorted(vers, key=LooseVersion)
@@ -792,24 +951,26 @@ class Agent(BaseAuditModel):
pks = list(set(pks))
self.winupdates.filter(pk__in=pks).delete()
except:
pass
def should_create_alert(
self, alert_template: "Optional[AlertTemplate]" = None
) -> bool:
return bool(
has_agent_notification = (
self.overdue_dashboard_alert
or self.overdue_email_alert
or self.overdue_text_alert
or (
alert_template
and (
alert_template.agent_always_alert
or alert_template.agent_always_email
or alert_template.agent_always_text
)
)
)
has_alert_template_notification = alert_template and (
alert_template.agent_always_alert
or alert_template.agent_always_email
or alert_template.agent_always_text
)
return bool(
has_agent_notification
or has_alert_template_notification
or has_webhook(alert_template, "agent")
or has_script_actions(alert_template, "agent")
)
def send_outage_email(self) -> None:
@@ -903,60 +1064,54 @@ class AgentCustomField(models.Model):
default=list,
)
class Meta:
unique_together = (("agent", "field"),)
def __str__(self) -> str:
return self.field.name
@property
def value(self) -> Union[List[Any], bool, str]:
if self.field.type == "multiple":
if self.field.type == CustomFieldType.MULTIPLE:
return cast(List[str], self.multiple_value)
elif self.field.type == "checkbox":
elif self.field.type == CustomFieldType.CHECKBOX:
return self.bool_value
else:
return cast(str, self.string_value)
return cast(str, self.string_value)
def save_to_field(self, value: Union[List[Any], bool, str]) -> None:
if self.field.type in [
"text",
"number",
"single",
"datetime",
]:
if self.field.type in (
CustomFieldType.TEXT,
CustomFieldType.NUMBER,
CustomFieldType.SINGLE,
CustomFieldType.DATETIME,
):
self.string_value = cast(str, value)
self.save()
elif self.field.type == "multiple":
elif self.field.type == CustomFieldType.MULTIPLE:
self.multiple_value = value.split(",")
self.save()
elif self.field.type == "checkbox":
elif self.field.type == CustomFieldType.CHECKBOX:
self.bool_value = bool(value)
self.save()
AGENT_HISTORY_TYPES = (
("task_run", "Task Run"),
("script_run", "Script Run"),
("cmd_run", "CMD Run"),
)
AGENT_HISTORY_STATUS = (("success", "Success"), ("failure", "Failure"))
class AgentHistory(models.Model):
objects = PermissionQuerySet.as_manager()
id = models.BigAutoField(primary_key=True)
agent = models.ForeignKey(
Agent,
related_name="history",
on_delete=models.CASCADE,
)
time = models.DateTimeField(auto_now_add=True)
type = models.CharField(
max_length=50, choices=AGENT_HISTORY_TYPES, default="cmd_run"
type: "AgentHistoryType" = models.CharField(
max_length=50,
choices=AgentHistoryType.choices,
default=AgentHistoryType.CMD_RUN,
)
command = models.TextField(null=True, blank=True, default="")
status = models.CharField(
max_length=50, choices=AGENT_HISTORY_STATUS, default="success"
)
username = models.CharField(max_length=255, default="system")
results = models.TextField(null=True, blank=True)
script = models.ForeignKey(
@@ -967,6 +1122,15 @@ class AgentHistory(models.Model):
on_delete=models.SET_NULL,
)
script_results = models.JSONField(null=True, blank=True)
custom_field = models.ForeignKey(
"core.CustomField",
null=True,
blank=True,
related_name="history",
on_delete=models.SET_NULL,
)
collector_all_output = models.BooleanField(default=False)
save_to_agent_note = models.BooleanField(default=False)
def __str__(self) -> str:
return f"{self.agent.hostname} - {self.type}"

View File

@@ -27,6 +27,9 @@ class AgentPerms(permissions.BasePermission):
class RecoverAgentPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool:
if "agent_id" not in view.kwargs.keys():
return _has_perm(r, "can_recover_agents")
return _has_perm(r, "can_recover_agents") and _has_perm_on_agent(
r.user, view.kwargs["agent_id"]
)
@@ -44,13 +47,6 @@ class UpdateAgentPerms(permissions.BasePermission):
return _has_perm(r, "can_update_agents")
class PingAgentPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool:
return _has_perm(r, "can_ping_agents") and _has_perm_on_agent(
r.user, view.kwargs["agent_id"]
)
class ManageProcPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool:
return _has_perm(r, "can_manage_procs") and _has_perm_on_agent(
@@ -93,10 +89,8 @@ class RunScriptPerms(permissions.BasePermission):
class AgentNotesPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool:
# permissions for GET /agents/notes/ endpoint
if r.method == "GET":
# permissions for /agents/<agent_id>/notes endpoint
if "agent_id" in view.kwargs.keys():
return _has_perm(r, "can_list_notes") and _has_perm_on_agent(
@@ -119,5 +113,15 @@ class AgentHistoryPerms(permissions.BasePermission):
return _has_perm(r, "can_list_agent_history") and _has_perm_on_agent(
r.user, view.kwargs["agent_id"]
)
else:
return _has_perm(r, "can_list_agent_history")
return _has_perm(r, "can_list_agent_history")
class AgentWOLPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool:
if "agent_id" in view.kwargs.keys():
return _has_perm(r, "can_send_wol") and _has_perm_on_agent(
r.user, view.kwargs["agent_id"]
)
return _has_perm(r, "can_send_wol")

View File

@@ -1,5 +1,6 @@
import pytz
from rest_framework import serializers
from tacticalrmm.constants import AGENT_STATUS_ONLINE, ALL_TIMEZONES
from winupdate.serializers import WinUpdatePolicySerializer
from .models import Agent, AgentCustomField, AgentHistory, Note
@@ -69,7 +70,7 @@ class AgentSerializer(serializers.ModelSerializer):
return policies
def get_all_timezones(self, obj):
return pytz.all_timezones
return ALL_TIMEZONES
class Meta:
model = Agent
@@ -88,29 +89,35 @@ class AgentTableSerializer(serializers.ModelSerializer):
last_seen = serializers.ReadOnlyField()
pending_actions_count = serializers.ReadOnlyField()
has_patches_pending = serializers.ReadOnlyField()
cpu_model = serializers.ReadOnlyField()
graphics = serializers.ReadOnlyField()
local_ips = serializers.ReadOnlyField()
make_model = serializers.ReadOnlyField()
physical_disks = serializers.ReadOnlyField()
serial_number = serializers.ReadOnlyField()
custom_fields = AgentCustomFieldSerializer(many=True, read_only=True)
def get_alert_template(self, obj):
if not obj.alert_template:
return None
else:
return {
"name": obj.alert_template.name,
"always_email": obj.alert_template.agent_always_email,
"always_text": obj.alert_template.agent_always_text,
"always_alert": obj.alert_template.agent_always_alert,
}
return {
"name": obj.alert_template.name,
"always_email": obj.alert_template.agent_always_email,
"always_text": obj.alert_template.agent_always_text,
"always_alert": obj.alert_template.agent_always_alert,
}
def get_logged_username(self, obj) -> str:
if obj.logged_in_username == "None" and obj.status == "online":
if obj.logged_in_username == "None" and obj.status == AGENT_STATUS_ONLINE:
return obj.last_logged_in_user
elif obj.logged_in_username != "None":
return obj.logged_in_username
else:
return "-"
return "-"
def get_italic(self, obj) -> bool:
return obj.logged_in_username == "None" and obj.status == "online"
return obj.logged_in_username == "None" and obj.status == AGENT_STATUS_ONLINE
class Meta:
model = Agent
@@ -139,16 +146,20 @@ class AgentTableSerializer(serializers.ModelSerializer):
"plat",
"goarch",
"has_patches_pending",
"version",
"operating_system",
"public_ip",
"cpu_model",
"graphics",
"local_ips",
"make_model",
"physical_disks",
"custom_fields",
"serial_number",
]
depth = 2
class WinAgentSerializer(serializers.ModelSerializer):
class Meta:
model = Agent
fields = "__all__"
class AgentHostnameSerializer(serializers.ModelSerializer):
client = serializers.ReadOnlyField(source="client.name")
site = serializers.ReadOnlyField(source="site.name")

View File

@@ -1,107 +1,41 @@
import asyncio
import datetime as dt
import random
from time import sleep
from typing import Optional
from typing import TYPE_CHECKING, Optional
from django.core.management import call_command
from django.utils import timezone as djangotime
from agents.models import Agent
from agents.utils import get_agent_url
from core.utils import get_core_settings
from django.conf import settings
from django.utils import timezone as djangotime
from logs.models import DebugLog, PendingAction
from packaging import version as pyver
from logs.models import DebugLog
from scripts.models import Script
from tacticalrmm.celery import app
from tacticalrmm.constants import (
AGENT_DEFER,
AGENT_OUTAGES_LOCK,
AGENT_STATUS_OVERDUE,
CheckStatus,
DebugLogType,
)
from tacticalrmm.helpers import rand_range
from tacticalrmm.utils import redis_lock
def agent_update(agent_id: str, force: bool = False) -> str:
agent = Agent.objects.get(agent_id=agent_id)
if pyver.parse(agent.version) <= pyver.parse("1.3.0"):
return "not supported"
# skip if we can't determine the arch
if agent.arch is None:
DebugLog.warning(
agent=agent,
log_type="agent_issues",
message=f"Unable to determine arch on {agent.hostname}({agent.agent_id}). Skipping agent update.",
)
return "noarch"
version = settings.LATEST_AGENT_VER
inno = agent.win_inno_exe
url = get_agent_url(agent.arch, agent.plat)
if not force:
if agent.pendingactions.filter(
action_type="agentupdate", status="pending"
).exists():
agent.pendingactions.filter(
action_type="agentupdate", status="pending"
).delete()
PendingAction.objects.create(
agent=agent,
action_type="agentupdate",
details={
"url": url,
"version": version,
"inno": inno,
},
)
nats_data = {
"func": "agentupdate",
"payload": {
"url": url,
"version": version,
"inno": inno,
},
}
asyncio.run(agent.nats_cmd(nats_data, wait=False))
return "created"
if TYPE_CHECKING:
from django.db.models.query import QuerySet
@app.task
def force_code_sign(agent_ids: list[str]) -> None:
chunks = (agent_ids[i : i + 70] for i in range(0, len(agent_ids), 70))
for chunk in chunks:
for agent_id in chunk:
agent_update(agent_id=agent_id, force=True)
sleep(2)
@app.task
def send_agent_update_task(agent_ids: list[str]) -> None:
chunks = (agent_ids[i : i + 70] for i in range(0, len(agent_ids), 70))
for chunk in chunks:
for agent_id in chunk:
agent_update(agent_id)
sleep(2)
def send_agent_update_task(*, agent_ids: list[str], token: str, force: bool) -> None:
agents: "QuerySet[Agent]" = Agent.objects.defer(*AGENT_DEFER).filter(
agent_id__in=agent_ids
)
for agent in agents:
agent.do_update(token=token, force=force)
@app.task
def auto_self_agent_update_task() -> None:
core = get_core_settings()
if not core.agent_auto_update:
return
q = Agent.objects.only("agent_id", "version")
agent_ids: list[str] = [
i.agent_id
for i in q
if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
]
chunks = (agent_ids[i : i + 70] for i in range(0, len(agent_ids), 70))
for chunk in chunks:
for agent_id in chunk:
agent_update(agent_id)
sleep(2)
call_command("update_agents")
@app.task
@@ -114,7 +48,7 @@ def agent_outage_email_task(pk: int, alert_interval: Optional[float] = None) ->
return "alert not found"
if not alert.email_sent:
sleep(random.randint(1, 5))
sleep(rand_range(100, 1500))
alert.agent.send_outage_email()
alert.email_sent = djangotime.now()
alert.save(update_fields=["email_sent"])
@@ -123,7 +57,7 @@ def agent_outage_email_task(pk: int, alert_interval: Optional[float] = None) ->
# 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, 5))
sleep(rand_range(100, 1500))
alert.agent.send_outage_email()
alert.email_sent = djangotime.now()
alert.save(update_fields=["email_sent"])
@@ -135,7 +69,7 @@ def agent_outage_email_task(pk: int, alert_interval: Optional[float] = None) ->
def agent_recovery_email_task(pk: int) -> str:
from alerts.models import Alert
sleep(random.randint(1, 5))
sleep(rand_range(100, 1500))
try:
alert = Alert.objects.get(pk=pk)
@@ -159,7 +93,7 @@ def agent_outage_sms_task(pk: int, alert_interval: Optional[float] = None) -> st
return "alert not found"
if not alert.sms_sent:
sleep(random.randint(1, 3))
sleep(rand_range(100, 1500))
alert.agent.send_outage_sms()
alert.sms_sent = djangotime.now()
alert.save(update_fields=["sms_sent"])
@@ -168,7 +102,7 @@ def agent_outage_sms_task(pk: int, alert_interval: Optional[float] = None) -> st
# 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, 3))
sleep(rand_range(100, 1500))
alert.agent.send_outage_sms()
alert.sms_sent = djangotime.now()
alert.save(update_fields=["sms_sent"])
@@ -180,7 +114,7 @@ def agent_outage_sms_task(pk: int, alert_interval: Optional[float] = None) -> st
def agent_recovery_sms_task(pk: int) -> str:
from alerts.models import Alert
sleep(random.randint(1, 3))
sleep(rand_range(100, 1500))
try:
alert = Alert.objects.get(pk=pk)
except Alert.DoesNotExist:
@@ -193,24 +127,20 @@ def agent_recovery_sms_task(pk: int) -> str:
return "ok"
@app.task
def agent_outages_task() -> None:
from alerts.models import Alert
@app.task(bind=True)
def agent_outages_task(self) -> str:
with redis_lock(AGENT_OUTAGES_LOCK, self.app.oid) as acquired:
if not acquired:
return f"{self.app.oid} still running"
agents = Agent.objects.only(
"pk",
"agent_id",
"last_seen",
"offline_time",
"overdue_time",
"overdue_email_alert",
"overdue_text_alert",
"overdue_dashboard_alert",
)
from alerts.models import Alert
from core.tasks import _get_agent_qs
for agent in agents:
if agent.status == "overdue":
Alert.handle_alert_failure(agent)
for agent in _get_agent_qs():
if agent.status == AGENT_STATUS_OVERDUE:
Alert.handle_alert_failure(agent)
return "completed"
@app.task
@@ -221,6 +151,8 @@ def run_script_email_results_task(
emails: list[str],
args: list[str] = [],
history_pk: int = 0,
run_as_user: bool = False,
env_vars: list[str] = [],
):
agent = Agent.objects.get(pk=agentpk)
script = Script.objects.get(pk=scriptpk)
@@ -231,17 +163,19 @@ def run_script_email_results_task(
timeout=nats_timeout,
wait=True,
history_pk=history_pk,
run_as_user=run_as_user,
env_vars=env_vars,
)
if r == "timeout":
DebugLog.error(
agent=agent,
log_type="scripting",
log_type=DebugLogType.SCRIPTING,
message=f"{agent.hostname}({agent.pk}) timed out running script.",
)
return
CORE = get_core_settings()
subject = f"{agent.hostname} {script.name} Results"
subject = f"{agent.client.name}, {agent.site.name}, {agent.hostname} {script.name} Results"
exec_time = "{:.4f}".format(r["execution_time"])
body = (
subject
@@ -289,7 +223,7 @@ def clear_faults_task(older_than_days: int) -> None:
for check in agent.get_checks_with_policies():
# reset check status
if check.check_result:
check.check_result.status = "passing"
check.check_result.status = CheckStatus.PASSING
check.check_result.save(update_fields=["status"])
if check.alert.filter(agent=agent, resolved=False).exists():
alert = Alert.create_or_return_check_alert(check, agent=agent)
@@ -318,3 +252,8 @@ def prune_agent_history(older_than_days: int) -> str:
).delete()
return "ok"
@app.task
def bulk_recover_agents_task() -> None:
call_command("bulk_restart_agents")

View File

View File

@@ -0,0 +1,106 @@
from unittest.mock import patch
from rest_framework.response import Response
from tacticalrmm.test import TacticalTestCase
class TestAgentInstalls(TacticalTestCase):
def setUp(self) -> None:
self.authenticate()
self.setup_coresettings()
self.setup_base_instance()
@patch("agents.utils.generate_linux_install")
@patch("knox.models.AuthToken.objects.create")
@patch("tacticalrmm.utils.generate_winagent_exe")
@patch("core.utils.token_is_valid")
@patch("agents.utils.get_agent_url")
def test_install_agent(
self,
mock_agent_url,
mock_token_valid,
mock_gen_win_exe,
mock_auth,
mock_linux_install,
):
mock_agent_url.return_value = "https://example.com"
mock_token_valid.return_value = "", False
mock_gen_win_exe.return_value = Response("ok")
mock_auth.return_value = "", "token"
mock_linux_install.return_value = Response("ok")
url = "/agents/installer/"
# test windows dynamic exe
data = {
"installMethod": "exe",
"client": self.site2.client.pk,
"site": self.site2.pk,
"expires": 24,
"agenttype": "server",
"power": 0,
"rdp": 1,
"ping": 0,
"goarch": "amd64",
"api": "https://api.example.com",
"fileName": "rmm-client-site-server.exe",
"plat": "windows",
}
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
mock_gen_win_exe.assert_called_with(
client=self.site2.client.pk,
site=self.site2.pk,
agent_type="server",
rdp=1,
ping=0,
power=0,
goarch="amd64",
token="token",
api="https://api.example.com",
file_name="rmm-client-site-server.exe",
)
# test linux no code sign
data["plat"] = "linux"
data["installMethod"] = "bash"
data["rdp"] = 0
data["agenttype"] = "workstation"
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 400)
# test linux
mock_token_valid.return_value = "token123", True
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
mock_linux_install.assert_called_with(
client=str(self.site2.client.pk),
site=str(self.site2.pk),
agent_type="workstation",
arch="amd64",
token="token",
api="https://api.example.com",
download_url="https://example.com",
)
# test manual
data["rdp"] = 1
data["installMethod"] = "manual"
r = self.client.post(url, data, format="json")
self.assertIn("rdp", r.json()["cmd"])
self.assertNotIn("power", r.json()["cmd"])
data.update({"ping": 1, "power": 1})
r = self.client.post(url, data, format="json")
self.assertIn("power", r.json()["cmd"])
self.assertIn("ping", r.json()["cmd"])
# test powershell
data["installMethod"] = "powershell"
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
self.check_not_authenticated("post", url)

View File

@@ -0,0 +1,61 @@
from unittest.mock import patch
from model_bakery import baker
from agents.models import Agent
from tacticalrmm.constants import AgentMonType
from tacticalrmm.test import TacticalTestCase
class AgentSaveTestCase(TacticalTestCase):
def setUp(self):
self.client1 = baker.make("clients.Client")
self.client2 = baker.make("clients.Client")
self.site1 = baker.make("clients.Site", client=self.client1)
self.site2 = baker.make("clients.Site", client=self.client2)
self.site3 = baker.make("clients.Site", client=self.client2)
self.agent = baker.make(
"agents.Agent",
site=self.site1,
monitoring_type=AgentMonType.SERVER,
)
@patch.object(Agent, "set_alert_template")
def test_set_alert_template_called_on_mon_type_change(
self, mock_set_alert_template
):
self.agent.monitoring_type = AgentMonType.WORKSTATION
self.agent.save()
mock_set_alert_template.assert_called_once()
@patch.object(Agent, "set_alert_template")
def test_set_alert_template_called_on_site_change(self, mock_set_alert_template):
self.agent.site = self.site2
self.agent.save()
mock_set_alert_template.assert_called_once()
@patch.object(Agent, "set_alert_template")
def test_set_alert_template_called_on_site_and_montype_change(
self, mock_set_alert_template
):
print(f"before: {self.agent.monitoring_type} site: {self.agent.site_id}")
self.agent.site = self.site3
self.agent.monitoring_type = AgentMonType.WORKSTATION
self.agent.save()
mock_set_alert_template.assert_called_once()
print(f"after: {self.agent.monitoring_type} site: {self.agent.site_id}")
@patch.object(Agent, "set_alert_template")
def test_set_alert_template_not_called_without_changes(
self, mock_set_alert_template
):
self.agent.save()
mock_set_alert_template.assert_not_called()
@patch.object(Agent, "set_alert_template")
def test_set_alert_template_not_called_on_non_relevant_field_change(
self, mock_set_alert_template
):
self.agent.hostname = "abc123"
self.agent.save()
mock_set_alert_template.assert_not_called()

View File

@@ -0,0 +1,313 @@
from unittest.mock import patch
from django.conf import settings
from django.core.management import call_command
from model_bakery import baker
from packaging import version as pyver
from agents.models import Agent
from agents.tasks import auto_self_agent_update_task, send_agent_update_task
from logs.models import PendingAction
from tacticalrmm.constants import (
AGENT_DEFER,
AgentMonType,
AgentPlat,
GoArch,
PAAction,
PAStatus,
)
from tacticalrmm.test import TacticalTestCase
class TestAgentUpdate(TacticalTestCase):
def setUp(self) -> None:
self.authenticate()
self.setup_coresettings()
self.setup_base_instance()
@patch("agents.management.commands.update_agents.send_agent_update_task.delay")
@patch("agents.management.commands.update_agents.token_is_valid")
@patch("agents.management.commands.update_agents.get_core_settings")
def test_update_agents_mgmt_command(self, mock_core, mock_token, mock_update):
mock_token.return_value = ("token123", True)
baker.make_recipe(
"agents.online_agent",
site=self.site1,
monitoring_type=AgentMonType.SERVER,
plat=AgentPlat.WINDOWS,
version="2.0.3",
_quantity=6,
)
baker.make_recipe(
"agents.online_agent",
site=self.site3,
monitoring_type=AgentMonType.WORKSTATION,
plat=AgentPlat.LINUX,
version="2.0.3",
_quantity=5,
)
baker.make_recipe(
"agents.online_agent",
site=self.site2,
monitoring_type=AgentMonType.SERVER,
plat=AgentPlat.WINDOWS,
version=settings.LATEST_AGENT_VER,
_quantity=8,
)
mock_core.return_value.agent_auto_update = False
call_command("update_agents")
mock_update.assert_not_called()
mock_core.return_value.agent_auto_update = True
call_command("update_agents")
ids = list(
Agent.objects.defer(*AGENT_DEFER)
.exclude(version=settings.LATEST_AGENT_VER)
.values_list("agent_id", flat=True)
)
mock_update.assert_called_with(agent_ids=ids, token="token123", force=False)
@patch("agents.models.Agent.nats_cmd")
@patch("agents.models.get_agent_url")
def test_do_update(self, mock_agent_url, mock_nats_cmd):
mock_agent_url.return_value = "https://example.com/123"
# test noarch
agent_noarch = baker.make_recipe(
"agents.online_agent",
site=self.site1,
monitoring_type=AgentMonType.SERVER,
plat=AgentPlat.WINDOWS,
version="2.1.1",
)
r = agent_noarch.do_update(token="", force=True)
self.assertEqual(r, "noarch")
# test too old
agent_old = baker.make_recipe(
"agents.online_agent",
site=self.site2,
monitoring_type=AgentMonType.SERVER,
plat=AgentPlat.WINDOWS,
version="1.3.0",
goarch=GoArch.AMD64,
)
r = agent_old.do_update(token="", force=True)
self.assertEqual(r, "not supported")
win = baker.make_recipe(
"agents.online_agent",
site=self.site1,
monitoring_type=AgentMonType.SERVER,
plat=AgentPlat.WINDOWS,
version="2.1.1",
goarch=GoArch.AMD64,
)
lin = baker.make_recipe(
"agents.online_agent",
site=self.site3,
monitoring_type=AgentMonType.WORKSTATION,
plat=AgentPlat.LINUX,
version="2.1.1",
goarch=GoArch.ARM32,
)
# test windows agent update
r = win.do_update(token="", force=False)
self.assertEqual(r, "created")
mock_nats_cmd.assert_called_with(
{
"func": "agentupdate",
"payload": {
"url": "https://example.com/123",
"version": settings.LATEST_AGENT_VER,
"inno": f"tacticalagent-v{settings.LATEST_AGENT_VER}-windows-amd64.exe",
},
},
wait=False,
)
action1 = PendingAction.objects.get(agent__agent_id=win.agent_id)
self.assertEqual(action1.action_type, PAAction.AGENT_UPDATE)
self.assertEqual(action1.status, PAStatus.PENDING)
self.assertEqual(action1.details["url"], "https://example.com/123")
self.assertEqual(
action1.details["inno"],
f"tacticalagent-v{settings.LATEST_AGENT_VER}-windows-amd64.exe",
)
self.assertEqual(action1.details["version"], settings.LATEST_AGENT_VER)
mock_nats_cmd.reset_mock()
# test linux agent update
r = lin.do_update(token="", force=False)
mock_nats_cmd.assert_called_with(
{
"func": "agentupdate",
"payload": {
"url": "https://example.com/123",
"version": settings.LATEST_AGENT_VER,
"inno": f"tacticalagent-v{settings.LATEST_AGENT_VER}-linux-arm.exe",
},
},
wait=False,
)
action2 = PendingAction.objects.get(agent__agent_id=lin.agent_id)
self.assertEqual(action2.action_type, PAAction.AGENT_UPDATE)
self.assertEqual(action2.status, PAStatus.PENDING)
self.assertEqual(action2.details["url"], "https://example.com/123")
self.assertEqual(
action2.details["inno"],
f"tacticalagent-v{settings.LATEST_AGENT_VER}-linux-arm.exe",
)
self.assertEqual(action2.details["version"], settings.LATEST_AGENT_VER)
# check if old agent update pending actions are being deleted
# should only be 1 pending action at all times
pa_count = win.pendingactions.filter(
action_type=PAAction.AGENT_UPDATE, status=PAStatus.PENDING
).count()
self.assertEqual(pa_count, 1)
for _ in range(4):
win.do_update(token="", force=False)
pa_count = win.pendingactions.filter(
action_type=PAAction.AGENT_UPDATE, status=PAStatus.PENDING
).count()
self.assertEqual(pa_count, 1)
def test_auto_self_agent_update_task(self):
auto_self_agent_update_task()
@patch("agents.models.Agent.do_update")
def test_send_agent_update_task(self, mock_update):
baker.make_recipe(
"agents.online_agent",
site=self.site2,
monitoring_type=AgentMonType.SERVER,
plat=AgentPlat.WINDOWS,
version="2.1.1",
goarch=GoArch.AMD64,
_quantity=6,
)
ids = list(
Agent.objects.defer(*AGENT_DEFER)
.exclude(version=settings.LATEST_AGENT_VER)
.values_list("agent_id", flat=True)
)
send_agent_update_task(agent_ids=ids, token="", force=False)
self.assertEqual(mock_update.call_count, 6)
@patch("agents.views.token_is_valid")
@patch("agents.tasks.send_agent_update_task.delay")
def test_update_agents(self, mock_update, mock_token):
mock_token.return_value = ("", False)
url = "/agents/update/"
baker.make_recipe(
"agents.online_agent",
site=self.site2,
monitoring_type=AgentMonType.SERVER,
plat=AgentPlat.WINDOWS,
version="2.1.1",
goarch=GoArch.AMD64,
_quantity=7,
)
baker.make_recipe(
"agents.online_agent",
site=self.site2,
monitoring_type=AgentMonType.SERVER,
plat=AgentPlat.WINDOWS,
version=settings.LATEST_AGENT_VER,
goarch=GoArch.AMD64,
_quantity=3,
)
baker.make_recipe(
"agents.online_agent",
site=self.site2,
monitoring_type=AgentMonType.WORKSTATION,
plat=AgentPlat.LINUX,
version="2.0.1",
goarch=GoArch.ARM32,
_quantity=9,
)
agent_ids: list[str] = list(
Agent.objects.only("agent_id").values_list("agent_id", flat=True)
)
data = {"agent_ids": agent_ids}
expected: list[str] = [
i.agent_id
for i in Agent.objects.only("agent_id", "version")
if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
]
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
mock_update.assert_called_with(agent_ids=expected, token="", force=False)
self.check_not_authenticated("post", url)
@patch("agents.views.token_is_valid")
@patch("agents.tasks.send_agent_update_task.delay")
def test_agent_update_permissions(self, update_task, mock_token):
mock_token.return_value = ("", False)
agents = baker.make_recipe("agents.agent", _quantity=5)
other_agents = baker.make_recipe("agents.agent", _quantity=7)
url = "/agents/update/"
data = {
"agent_ids": [agent.agent_id for agent in agents]
+ [agent.agent_id for agent in other_agents]
}
# test superuser access
self.check_authorized_superuser("post", url, data)
update_task.assert_called_with(
agent_ids=data["agent_ids"], token="", force=False
)
update_task.reset_mock()
user = self.create_user_with_roles([])
self.client.force_authenticate(user=user)
self.check_not_authorized("post", url, data)
update_task.assert_not_called()
user.role.can_update_agents = True
user.role.save()
self.check_authorized("post", url, data)
update_task.assert_called_with(
agent_ids=data["agent_ids"], token="", force=False
)
update_task.reset_mock()
# limit to client
# user.role.can_view_clients.set([agents[0].client])
# self.check_authorized("post", url, data)
# update_task.assert_called_with(agent_ids=[agent.agent_id for agent in agents])
# update_task.reset_mock()
# add site
# user.role.can_view_sites.set([other_agents[0].site])
# self.check_authorized("post", url, data)
# update_task.assert_called_with(agent_ids=data["agent_ids"])
# update_task.reset_mock()
# remove client permissions
# user.role.can_view_clients.clear()
# self.check_authorized("post", url, data)
# update_task.assert_called_with(
# agent_ids=[agent.agent_id for agent in other_agents]
# )

View File

@@ -0,0 +1,59 @@
from unittest.mock import patch
from django.conf import settings
from agents.utils import generate_linux_install, get_agent_url
from tacticalrmm.test import TacticalTestCase
class TestAgentUtils(TacticalTestCase):
def setUp(self) -> None:
self.authenticate()
self.setup_coresettings()
self.setup_base_instance()
def test_get_agent_url(self):
ver = settings.LATEST_AGENT_VER
# test without token
r = get_agent_url(goarch="amd64", plat="windows", token="")
expected = f"https://github.com/amidaware/rmmagent/releases/download/v{ver}/tacticalagent-v{ver}-windows-amd64.exe"
self.assertEqual(r, expected)
# test with token
r = get_agent_url(goarch="386", plat="linux", token="token123")
expected = f"https://{settings.AGENTS_URL}version={ver}&arch=386&token=token123&plat=linux&api=api.example.com"
@patch("agents.utils.get_mesh_device_id")
@patch("agents.utils.asyncio.run")
@patch("agents.utils.get_mesh_ws_url")
@patch("agents.utils.get_core_settings")
def test_generate_linux_install(
self, mock_core, mock_mesh, mock_async_run, mock_mesh_device_id
):
mock_mesh_device_id.return_value = "meshdeviceid"
mock_core.return_value.mesh_site = "meshsite"
mock_async_run.return_value = "meshid"
mock_mesh.return_value = "meshws"
r = generate_linux_install(
client="1",
site="1",
agent_type="server",
arch="amd64",
token="token123",
api="api.example.com",
download_url="asdasd3423",
)
ret = r.getvalue().decode("utf-8")
self.assertIn(r"agentDL='asdasd3423'", ret)
self.assertIn(
r"meshDL='meshsite/meshagents?id=meshid&installflags=2&meshinstall=6'", ret
)
self.assertIn(r"apiURL='api.example.com'", ret)
self.assertIn(r"agentDL='asdasd3423'", ret)
self.assertIn(r"token='token123'", ret)
self.assertIn(r"clientID='1'", ret)
self.assertIn(r"siteID='1'", ret)
self.assertIn(r"agentType='server'", ret)

View File

@@ -1,28 +1,32 @@
import json
import os
from itertools import cycle
from unittest.mock import patch
import pytz
from typing import TYPE_CHECKING
from unittest.mock import PropertyMock, patch
from zoneinfo import ZoneInfo
from django.conf import settings
from django.test import modify_settings
from django.utils import timezone as djangotime
from logs.models import PendingAction
from model_bakery import baker
from packaging import version as pyver
from winupdate.models import WinUpdatePolicy
from winupdate.serializers import WinUpdatePolicySerializer
from tacticalrmm.test import TacticalTestCase
from .models import Agent, AgentCustomField, AgentHistory, Note
from .serializers import (
from agents.models import Agent, AgentCustomField, AgentHistory, Note
from agents.serializers import (
AgentHistorySerializer,
AgentHostnameSerializer,
AgentNoteSerializer,
AgentSerializer,
)
from .tasks import auto_self_agent_update_task
from tacticalrmm.constants import (
AGENT_STATUS_OFFLINE,
AGENT_STATUS_ONLINE,
AgentMonType,
CustomFieldModel,
CustomFieldType,
EvtLogNames,
)
from tacticalrmm.test import TacticalTestCase
from winupdate.models import WinUpdatePolicy
from winupdate.serializers import WinUpdatePolicySerializer
if TYPE_CHECKING:
from clients.models import Client, Site
@@ -30,11 +34,6 @@ if TYPE_CHECKING:
base_url = "/agents"
@modify_settings(
MIDDLEWARE={
"remove": "tacticalrmm.middleware.LinuxMiddleware",
}
)
class TestAgentsList(TacticalTestCase):
def setUp(self) -> None:
self.authenticate()
@@ -51,24 +50,27 @@ class TestAgentsList(TacticalTestCase):
site3: "Site" = baker.make("clients.Site", client=company2)
baker.make_recipe(
"agents.online_agent", site=site1, monitoring_type="server", _quantity=15
"agents.online_agent",
site=site1,
monitoring_type=AgentMonType.SERVER,
_quantity=15,
)
baker.make_recipe(
"agents.online_agent",
site=site2,
monitoring_type="workstation",
monitoring_type=AgentMonType.WORKSTATION,
_quantity=10,
)
baker.make_recipe(
"agents.online_agent",
site=site3,
monitoring_type="server",
monitoring_type=AgentMonType.SERVER,
_quantity=4,
)
baker.make_recipe(
"agents.online_agent",
site=site3,
monitoring_type="workstation",
monitoring_type=AgentMonType.WORKSTATION,
_quantity=7,
)
@@ -100,11 +102,6 @@ class TestAgentsList(TacticalTestCase):
self.check_not_authenticated("get", url)
@modify_settings(
MIDDLEWARE={
"remove": "tacticalrmm.middleware.LinuxMiddleware",
}
)
class TestAgentViews(TacticalTestCase):
def setUp(self):
self.authenticate()
@@ -117,6 +114,17 @@ class TestAgentViews(TacticalTestCase):
)
baker.make_recipe("winupdate.winupdate_policy", agent=self.agent)
@patch("agents.tasks.bulk_recover_agents_task.delay")
def test_bulk_agent_recovery(self, mock_task):
mock_task.return_value = None
url = f"{base_url}/bulkrecovery/"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
mock_task.assert_called_once()
self.check_not_authenticated("get", url)
def test_get_agent(self):
url = f"{base_url}/{self.agent.agent_id}/"
@@ -170,7 +178,11 @@ class TestAgentViews(TacticalTestCase):
self.assertEqual(data["run_time_days"], [2, 3, 6])
# test adding custom fields
field = baker.make("core.CustomField", model="agent", type="number")
field = baker.make(
"core.CustomField",
model=CustomFieldModel.AGENT,
type=CustomFieldType.NUMBER,
)
data = {
"site": site.pk,
"description": "asjdk234andasd",
@@ -227,7 +239,7 @@ class TestAgentViews(TacticalTestCase):
self.agent.save(update_fields=["policy"])
_ = self.agent.get_patch_policy()
self.agent.monitoring_type = "workstation"
self.agent.monitoring_type = AgentMonType.WORKSTATION
self.agent.save(update_fields=["monitoring_type"])
_ = self.agent.get_patch_policy()
@@ -239,7 +251,7 @@ class TestAgentViews(TacticalTestCase):
self.coresettings.save(update_fields=["server_policy", "workstation_policy"])
_ = self.agent.get_patch_policy()
self.agent.monitoring_type = "server"
self.agent.monitoring_type = AgentMonType.SERVER
self.agent.save(update_fields=["monitoring_type"])
_ = self.agent.get_patch_policy()
@@ -254,40 +266,6 @@ class TestAgentViews(TacticalTestCase):
self.check_not_authenticated("get", url)
@patch("agents.tasks.send_agent_update_task.delay")
def test_update_agents(self, mock_task):
url = f"{base_url}/update/"
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,
)
agent_ids: list[str] = list(
Agent.objects.only("agent_id", "version").values_list("agent_id", flat=True)
)
data = {"agent_ids": agent_ids}
expected: list[str] = [
i.agent_id
for i in Agent.objects.only("agent_id", "version")
if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
]
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
mock_task.assert_called_with(agent_ids=expected)
self.check_not_authenticated("post", url)
@patch("time.sleep", return_value=None)
@patch("agents.models.Agent.nats_cmd")
def test_agent_ping(self, nats_cmd, mock_sleep):
@@ -296,25 +274,25 @@ class TestAgentViews(TacticalTestCase):
nats_cmd.return_value = "timeout"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
ret = {"name": self.agent.hostname, "status": "offline"}
ret = {"name": self.agent.hostname, "status": AGENT_STATUS_OFFLINE}
self.assertEqual(r.json(), ret)
nats_cmd.return_value = "natsdown"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
ret = {"name": self.agent.hostname, "status": "offline"}
ret = {"name": self.agent.hostname, "status": AGENT_STATUS_OFFLINE}
self.assertEqual(r.json(), ret)
nats_cmd.return_value = "pong"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
ret = {"name": self.agent.hostname, "status": "online"}
ret = {"name": self.agent.hostname, "status": AGENT_STATUS_ONLINE}
self.assertEqual(r.json(), ret)
nats_cmd.return_value = "asdasjdaksdasd"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
ret = {"name": self.agent.hostname, "status": "offline"}
ret = {"name": self.agent.hostname, "status": AGENT_STATUS_OFFLINE}
self.assertEqual(r.json(), ret)
self.check_not_authenticated("get", url)
@@ -374,7 +352,7 @@ class TestAgentViews(TacticalTestCase):
"func": "eventlog",
"timeout": 30,
"payload": {
"logname": "Application",
"logname": EvtLogNames.APPLICATION,
"days": str(22),
},
},
@@ -389,7 +367,7 @@ class TestAgentViews(TacticalTestCase):
"func": "eventlog",
"timeout": 180,
"payload": {
"logname": "Security",
"logname": EvtLogNames.SECURITY,
"days": str(6),
},
},
@@ -425,6 +403,7 @@ class TestAgentViews(TacticalTestCase):
"cmd": "ipconfig",
"shell": "cmd",
"timeout": 30,
"run_as_user": False,
}
mock_ret.return_value = "nt authority\\system"
r = self.client.post(url, data, format="json")
@@ -439,16 +418,20 @@ class TestAgentViews(TacticalTestCase):
@patch("agents.models.Agent.nats_cmd")
def test_reboot_later(self, nats_cmd):
nats_cmd.return_value = "ok"
url = f"{base_url}/{self.agent.agent_id}/reboot/"
data = {
"datetime": "2025-08-29T18:41:02",
}
# ensure we don't allow dates in past
data = {"datetime": "2022-07-11T01:51"}
r = self.client.patch(url, data, format="json")
self.assertEqual(r.status_code, 400)
self.assertEqual(r.data, "Date cannot be set in the past")
nats_cmd.return_value = "ok"
# test with date in future
data["datetime"] = "2027-08-29T18:41"
r = self.client.patch(url, data, format="json")
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data["time"], "August 29, 2025 at 06:41 PM")
self.assertEqual(r.data["time"], "August 29, 2027 at 06:41 PM")
self.assertEqual(r.data["agent"], self.agent.hostname)
nats_data = {
@@ -461,12 +444,12 @@ class TestAgentViews(TacticalTestCase):
"multiple_instances": 2,
"trigger": "runonce",
"name": r.data["task_name"],
"start_year": 2025,
"start_year": 2027,
"start_month": 8,
"start_day": 29,
"start_hour": 18,
"start_min": 41,
"expire_year": 2025,
"expire_year": 2027,
"expire_month": 8,
"expire_day": 29,
"expire_hour": 18,
@@ -489,42 +472,6 @@ class TestAgentViews(TacticalTestCase):
self.check_not_authenticated("patch", url)
def test_install_agent(self):
url = f"{base_url}/installer/"
site = baker.make("clients.Site")
data = {
"client": site.client.pk,
"site": site.pk,
"arch": "64",
"expires": 23,
"installMethod": "manual",
"api": "https://api.example.com",
"agenttype": "server",
"rdp": 1,
"ping": 0,
"power": 0,
"fileName": "rmm-client-site-server.exe",
}
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
data["arch"] = "64"
r = self.client.post(url, data, format="json")
self.assertIn("rdp", r.json()["cmd"])
self.assertNotIn("power", r.json()["cmd"])
data.update({"ping": 1, "power": 1})
r = self.client.post(url, data, format="json")
self.assertIn("power", r.json()["cmd"])
self.assertIn("ping", r.json()["cmd"])
data["installMethod"] = "powershell"
self.assertEqual(r.status_code, 200)
self.check_not_authenticated("post", url)
@patch("meshctrl.utils.get_login_token")
def test_meshcentral_tabs(self, mock_token):
url = f"{base_url}/{self.agent.agent_id}/meshcentral/"
@@ -579,10 +526,9 @@ class TestAgentViews(TacticalTestCase):
@patch("agents.tasks.run_script_email_results_task.delay")
@patch("agents.models.Agent.run_script")
def test_run_script(self, run_script, email_task):
from agents.models import AgentCustomField, AgentHistory, Note
from clients.models import ClientCustomField, SiteCustomField
from .models import AgentCustomField, AgentHistory, Note
run_script.return_value = "ok"
url = f"/agents/{self.agent.agent_id}/runscript/"
script = baker.make_recipe("scripts.script")
@@ -593,6 +539,8 @@ class TestAgentViews(TacticalTestCase):
"output": "wait",
"args": [],
"timeout": 15,
"run_as_user": False,
"env_vars": ["hello=world", "foo=bar"],
}
r = self.client.post(url, data, format="json")
@@ -602,7 +550,13 @@ class TestAgentViews(TacticalTestCase):
raise AgentHistory.DoesNotExist
run_script.assert_called_with(
scriptpk=script.pk, args=[], timeout=18, wait=True, history_pk=hist.pk
scriptpk=script.pk,
args=[],
timeout=18,
wait=True,
history_pk=hist.pk,
run_as_user=False,
env_vars=["hello=world", "foo=bar"],
)
run_script.reset_mock()
@@ -614,15 +568,21 @@ class TestAgentViews(TacticalTestCase):
"timeout": 15,
"emailMode": "default",
"emails": ["admin@example.com", "bob@example.com"],
"run_as_user": False,
"env_vars": ["hello=world", "foo=bar"],
}
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
hist = AgentHistory.objects.filter(agent=self.agent, script=script).last()
email_task.assert_called_with(
agentpk=self.agent.pk,
scriptpk=script.pk,
nats_timeout=18,
emails=[],
args=["abc", "123"],
history_pk=hist.pk,
run_as_user=False,
env_vars=["hello=world", "foo=bar"],
)
email_task.reset_mock()
@@ -630,12 +590,16 @@ class TestAgentViews(TacticalTestCase):
data["emailMode"] = "custom"
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
hist = AgentHistory.objects.filter(agent=self.agent, script=script).last()
email_task.assert_called_with(
agentpk=self.agent.pk,
scriptpk=script.pk,
nats_timeout=18,
emails=["admin@example.com", "bob@example.com"],
args=["abc", "123"],
history_pk=hist.pk,
run_as_user=False,
env_vars=["hello=world", "foo=bar"],
)
# test fire and forget
@@ -644,6 +608,8 @@ class TestAgentViews(TacticalTestCase):
"output": "forget",
"args": ["hello", "world"],
"timeout": 22,
"run_as_user": True,
"env_vars": ["hello=world", "foo=bar"],
}
r = self.client.post(url, data, format="json")
@@ -653,14 +619,19 @@ class TestAgentViews(TacticalTestCase):
raise AgentHistory.DoesNotExist
run_script.assert_called_with(
scriptpk=script.pk, args=["hello", "world"], timeout=25, history_pk=hist.pk
scriptpk=script.pk,
args=["hello", "world"],
timeout=25,
history_pk=hist.pk,
run_as_user=True,
env_vars=["hello=world", "foo=bar"],
)
run_script.reset_mock()
# test collector
# save to agent custom field
custom_field = baker.make("core.CustomField", model="agent")
custom_field = baker.make("core.CustomField", model=CustomFieldModel.AGENT)
data = {
"script": script.pk,
"output": "collector",
@@ -668,6 +639,8 @@ class TestAgentViews(TacticalTestCase):
"timeout": 22,
"custom_field": custom_field.pk,
"save_all_output": True,
"run_as_user": False,
"env_vars": ["hello=world", "foo=bar"],
}
r = self.client.post(url, data, format="json")
@@ -682,6 +655,8 @@ class TestAgentViews(TacticalTestCase):
timeout=25,
wait=True,
history_pk=hist.pk,
run_as_user=False,
env_vars=["hello=world", "foo=bar"],
)
run_script.reset_mock()
@@ -699,6 +674,8 @@ class TestAgentViews(TacticalTestCase):
"timeout": 22,
"custom_field": custom_field.pk,
"save_all_output": False,
"run_as_user": False,
"env_vars": ["hello=world", "foo=bar"],
}
r = self.client.post(url, data, format="json")
@@ -713,6 +690,8 @@ class TestAgentViews(TacticalTestCase):
timeout=25,
wait=True,
history_pk=hist.pk,
run_as_user=False,
env_vars=["hello=world", "foo=bar"],
)
run_script.reset_mock()
@@ -724,7 +703,7 @@ class TestAgentViews(TacticalTestCase):
)
# save to client custom field
custom_field = baker.make("core.CustomField", model="client")
custom_field = baker.make("core.CustomField", model=CustomFieldModel.CLIENT)
data = {
"script": script.pk,
"output": "collector",
@@ -732,6 +711,8 @@ class TestAgentViews(TacticalTestCase):
"timeout": 22,
"custom_field": custom_field.pk,
"save_all_output": False,
"run_as_user": False,
"env_vars": ["hello=world", "foo=bar"],
}
r = self.client.post(url, data, format="json")
@@ -746,6 +727,8 @@ class TestAgentViews(TacticalTestCase):
timeout=25,
wait=True,
history_pk=hist.pk,
run_as_user=False,
env_vars=["hello=world", "foo=bar"],
)
run_script.reset_mock()
@@ -762,6 +745,8 @@ class TestAgentViews(TacticalTestCase):
"output": "note",
"args": ["hello", "world"],
"timeout": 22,
"run_as_user": False,
"env_vars": ["hello=world", "foo=bar"],
}
r = self.client.post(url, data, format="json")
@@ -776,11 +761,74 @@ class TestAgentViews(TacticalTestCase):
timeout=25,
wait=True,
history_pk=hist.pk,
run_as_user=False,
env_vars=["hello=world", "foo=bar"],
)
run_script.reset_mock()
self.assertEqual(Note.objects.get(agent=self.agent).note, "ok")
# test run on server
with patch("core.utils.run_server_script") as mock_run_server_script:
mock_run_server_script.return_value = ("output", "error", 1.23456789, 0)
data = {
"script": script.pk,
"output": "wait",
"args": ["arg1", "arg2"],
"timeout": 15,
"run_as_user": False,
"env_vars": ["key1=val1", "key2=val2"],
"run_on_server": True,
}
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
hist = AgentHistory.objects.filter(agent=self.agent, script=script).last()
if not hist:
raise AgentHistory.DoesNotExist
mock_run_server_script.assert_called_with(
body=script.script_body,
args=script.parse_script_args(self.agent, script.shell, data["args"]),
env_vars=script.parse_script_env_vars(
self.agent, script.shell, data["env_vars"]
),
shell=script.shell,
timeout=18,
)
expected_ret = {
"stdout": "output",
"stderr": "error",
"execution_time": "1.2346",
"retcode": 0,
}
self.assertEqual(r.data, expected_ret)
hist.refresh_from_db()
expected_script_results = {**expected_ret, "id": hist.pk}
self.assertEqual(hist.script_results, expected_script_results)
# test run on server with server scripts disabled
with patch(
"core.models.CoreSettings.server_scripts_enabled",
new_callable=PropertyMock,
) as server_scripts_enabled:
server_scripts_enabled.return_value = False
data = {
"script": script.pk,
"output": "wait",
"args": ["arg1", "arg2"],
"timeout": 15,
"run_as_user": False,
"env_vars": ["key1=val1", "key2=val2"],
"run_on_server": True,
}
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 400)
def test_get_notes(self):
url = f"{base_url}/notes/"
@@ -868,7 +916,6 @@ class TestAgentViews(TacticalTestCase):
self.check_not_authenticated("delete", url)
def test_get_agent_history(self):
# setup data
agent = baker.make_recipe("agents.agent")
history = baker.make("agents.AgentHistory", agent=agent, _quantity=30)
@@ -880,17 +927,12 @@ class TestAgentViews(TacticalTestCase):
# test pulling data
r = self.client.get(url, format="json")
ctx = {"default_tz": pytz.timezone("America/Los_Angeles")}
ctx = {"default_tz": ZoneInfo("America/Los_Angeles")}
data = AgentHistorySerializer(history, many=True, context=ctx).data
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data, data) # type:ignore
@modify_settings(
MIDDLEWARE={
"remove": "tacticalrmm.middleware.LinuxMiddleware",
}
)
class TestAgentViewsNew(TacticalTestCase):
def setUp(self):
self.authenticate()
@@ -925,11 +967,6 @@ class TestAgentViewsNew(TacticalTestCase):
self.check_not_authenticated("post", url)
@modify_settings(
MIDDLEWARE={
"remove": "tacticalrmm.middleware.LinuxMiddleware",
}
)
class TestAgentPermissions(TacticalTestCase):
def setUp(self):
self.setup_client()
@@ -1034,7 +1071,6 @@ class TestAgentPermissions(TacticalTestCase):
@patch("time.sleep")
@patch("agents.models.Agent.nats_cmd", return_value="ok")
def test_agent_actions_permissions(self, nats_cmd, sleep):
agent = baker.make_recipe("agents.agent")
unauthorized_agent = baker.make_recipe("agents.agent")
@@ -1045,7 +1081,6 @@ class TestAgentPermissions(TacticalTestCase):
{"method": "post", "action": "recover", "role": "can_recover_agents"},
{"method": "post", "action": "reboot", "role": "can_reboot_agents"},
{"method": "patch", "action": "reboot", "role": "can_reboot_agents"},
{"method": "get", "action": "ping", "role": "can_ping_agents"},
{"method": "get", "action": "meshcentral", "role": "can_use_mesh"},
{"method": "post", "action": "meshcentral/recover", "role": "can_use_mesh"},
{"method": "get", "action": "processes", "role": "can_manage_procs"},
@@ -1125,55 +1160,6 @@ class TestAgentPermissions(TacticalTestCase):
self.check_authorized("post", url, site_data)
self.check_authorized("post", url, client_data)
@patch("agents.tasks.send_agent_update_task.delay")
def test_agent_update_permissions(self, update_task):
agents = baker.make_recipe("agents.agent", _quantity=5)
other_agents = baker.make_recipe("agents.agent", _quantity=7)
url = f"{base_url}/update/"
data = {
"agent_ids": [agent.agent_id for agent in agents]
+ [agent.agent_id for agent in other_agents]
}
# test superuser access
self.check_authorized_superuser("post", url, data)
update_task.assert_called_with(agent_ids=data["agent_ids"])
update_task.reset_mock()
user = self.create_user_with_roles([])
self.client.force_authenticate(user=user)
self.check_not_authorized("post", url, data)
update_task.assert_not_called()
user.role.can_update_agents = True
user.role.save()
self.check_authorized("post", url, data)
update_task.assert_called_with(agent_ids=data["agent_ids"])
update_task.reset_mock()
# limit to client
# user.role.can_view_clients.set([agents[0].client])
# self.check_authorized("post", url, data)
# update_task.assert_called_with(agent_ids=[agent.agent_id for agent in agents])
# update_task.reset_mock()
# add site
# user.role.can_view_sites.set([other_agents[0].site])
# self.check_authorized("post", url, data)
# update_task.assert_called_with(agent_ids=data["agent_ids"])
# update_task.reset_mock()
# remove client permissions
# user.role.can_view_clients.clear()
# self.check_authorized("post", url, data)
# update_task.assert_called_with(
# agent_ids=[agent.agent_id for agent in other_agents]
# )
def test_get_agent_version_permissions(self):
agents = baker.make_recipe("agents.agent", _quantity=5)
other_agents = baker.make_recipe("agents.agent", _quantity=7)
@@ -1211,7 +1197,6 @@ class TestAgentPermissions(TacticalTestCase):
self.assertEqual(len(response.data["agents"]), 7)
def test_generating_agent_installer_permissions(self):
client = baker.make("clients.Client")
client_site = baker.make("clients.Site", client=client)
site = baker.make("clients.Site")
@@ -1274,7 +1259,6 @@ class TestAgentPermissions(TacticalTestCase):
self.check_not_authorized("post", url, data)
def test_agent_notes_permissions(self):
agent = baker.make_recipe("agents.agent")
notes = baker.make("agents.Note", agent=agent, _quantity=5)
@@ -1363,9 +1347,9 @@ class TestAgentPermissions(TacticalTestCase):
sites = baker.make("clients.Site", _quantity=2)
agent = baker.make_recipe("agents.agent", site=sites[0])
history = baker.make("agents.AgentHistory", agent=agent, _quantity=5)
history = baker.make("agents.AgentHistory", agent=agent, _quantity=5) # noqa
unauthorized_agent = baker.make_recipe("agents.agent", site=sites[1])
unauthorized_history = baker.make(
unauthorized_history = baker.make( # noqa
"agents.AgentHistory", agent=unauthorized_agent, _quantity=6
)
@@ -1403,154 +1387,13 @@ class TestAgentPermissions(TacticalTestCase):
self.check_authorized_superuser("get", unauthorized_url)
@modify_settings(
MIDDLEWARE={
"remove": "tacticalrmm.middleware.LinuxMiddleware",
}
)
class TestAgentTasks(TacticalTestCase):
def setUp(self):
self.authenticate()
self.setup_coresettings()
@patch("agents.utils.get_agent_url")
@patch("agents.models.Agent.nats_cmd")
def test_agent_update(self, nats_cmd, get_url):
get_url.return_value = "https://exe.tacticalrmm.io"
from agents.tasks import agent_update
agent_noarch = baker.make_recipe(
"agents.agent",
operating_system="Error getting OS",
version=settings.LATEST_AGENT_VER,
)
r = agent_update(agent_noarch.agent_id)
self.assertEqual(r, "noarch")
agent_130 = baker.make_recipe(
"agents.agent",
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
version="1.3.0",
)
r = agent_update(agent_130.agent_id)
self.assertEqual(r, "not supported")
# test __without__ code signing
agent64_nosign = baker.make_recipe(
"agents.agent",
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
version="1.4.14",
)
r = agent_update(agent64_nosign.agent_id)
self.assertEqual(r, "created")
action = PendingAction.objects.get(agent__agent_id=agent64_nosign.agent_id)
self.assertEqual(action.action_type, "agentupdate")
self.assertEqual(action.status, "pending")
self.assertEqual(
action.details["url"],
f"https://github.com/amidaware/rmmagent/releases/download/v{settings.LATEST_AGENT_VER}/winagent-v{settings.LATEST_AGENT_VER}.exe",
)
self.assertEqual(
action.details["inno"], f"winagent-v{settings.LATEST_AGENT_VER}.exe"
)
self.assertEqual(action.details["version"], settings.LATEST_AGENT_VER)
nats_cmd.assert_called_with(
{
"func": "agentupdate",
"payload": {
"url": f"https://github.com/amidaware/rmmagent/releases/download/v{settings.LATEST_AGENT_VER}/winagent-v{settings.LATEST_AGENT_VER}.exe",
"version": settings.LATEST_AGENT_VER,
"inno": f"winagent-v{settings.LATEST_AGENT_VER}.exe",
},
},
wait=False,
)
# test __with__ code signing (64 bit)
""" codesign = baker.make("core.CodeSignToken", token="testtoken123")
agent64_sign = baker.make_recipe(
"agents.agent",
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
version="1.4.14",
)
nats_cmd.return_value = "ok"
get_exe.return_value = "https://exe.tacticalrmm.io"
r = agent_update(agent64_sign.pk, codesign.token)
self.assertEqual(r, "created")
nats_cmd.assert_called_with(
{
"func": "agentupdate",
"payload": {
"url": f"https://exe.tacticalrmm.io/api/v1/winagents/?version={settings.LATEST_AGENT_VER}&arch=64&token=testtoken123",
"version": settings.LATEST_AGENT_VER,
"inno": f"winagent-v{settings.LATEST_AGENT_VER}.exe",
},
},
wait=False,
)
action = PendingAction.objects.get(agent__pk=agent64_sign.pk)
self.assertEqual(action.action_type, "agentupdate")
self.assertEqual(action.status, "pending")
# test __with__ code signing (32 bit)
agent32_sign = baker.make_recipe(
"agents.agent",
operating_system="Windows 10 Pro, 32 bit (build 19041.450)",
version="1.4.14",
)
nats_cmd.return_value = "ok"
get_exe.return_value = "https://exe.tacticalrmm.io"
r = agent_update(agent32_sign.pk, codesign.token)
self.assertEqual(r, "created")
nats_cmd.assert_called_with(
{
"func": "agentupdate",
"payload": {
"url": f"https://exe.tacticalrmm.io/api/v1/winagents/?version={settings.LATEST_AGENT_VER}&arch=32&token=testtoken123",
"version": settings.LATEST_AGENT_VER,
"inno": f"winagent-v{settings.LATEST_AGENT_VER}-x86.exe",
},
},
wait=False,
)
action = PendingAction.objects.get(agent__pk=agent32_sign.pk)
self.assertEqual(action.action_type, "agentupdate")
self.assertEqual(action.status, "pending") """
@patch("agents.tasks.agent_update")
@patch("agents.tasks.sleep", return_value=None)
def test_auto_self_agent_update_task(self, mock_sleep, agent_update):
baker.make_recipe(
"agents.agent",
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
version=settings.LATEST_AGENT_VER,
_quantity=23,
)
baker.make_recipe(
"agents.agent",
operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
version="1.3.0",
_quantity=33,
)
self.coresettings.agent_auto_update = False
self.coresettings.save(update_fields=["agent_auto_update"])
r = auto_self_agent_update_task.s().apply()
self.assertEqual(agent_update.call_count, 0)
self.coresettings.agent_auto_update = True
self.coresettings.save(update_fields=["agent_auto_update"])
r = auto_self_agent_update_task.s().apply()
self.assertEqual(agent_update.call_count, 33)
def test_agent_history_prune_task(self):
from .tasks import prune_agent_history
from agents.tasks import prune_agent_history
# setup data
agent = baker.make_recipe("agents.agent")

View File

@@ -0,0 +1,46 @@
from unittest.mock import call, patch
from django.core.management import call_command
from model_bakery import baker
from tacticalrmm.constants import AgentMonType, AgentPlat
from tacticalrmm.test import TacticalTestCase
class TestBulkRestartAgents(TacticalTestCase):
def setUp(self) -> None:
self.authenticate()
self.setup_coresettings()
self.setup_base_instance()
@patch("core.management.commands.bulk_restart_agents.sleep")
@patch("agents.models.Agent.recover")
@patch("core.management.commands.bulk_restart_agents.get_mesh_ws_url")
def test_bulk_restart_agents_mgmt_cmd(
self, get_mesh_ws_url, recover, mock_sleep
) -> None:
get_mesh_ws_url.return_value = "https://mesh.example.com/test"
baker.make_recipe(
"agents.online_agent",
site=self.site1,
monitoring_type=AgentMonType.SERVER,
plat=AgentPlat.WINDOWS,
)
baker.make_recipe(
"agents.online_agent",
site=self.site3,
monitoring_type=AgentMonType.SERVER,
plat=AgentPlat.LINUX,
)
calls = [
call("tacagent", "https://mesh.example.com/test", wait=False),
call("mesh", "", wait=False),
]
call_command("bulk_restart_agents")
recover.assert_has_calls(calls)
mock_sleep.assert_called_with(10)

View File

@@ -0,0 +1,63 @@
from typing import TYPE_CHECKING
from unittest.mock import patch
from model_bakery import baker
from tacticalrmm.constants import AgentMonType, AgentPlat
from tacticalrmm.test import TacticalTestCase
if TYPE_CHECKING:
from clients.models import Client, Site
class TestRecovery(TacticalTestCase):
def setUp(self) -> None:
self.authenticate()
self.setup_coresettings()
self.client1: "Client" = baker.make("clients.Client")
self.site1: "Site" = baker.make("clients.Site", client=self.client1)
@patch("agents.models.Agent.recover")
@patch("agents.views.get_mesh_ws_url")
def test_recover(self, get_mesh_ws_url, recover) -> None:
get_mesh_ws_url.return_value = "https://mesh.example.com"
agent = baker.make_recipe(
"agents.online_agent",
site=self.site1,
monitoring_type=AgentMonType.SERVER,
plat=AgentPlat.WINDOWS,
)
url = f"/agents/{agent.agent_id}/recover/"
# test successfull tacticalagent recovery
data = {"mode": "tacagent"}
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
recover.assert_called_with("tacagent", "https://mesh.example.com", wait=False)
get_mesh_ws_url.assert_called_once()
# reset mocks
recover.reset_mock()
get_mesh_ws_url.reset_mock()
# test successfull mesh agent recovery
data = {"mode": "mesh"}
recover.return_value = ("ok", False)
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
get_mesh_ws_url.assert_not_called()
recover.assert_called_with("mesh", "")
# reset mocks
recover.reset_mock()
get_mesh_ws_url.reset_mock()
# test failed mesh agent recovery
data = {"mode": "mesh"}
recover.return_value = ("Unable to contact the agent", True)
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 400)
self.check_not_authenticated("post", url)

View File

@@ -1,6 +1,7 @@
from django.urls import path
from autotasks.views import GetAddAutoTasks
from checks.views import GetAddChecks
from django.urls import path
from logs.views import PendingActions
from . import views
@@ -14,6 +15,7 @@ urlpatterns = [
path("<agent:agent_id>/wmi/", views.WMI.as_view()),
path("<agent:agent_id>/recover/", views.recover),
path("<agent:agent_id>/reboot/", views.Reboot.as_view()),
path("<agent:agent_id>/shutdown/", views.Shutdown.as_view()),
path("<agent:agent_id>/ping/", views.ping),
# alias for checks get view
path("<agent:agent_id>/checks/", GetAddChecks.as_view()),
@@ -40,4 +42,7 @@ urlpatterns = [
path("versions/", views.get_agent_versions),
path("update/", views.update_agents),
path("installer/", views.install_agent),
path("bulkrecovery/", views.bulk_agent_recovery),
path("scripthistory/", views.ScriptRunHistory.as_view()),
path("<agent:agent_id>/wol/", views.wol),
]

View File

@@ -1,38 +1,28 @@
import asyncio
import tempfile
import urllib.parse
from io import StringIO
from pathlib import Path
from core.models import CodeSignToken
from core.utils import get_mesh_device_id, get_mesh_ws_url, get_core_settings
from django.conf import settings
from django.http import FileResponse
from core.utils import get_core_settings, get_mesh_device_id, get_mesh_ws_url
from tacticalrmm.constants import MeshAgentIdent
def get_agent_url(arch: str, plat: str) -> str:
if plat == "windows":
endpoint = "winagents"
dl_url = settings.DL_32 if arch == "32" else settings.DL_64
else:
endpoint = "linuxagents"
dl_url = ""
token = CodeSignToken.objects.first()
if not token:
return dl_url
if token.is_valid:
base_url = settings.EXE_GEN_URL + f"/api/v1/{endpoint}/?"
def get_agent_url(*, goarch: str, plat: str, token: str = "") -> str:
ver = settings.LATEST_AGENT_VER
if token:
params = {
"version": settings.LATEST_AGENT_VER,
"arch": arch,
"token": token.token,
"version": ver,
"arch": goarch,
"token": token,
"plat": plat,
"api": settings.ALLOWED_HOSTS[0],
}
dl_url = base_url + urllib.parse.urlencode(params)
return settings.AGENTS_URL + urllib.parse.urlencode(params)
return dl_url
return f"https://github.com/amidaware/rmmagent/releases/download/v{ver}/tacticalagent-v{ver}-{plat}-{goarch}.exe"
def generate_linux_install(
@@ -44,7 +34,6 @@ def generate_linux_install(
api: str,
download_url: str,
) -> FileResponse:
match arch:
case "amd64":
arch_id = MeshAgentIdent.LINUX64
@@ -62,12 +51,10 @@ def generate_linux_install(
uri = get_mesh_ws_url()
mesh_id = asyncio.run(get_mesh_device_id(uri, core.mesh_device_group))
mesh_dl = (
f"{core.mesh_site}/meshagents?id={mesh_id}&installflags=0&meshinstall={arch_id}"
f"{core.mesh_site}/meshagents?id={mesh_id}&installflags=2&meshinstall={arch_id}"
)
sh = settings.LINUX_AGENT_SCRIPT
with open(sh, "r") as f:
text = f.read()
text = Path(settings.LINUX_AGENT_SCRIPT).read_text()
replace = {
"agentDLChange": download_url,
@@ -82,11 +69,8 @@ def generate_linux_install(
for i, j in replace.items():
text = text.replace(i, j)
with tempfile.NamedTemporaryFile() as fp:
with open(fp.name, "w") as f:
f.write(text)
f.write("\n")
text += "\n"
with StringIO(text) as fp:
return FileResponse(
open(fp.name, "rb"), as_attachment=True, filename="linux_agent_install.sh"
fp.read(), as_attachment=True, filename="linux_agent_install.sh"
)

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
# Generated by Django 4.0.3 on 2022-04-07 17:28
from django.db import migrations, models
import django.db.models.deletion
from django.db import migrations, models
def delete_alerts_without_agent(apps, schema):

View File

@@ -0,0 +1,23 @@
# Generated by Django 4.0.5 on 2022-06-29 07:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('alerts', '0011_alter_alert_agent'),
]
operations = [
migrations.AlterField(
model_name='alert',
name='action_retcode',
field=models.BigIntegerField(blank=True, null=True),
),
migrations.AlterField(
model_name='alert',
name='resolved_action_retcode',
field=models.BigIntegerField(blank=True, null=True),
),
]

View File

@@ -0,0 +1,36 @@
# Generated by Django 4.1.3 on 2022-11-26 20:22
import django.contrib.postgres.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("alerts", "0012_alter_alert_action_retcode_and_more"),
]
operations = [
migrations.AddField(
model_name="alerttemplate",
name="action_env_vars",
field=django.contrib.postgres.fields.ArrayField(
base_field=models.TextField(blank=True, null=True),
blank=True,
default=list,
null=True,
size=None,
),
),
migrations.AddField(
model_name="alerttemplate",
name="resolved_action_env_vars",
field=django.contrib.postgres.fields.ArrayField(
base_field=models.TextField(blank=True, null=True),
blank=True,
default=list,
null=True,
size=None,
),
),
]

View File

@@ -0,0 +1,55 @@
# Generated by Django 4.2.13 on 2024-06-28 20:21
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("core", "0045_coresettings_enable_server_scripts_and_more"),
("alerts", "0013_alerttemplate_action_env_vars_and_more"),
]
operations = [
migrations.AddField(
model_name="alerttemplate",
name="action_rest",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="url_action_alert_template",
to="core.urlaction",
),
),
migrations.AddField(
model_name="alerttemplate",
name="action_type",
field=models.CharField(
choices=[("script", "Script"), ("server", "Server"), ("rest", "Rest")],
default="script",
max_length=10,
),
),
migrations.AddField(
model_name="alerttemplate",
name="resolved_action_rest",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="resolved_url_action_alert_template",
to="core.urlaction",
),
),
migrations.AddField(
model_name="alerttemplate",
name="resolved_action_type",
field=models.CharField(
choices=[("script", "Script"), ("server", "Server"), ("rest", "Rest")],
default="script",
max_length=10,
),
),
]

View File

@@ -1,15 +1,26 @@
from __future__ import annotations
import re
from typing import TYPE_CHECKING, Union, Optional, Dict, Any, List, cast
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union, cast
from django.contrib.postgres.fields import ArrayField
from django.db import models
from django.db.models.fields import BooleanField, PositiveIntegerField
from django.utils import timezone as djangotime
from logs.models import BaseAuditModel, DebugLog
from core.utils import run_server_script, run_url_rest_action
from logs.models import BaseAuditModel, DebugLog
from tacticalrmm.constants import (
AgentHistoryType,
AgentMonType,
AlertSeverity,
AlertTemplateActionType,
AlertType,
CheckType,
DebugLogType,
)
from tacticalrmm.logger import logger
from tacticalrmm.models import PermissionQuerySet
from tacticalrmm.utils import RE_DB_VALUE, get_db_value
if TYPE_CHECKING:
from agents.models import Agent
@@ -18,20 +29,6 @@ if TYPE_CHECKING:
from clients.models import Client, Site
SEVERITY_CHOICES = [
("info", "Informational"),
("warning", "Warning"),
("error", "Error"),
]
ALERT_TYPE_CHOICES = [
("availability", "Availability"),
("check", "Check"),
("task", "Task"),
("custom", "Custom"),
]
class Alert(models.Model):
objects = PermissionQuerySet.as_manager()
@@ -57,7 +54,7 @@ class Alert(models.Model):
blank=True,
)
alert_type = models.CharField(
max_length=20, choices=ALERT_TYPE_CHOICES, default="availability"
max_length=20, choices=AlertType.choices, default=AlertType.AVAILABILITY
)
message = models.TextField(null=True, blank=True)
alert_time = models.DateTimeField(auto_now_add=True, null=True, blank=True)
@@ -65,7 +62,9 @@ class Alert(models.Model):
snooze_until = models.DateTimeField(null=True, blank=True)
resolved = models.BooleanField(default=False)
resolved_on = models.DateTimeField(null=True, blank=True)
severity = models.CharField(max_length=30, choices=SEVERITY_CHOICES, default="info")
severity = models.CharField(
max_length=30, choices=AlertSeverity.choices, default=AlertSeverity.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)
@@ -74,21 +73,21 @@ class Alert(models.Model):
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_retcode = models.BigIntegerField(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_retcode = models.BigIntegerField(null=True, blank=True)
resolved_action_execution_time = models.CharField(
max_length=100, null=True, blank=True
)
def __str__(self) -> str:
return self.message
return f"{self.alert_type} - {self.message}"
@property
def assigned_agent(self) -> "Agent":
def assigned_agent(self) -> "Optional[Agent]":
return self.agent
@property
@@ -99,6 +98,15 @@ class Alert(models.Model):
def client(self) -> "Client":
return self.agent.client
@property
def get_result(self):
if self.alert_type == AlertType.CHECK:
return self.assigned_check.checkresults.get(agent=self.agent)
elif self.alert_type == AlertType.TASK:
return self.assigned_task.taskresults.get(agent=self.agent)
return None
def resolve(self) -> None:
self.resolved = True
self.resolved_on = djangotime.now()
@@ -110,8 +118,11 @@ class Alert(models.Model):
def create_or_return_availability_alert(
cls, agent: Agent, skip_create: bool = False
) -> Optional[Alert]:
if agent.maintenance_mode:
return None
if not cls.objects.filter(
agent=agent, alert_type="availability", resolved=False
agent=agent, alert_type=AlertType.AVAILABILITY, resolved=False
).exists():
if skip_create:
return None
@@ -120,9 +131,9 @@ class Alert(models.Model):
Alert,
cls.objects.create(
agent=agent,
alert_type="availability",
severity="error",
message=f"{agent.hostname} in {agent.client.name}\\{agent.site.name} is overdue.",
alert_type=AlertType.AVAILABILITY,
severity=AlertSeverity.ERROR,
message=f"{agent.hostname} in {agent.client.name}, {agent.site.name} is overdue.",
hidden=True,
),
)
@@ -131,12 +142,12 @@ class Alert(models.Model):
return cast(
Alert,
cls.objects.get(
agent=agent, alert_type="availability", resolved=False
agent=agent, alert_type=AlertType.AVAILABILITY, resolved=False
),
)
except cls.MultipleObjectsReturned:
alerts = cls.objects.filter(
agent=agent, alert_type="availability", resolved=False
agent=agent, alert_type=AlertType.AVAILABILITY, resolved=False
)
last_alert = cast(Alert, alerts.last())
@@ -158,6 +169,8 @@ class Alert(models.Model):
alert_severity: Optional[str] = None,
skip_create: bool = False,
) -> "Optional[Alert]":
if agent.maintenance_mode:
return None
# need to pass agent if the check is a policy
if not cls.objects.filter(
@@ -173,11 +186,18 @@ class Alert(models.Model):
cls.objects.create(
assigned_check=check,
agent=agent,
alert_type="check",
severity=check.alert_severity
if check.check_type
not in ["memory", "cpuload", "diskspace", "script"]
else alert_severity,
alert_type=AlertType.CHECK,
severity=(
check.alert_severity
if check.check_type
not in {
CheckType.MEMORY,
CheckType.CPU_LOAD,
CheckType.DISK_SPACE,
CheckType.SCRIPT,
}
else alert_severity
),
message=f"{agent.hostname} has a {check.check_type} check: {check.readable_desc} that failed.",
hidden=True,
),
@@ -216,6 +236,8 @@ class Alert(models.Model):
agent: "Agent",
skip_create: bool = False,
) -> "Optional[Alert]":
if agent.maintenance_mode:
return None
if not cls.objects.filter(
assigned_task=task,
@@ -230,7 +252,7 @@ class Alert(models.Model):
cls.objects.create(
assigned_task=task,
agent=agent,
alert_type="task",
alert_type=AlertType.TASK,
severity=task.alert_severity,
message=f"{agent.hostname} has task: {task.name} that failed.",
hidden=True,
@@ -268,10 +290,12 @@ class Alert(models.Model):
def handle_alert_failure(
cls, instance: Union[Agent, TaskResult, CheckResult]
) -> None:
from agents.models import Agent
from agents.models import Agent, AgentHistory
from autotasks.models import TaskResult
from checks.models import CheckResult
from core.models import CoreSettings
core = CoreSettings.objects.first()
# set variables
dashboard_severities = None
email_severities = None
@@ -282,7 +306,7 @@ class Alert(models.Model):
alert_interval = None
email_task = None
text_task = None
run_script_action = None
should_run_script_or_webhook = False
# check what the instance passed is
if isinstance(instance, Agent):
@@ -296,19 +320,19 @@ class Alert(models.Model):
dashboard_alert = instance.overdue_dashboard_alert
alert_template = instance.alert_template
maintenance_mode = instance.maintenance_mode
alert_severity = "error"
alert_severity = AlertSeverity.ERROR
agent = instance
dashboard_severities = [AlertSeverity.ERROR]
email_severities = [AlertSeverity.ERROR]
text_severities = [AlertSeverity.ERROR]
# set alert_template settings
if alert_template:
dashboard_severities = ["error"]
email_severities = ["error"]
text_severities = ["error"]
always_dashboard = alert_template.agent_always_alert
always_email = alert_template.agent_always_email
always_text = alert_template.agent_always_text
alert_interval = alert_template.agent_periodic_alert_days
run_script_action = alert_template.agent_script_actions
should_run_script_or_webhook = alert_template.agent_script_actions
elif isinstance(instance, CheckResult):
from checks.tasks import (
@@ -327,21 +351,39 @@ class Alert(models.Model):
alert_severity = (
instance.assigned_check.alert_severity
if instance.assigned_check.check_type
not in ["memory", "cpuload", "diskspace", "script"]
not in {
CheckType.MEMORY,
CheckType.CPU_LOAD,
CheckType.DISK_SPACE,
CheckType.SCRIPT,
}
else instance.alert_severity
)
agent = instance.agent
# set alert_template settings
if alert_template:
dashboard_severities = alert_template.check_dashboard_alert_severity
email_severities = alert_template.check_email_alert_severity
text_severities = alert_template.check_text_alert_severity
dashboard_severities = (
alert_template.check_dashboard_alert_severity
or [
AlertSeverity.ERROR,
AlertSeverity.WARNING,
AlertSeverity.INFO,
]
)
email_severities = alert_template.check_email_alert_severity or [
AlertSeverity.ERROR,
AlertSeverity.WARNING,
]
text_severities = alert_template.check_text_alert_severity or [
AlertSeverity.ERROR,
AlertSeverity.WARNING,
]
always_dashboard = alert_template.check_always_alert
always_email = alert_template.check_always_email
always_text = alert_template.check_always_text
alert_interval = alert_template.check_periodic_alert_days
run_script_action = alert_template.check_script_actions
should_run_script_or_webhook = alert_template.check_script_actions
elif isinstance(instance, TaskResult):
from autotasks.tasks import handle_task_email_alert, handle_task_sms_alert
@@ -359,14 +401,23 @@ class Alert(models.Model):
# set alert_template settings
if alert_template:
dashboard_severities = alert_template.task_dashboard_alert_severity
email_severities = alert_template.task_email_alert_severity
text_severities = alert_template.task_text_alert_severity
dashboard_severities = alert_template.task_dashboard_alert_severity or [
AlertSeverity.ERROR,
AlertSeverity.WARNING,
]
email_severities = alert_template.task_email_alert_severity or [
AlertSeverity.ERROR,
AlertSeverity.WARNING,
]
text_severities = alert_template.task_text_alert_severity or [
AlertSeverity.ERROR,
AlertSeverity.WARNING,
]
always_dashboard = alert_template.task_always_alert
always_email = alert_template.task_always_email
always_text = alert_template.task_always_text
alert_interval = alert_template.task_periodic_alert_days
run_script_action = alert_template.task_script_actions
should_run_script_or_webhook = alert_template.task_script_actions
else:
return
@@ -384,7 +435,6 @@ class Alert(models.Model):
# create alert in dashboard if enabled
if dashboard_alert or always_dashboard:
# check if alert template is set and specific severities are configured
if (
not alert_template
@@ -395,13 +445,23 @@ class Alert(models.Model):
alert.hidden = False
alert.save(update_fields=["hidden"])
# TODO rework this
if alert.severity == AlertSeverity.INFO and not core.notify_on_info_alerts:
email_alert = False
always_email = False
elif (
alert.severity == AlertSeverity.WARNING
and not core.notify_on_warning_alerts
):
email_alert = False
always_email = False
# send email if enabled
if email_alert or always_email:
# check if alert template is set and specific severities are configured
if (
not alert_template
or alert_template
if not alert_template or (
alert_template
and email_severities
and alert.severity in email_severities
):
@@ -410,33 +470,89 @@ class Alert(models.Model):
alert_interval=alert_interval,
)
# TODO rework this
if alert.severity == AlertSeverity.INFO and not core.notify_on_info_alerts:
text_alert = False
always_text = False
elif (
alert.severity == AlertSeverity.WARNING
and not core.notify_on_warning_alerts
):
text_alert = False
always_text = False
# send text if enabled
if text_alert or always_text:
# check if alert template is set and specific severities are configured
if (
not alert_template
or alert_template
and text_severities
and alert.severity in text_severities
if not alert_template or (
alert_template and text_severities and alert.severity in text_severities
):
text_task.delay(pk=alert.pk, alert_interval=alert_interval)
# check if any scripts should be run
if (
alert_template
and alert_template.action
and run_script_action
and not alert.action_run
):
r = agent.run_script(
scriptpk=alert_template.action.pk,
args=alert.parse_script_args(alert_template.action_args),
timeout=alert_template.action_timeout,
wait=True,
full=True,
run_on_any=True,
)
# check if any scripts/webhooks should be run
if alert_template and not alert.action_run and should_run_script_or_webhook:
if (
alert_template.action_type == AlertTemplateActionType.SCRIPT
and alert_template.action
):
hist = AgentHistory.objects.create(
agent=agent,
type=AgentHistoryType.SCRIPT_RUN,
script=alert_template.action,
username="alert-action-failure",
)
r = agent.run_script(
scriptpk=alert_template.action.pk,
args=alert.parse_script_args(alert_template.action_args),
timeout=alert_template.action_timeout,
wait=True,
history_pk=hist.pk,
full=True,
run_on_any=True,
run_as_user=False,
env_vars=alert.parse_script_args(alert_template.action_env_vars),
)
elif (
alert_template.action_type == AlertTemplateActionType.SERVER
and alert_template.action
):
stdout, stderr, execution_time, retcode = run_server_script(
body=alert_template.action.script_body,
args=alert.parse_script_args(alert_template.action_args),
timeout=alert_template.action_timeout,
env_vars=alert.parse_script_args(alert_template.action_env_vars),
shell=alert_template.action.shell,
)
r = {
"retcode": retcode,
"stdout": stdout,
"stderr": stderr,
"execution_time": execution_time,
}
elif alert_template.action_type == AlertTemplateActionType.REST:
if (
alert.severity == AlertSeverity.INFO
and not core.notify_on_info_alerts
or alert.severity == AlertSeverity.WARNING
and not core.notify_on_warning_alerts
):
return
else:
output, status = run_url_rest_action(
action_id=alert_template.action_rest.id, instance=alert
)
logger.debug(f"{output=} {status=}")
r = {
"stdout": output,
"stderr": "",
"execution_time": 0,
"retcode": status,
}
else:
return
# command was successful
if isinstance(r, dict):
@@ -447,26 +563,37 @@ class Alert(models.Model):
alert.action_run = djangotime.now()
alert.save()
else:
DebugLog.error(
agent=agent,
log_type="scripting",
message=f"Failure action: {alert_template.action.name} failed to run on any agent for {agent.hostname}({agent.pk}) failure alert",
)
if alert_template.action_type == AlertTemplateActionType.SCRIPT:
DebugLog.error(
agent=agent,
log_type=DebugLogType.SCRIPTING,
message=f"Failure action: {alert_template.action.name} failed to run on any agent for {agent.hostname}({agent.pk}) failure alert",
)
else:
DebugLog.error(
log_type=DebugLogType.SCRIPTING,
message=f"Failure action: {alert_template.action.name} failed to run on server for failure alert",
)
@classmethod
def handle_alert_resolve(
cls, instance: Union[Agent, TaskResult, CheckResult]
) -> None:
from agents.models import Agent
from agents.models import Agent, AgentHistory
from autotasks.models import TaskResult
from checks.models import CheckResult
from core.models import CoreSettings
core = CoreSettings.objects.first()
# set variables
email_severities = None
text_severities = None
email_on_resolved = False
text_on_resolved = False
resolved_email_task = None
resolved_text_task = None
run_script_action = None
should_run_script_or_webhook = False
# check what the instance passed is
if isinstance(instance, Agent):
@@ -482,7 +609,9 @@ class Alert(models.Model):
if alert_template:
email_on_resolved = alert_template.agent_email_on_resolved
text_on_resolved = alert_template.agent_text_on_resolved
run_script_action = alert_template.agent_script_actions
should_run_script_or_webhook = alert_template.agent_script_actions
email_severities = [AlertSeverity.ERROR]
text_severities = [AlertSeverity.ERROR]
if agent.overdue_email_alert:
email_on_resolved = True
@@ -505,7 +634,15 @@ class Alert(models.Model):
if alert_template:
email_on_resolved = alert_template.check_email_on_resolved
text_on_resolved = alert_template.check_text_on_resolved
run_script_action = alert_template.check_script_actions
should_run_script_or_webhook = alert_template.check_script_actions
email_severities = alert_template.check_email_alert_severity or [
AlertSeverity.ERROR,
AlertSeverity.WARNING,
]
text_severities = alert_template.check_text_alert_severity or [
AlertSeverity.ERROR,
AlertSeverity.WARNING,
]
elif isinstance(instance, TaskResult):
from autotasks.tasks import (
@@ -523,7 +660,15 @@ class Alert(models.Model):
if alert_template:
email_on_resolved = alert_template.task_email_on_resolved
text_on_resolved = alert_template.task_text_on_resolved
run_script_action = alert_template.task_script_actions
should_run_script_or_webhook = alert_template.task_script_actions
email_severities = alert_template.task_email_alert_severity or [
AlertSeverity.ERROR,
AlertSeverity.WARNING,
]
text_severities = alert_template.task_text_alert_severity or [
AlertSeverity.ERROR,
AlertSeverity.WARNING,
]
else:
return
@@ -538,27 +683,103 @@ class Alert(models.Model):
# check if a resolved email notification should be send
if email_on_resolved and not alert.resolved_email_sent:
resolved_email_task.delay(pk=alert.pk)
if alert.severity == AlertSeverity.INFO and not core.notify_on_info_alerts:
pass
elif (
alert.severity == AlertSeverity.WARNING
and not core.notify_on_warning_alerts
):
pass
elif email_severities and alert.severity not in email_severities:
pass
else:
resolved_email_task.delay(pk=alert.pk)
# check if resolved text should be sent
if text_on_resolved and not alert.resolved_sms_sent:
resolved_text_task.delay(pk=alert.pk)
if alert.severity == AlertSeverity.INFO and not core.notify_on_info_alerts:
pass
# check if resolved script should be run
elif (
alert.severity == AlertSeverity.WARNING
and not core.notify_on_warning_alerts
):
pass
elif text_severities and alert.severity not in text_severities:
pass
else:
resolved_text_task.delay(pk=alert.pk)
# check if resolved script/webhook should be run
if (
alert_template
and alert_template.resolved_action
and run_script_action
and not alert.resolved_action_run
and should_run_script_or_webhook
):
r = agent.run_script(
scriptpk=alert_template.resolved_action.pk,
args=alert.parse_script_args(alert_template.resolved_action_args),
timeout=alert_template.resolved_action_timeout,
wait=True,
full=True,
run_on_any=True,
)
if (
alert_template.resolved_action_type == AlertTemplateActionType.SCRIPT
and alert_template.resolved_action
):
hist = AgentHistory.objects.create(
agent=agent,
type=AgentHistoryType.SCRIPT_RUN,
script=alert_template.resolved_action,
username="alert-action-resolved",
)
r = agent.run_script(
scriptpk=alert_template.resolved_action.pk,
args=alert.parse_script_args(alert_template.resolved_action_args),
timeout=alert_template.resolved_action_timeout,
wait=True,
history_pk=hist.pk,
full=True,
run_on_any=True,
run_as_user=False,
env_vars=alert_template.resolved_action_env_vars,
)
elif (
alert_template.resolved_action_type == AlertTemplateActionType.SERVER
and alert_template.resolved_action
):
stdout, stderr, execution_time, retcode = run_server_script(
body=alert_template.resolved_action.script_body,
args=alert.parse_script_args(alert_template.resolved_action_args),
timeout=alert_template.resolved_action_timeout,
env_vars=alert.parse_script_args(
alert_template.resolved_action_env_vars
),
shell=alert_template.resolved_action.shell,
)
r = {
"stdout": stdout,
"stderr": stderr,
"execution_time": execution_time,
"retcode": retcode,
}
elif alert_template.action_type == AlertTemplateActionType.REST:
if (
alert.severity == AlertSeverity.INFO
and not core.notify_on_info_alerts
or alert.severity == AlertSeverity.WARNING
and not core.notify_on_warning_alerts
):
return
else:
output, status = run_url_rest_action(
action_id=alert_template.resolved_action_rest.id, instance=alert
)
logger.debug(f"{output=} {status=}")
r = {
"stdout": output,
"stderr": "",
"execution_time": 0,
"retcode": status,
}
else:
return
# command was successful
if isinstance(r, dict):
@@ -571,40 +792,36 @@ class Alert(models.Model):
alert.resolved_action_run = djangotime.now()
alert.save()
else:
DebugLog.error(
agent=agent,
log_type="scripting",
message=f"Resolved action: {alert_template.action.name} failed to run on any agent for {agent.hostname}({agent.pk}) resolved alert",
)
if (
alert_template.resolved_action_type
== AlertTemplateActionType.SCRIPT
):
DebugLog.error(
agent=agent,
log_type=DebugLogType.SCRIPTING,
message=f"Resolved action: {alert_template.action.name} failed to run on any agent for {agent.hostname}({agent.pk}) resolved alert",
)
else:
DebugLog.error(
log_type=DebugLogType.SCRIPTING,
message=f"Resolved action: {alert_template.action.name} failed to run on server for resolved alert",
)
def parse_script_args(self, args: List[str]) -> List[str]:
if not args:
return []
temp_args = list()
# pattern to match for injection
pattern = re.compile(".*\\{\\{alert\\.(.*)\\}\\}.*")
temp_args = []
for arg in args:
match = pattern.match(arg)
if match:
name = match.group(1)
temp_arg = arg
for string, model, prop in RE_DB_VALUE.findall(arg):
value = get_db_value(string=f"{model}.{prop}", instance=self)
# check if attr exists and isn't a function
if hasattr(self, name) and not callable(getattr(self, name)):
value = f"'{getattr(self, name)}'"
else:
continue
if value is not None:
temp_arg = temp_arg.replace(string, f"'{str(value)}'")
try:
temp_args.append(re.sub("\\{\\{.*\\}\\}", value, arg))
except Exception as e:
DebugLog.error(log_type="scripting", message=str(e))
continue
else:
temp_args.append(arg)
temp_args.append(temp_arg)
return temp_args
@@ -613,6 +830,11 @@ class AlertTemplate(BaseAuditModel):
name = models.CharField(max_length=100)
is_active = models.BooleanField(default=True)
action_type = models.CharField(
max_length=10,
choices=AlertTemplateActionType.choices,
default=AlertTemplateActionType.SCRIPT,
)
action = models.ForeignKey(
"scripts.Script",
related_name="alert_template",
@@ -620,13 +842,31 @@ class AlertTemplate(BaseAuditModel):
null=True,
on_delete=models.SET_NULL,
)
action_rest = models.ForeignKey(
"core.URLAction",
related_name="url_action_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_env_vars = ArrayField(
models.TextField(null=True, blank=True),
null=True,
blank=True,
default=list,
)
action_timeout = models.PositiveIntegerField(default=15)
resolved_action_type = models.CharField(
max_length=10,
choices=AlertTemplateActionType.choices,
default=AlertTemplateActionType.SCRIPT,
)
resolved_action = models.ForeignKey(
"scripts.Script",
related_name="resolved_alert_template",
@@ -634,12 +874,25 @@ class AlertTemplate(BaseAuditModel):
null=True,
on_delete=models.SET_NULL,
)
resolved_action_rest = models.ForeignKey(
"core.URLAction",
related_name="resolved_url_action_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_env_vars = ArrayField(
models.TextField(null=True, blank=True),
null=True,
blank=True,
default=list,
)
resolved_action_timeout = models.PositiveIntegerField(default=15)
# overrides the global recipients
@@ -666,21 +919,22 @@ class AlertTemplate(BaseAuditModel):
agent_always_text = BooleanField(null=True, blank=True, default=None)
agent_always_alert = BooleanField(null=True, blank=True, default=None)
agent_periodic_alert_days = PositiveIntegerField(blank=True, null=True, default=0)
agent_script_actions = BooleanField(null=True, blank=True, default=True)
# fmt: off
agent_script_actions = BooleanField(null=True, blank=True, default=True) # should be renamed because also deals with webhooks
# check alert settings
check_email_alert_severity = ArrayField(
models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
models.CharField(max_length=25, blank=True, choices=AlertSeverity.choices),
blank=True,
default=list,
)
check_text_alert_severity = ArrayField(
models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
models.CharField(max_length=25, blank=True, choices=AlertSeverity.choices),
blank=True,
default=list,
)
check_dashboard_alert_severity = ArrayField(
models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
models.CharField(max_length=25, blank=True, choices=AlertSeverity.choices),
blank=True,
default=list,
)
@@ -690,21 +944,22 @@ class AlertTemplate(BaseAuditModel):
check_always_text = BooleanField(null=True, blank=True, default=None)
check_always_alert = BooleanField(null=True, blank=True, default=None)
check_periodic_alert_days = PositiveIntegerField(blank=True, null=True, default=0)
check_script_actions = BooleanField(null=True, blank=True, default=True)
# fmt: off
check_script_actions = BooleanField(null=True, blank=True, default=True) # should be renamed because also deals with webhooks
# task alert settings
task_email_alert_severity = ArrayField(
models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
models.CharField(max_length=25, blank=True, choices=AlertSeverity.choices),
blank=True,
default=list,
)
task_text_alert_severity = ArrayField(
models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
models.CharField(max_length=25, blank=True, choices=AlertSeverity.choices),
blank=True,
default=list,
)
task_dashboard_alert_severity = ArrayField(
models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
models.CharField(max_length=25, blank=True, choices=AlertSeverity.choices),
blank=True,
default=list,
)
@@ -714,7 +969,8 @@ class AlertTemplate(BaseAuditModel):
task_always_text = BooleanField(null=True, blank=True, default=None)
task_always_alert = BooleanField(null=True, blank=True, default=None)
task_periodic_alert_days = PositiveIntegerField(blank=True, null=True, default=0)
task_script_actions = BooleanField(null=True, blank=True, default=True)
# fmt: off
task_script_actions = BooleanField(null=True, blank=True, default=True) # should be renamed because also deals with webhooks
# exclusion settings
exclude_workstations = BooleanField(null=True, blank=True, default=False)
@@ -738,9 +994,9 @@ class AlertTemplate(BaseAuditModel):
agent in self.excluded_agents.all()
or agent.site in self.excluded_sites.all()
or agent.client in self.excluded_clients.all()
or agent.monitoring_type == "workstation"
or agent.monitoring_type == AgentMonType.WORKSTATION
and self.exclude_workstations
or agent.monitoring_type == "server"
or agent.monitoring_type == AgentMonType.SERVER
and self.exclude_servers
)

View File

@@ -1,6 +1,9 @@
from typing import TYPE_CHECKING
from django.shortcuts import get_object_or_404
from rest_framework import permissions
from typing import TYPE_CHECKING
from tacticalrmm.constants import AlertTemplateActionType
from tacticalrmm.permissions import _has_perm, _has_perm_on_agent
if TYPE_CHECKING:
@@ -30,7 +33,7 @@ def _has_perm_on_alert(user: "User", id: int) -> bool:
class AlertPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool:
if r.method == "GET" or r.method == "PATCH":
if r.method in ("GET", "PATCH"):
if "pk" in view.kwargs.keys():
return _has_perm(r, "can_list_alerts") and _has_perm_on_alert(
r.user, view.kwargs["pk"]
@@ -50,5 +53,18 @@ class AlertTemplatePerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool:
if r.method == "GET":
return _has_perm(r, "can_list_alerttemplates")
else:
return _has_perm(r, "can_manage_alerttemplates")
if r.method in ("POST", "PUT", "PATCH"):
# ensure only users with explicit run server script perms can add/modify alert templates
# while also still requiring the manage alert template perm
if isinstance(r.data, dict):
if (
r.data.get("action_type") == AlertTemplateActionType.SERVER
or r.data.get("resolved_action_type")
== AlertTemplateActionType.SERVER
):
return _has_perm(r, "can_run_server_scripts") and _has_perm(
r, "can_manage_alerttemplates"
)
return _has_perm(r, "can_manage_alerttemplates")

View File

@@ -1,13 +1,14 @@
from automation.serializers import PolicySerializer
from clients.serializers import ClientMinimumSerializer, SiteMinimumSerializer
from rest_framework.fields import SerializerMethodField
from rest_framework.serializers import ModelSerializer, ReadOnlyField
from automation.serializers import PolicySerializer
from clients.serializers import ClientMinimumSerializer, SiteMinimumSerializer
from tacticalrmm.constants import AlertTemplateActionType
from .models import Alert, AlertTemplate
class AlertSerializer(ModelSerializer):
hostname = ReadOnlyField(source="assigned_agent.hostname")
agent_id = ReadOnlyField(source="assigned_agent.agent_id")
client = ReadOnlyField(source="client.name")
@@ -25,14 +26,29 @@ class AlertTemplateSerializer(ModelSerializer):
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")
action_name = SerializerMethodField()
resolved_action_name = SerializerMethodField()
applied_count = SerializerMethodField()
class Meta:
model = AlertTemplate
fields = "__all__"
def get_action_name(self, obj):
if obj.action_type == AlertTemplateActionType.REST and obj.action_rest:
return obj.action_rest.name
return obj.action.name if obj.action else ""
def get_resolved_action_name(self, obj):
if (
obj.resolved_action_type == AlertTemplateActionType.REST
and obj.resolved_action_rest
):
return obj.resolved_action_rest.name
return obj.resolved_action.name if obj.resolved_action else ""
def get_applied_count(self, instance):
return (
instance.policies.count()

View File

@@ -1,8 +1,10 @@
from django.utils import timezone as djangotime
from .models import Alert
from agents.models import Agent
from tacticalrmm.celery import app
from .models import Alert
@app.task
def unsnooze_alerts() -> str:

View File

@@ -1,17 +1,23 @@
from datetime import datetime, timedelta
from datetime import timedelta
from itertools import cycle
from unittest.mock import patch
from alerts.tasks import cache_agents_alert_template
from autotasks.models import TaskResult
from core.tasks import cache_db_fields_task, resolve_alerts_task
from core.utils import get_core_settings
from core.tasks import cache_db_fields_task, handle_resolved_stuff
from django.conf import settings
from django.utils import timezone as djangotime
from model_bakery import baker, seq
from tacticalrmm.constants import (
AgentMonType,
AlertSeverity,
AlertType,
CheckStatus,
URLActionType,
)
from tacticalrmm.test import TacticalTestCase
from autotasks.models import TaskResult
from .models import Alert, AlertTemplate
from .serializers import (
AlertSerializer,
@@ -27,6 +33,7 @@ class TestAlertsViews(TacticalTestCase):
self.authenticate()
self.setup_coresettings()
"""
def test_get_alerts(self):
url = "/alerts/"
@@ -38,15 +45,15 @@ class TestAlertsViews(TacticalTestCase):
alerts = baker.make(
"alerts.Alert",
agent=agent,
alert_time=seq(datetime.now(), timedelta(days=15)),
severity="warning",
alert_time=seq(djangotime.now(), timedelta(days=15)),
severity=AlertSeverity.WARNING,
_quantity=3,
)
baker.make(
"alerts.Alert",
assigned_check=check,
alert_time=seq(datetime.now(), timedelta(days=15)),
severity="error",
alert_time=seq(djangotime.now(), timedelta(days=15)),
severity=AlertSeverity.ERROR,
_quantity=7,
)
baker.make(
@@ -54,7 +61,7 @@ class TestAlertsViews(TacticalTestCase):
assigned_task=task,
snoozed=True,
snooze_until=djangotime.now(),
alert_time=seq(datetime.now(), timedelta(days=15)),
alert_time=seq(djangotime.now(), timedelta(days=15)),
_quantity=2,
)
baker.make(
@@ -62,7 +69,7 @@ class TestAlertsViews(TacticalTestCase):
agent=agent,
resolved=True,
resolved_on=djangotime.now(),
alert_time=seq(datetime.now(), timedelta(days=15)),
alert_time=seq(djangotime.now(), timedelta(days=15)),
_quantity=9,
)
@@ -119,13 +126,14 @@ class TestAlertsViews(TacticalTestCase):
self.assertEqual(len(resp.data), req["count"])
self.check_not_authenticated("patch", url)
"""
def test_add_alert(self):
url = "/alerts/"
agent = baker.make_recipe("agents.agent")
data = {
"alert_time": datetime.now(),
"alert_time": djangotime.now(),
"agent": agent.id,
"severity": "warning",
"alert_type": "availability",
@@ -274,12 +282,32 @@ class TestAlertsViews(TacticalTestCase):
resp = self.client.get("/alerts/templates/500/", format="json")
self.assertEqual(resp.status_code, 404)
alert_template = baker.make("alerts.AlertTemplate")
url = f"/alerts/templates/{alert_template.pk}/"
agent_script = baker.make("scripts.Script")
server_script = baker.make("scripts.Script")
webhook = baker.make("core.URLAction", action_type=URLActionType.REST)
alert_template_agent_script = baker.make(
"alerts.AlertTemplate", action=agent_script
)
url = f"/alerts/templates/{alert_template_agent_script.pk}/"
resp = self.client.get(url, format="json")
serializer = AlertTemplateSerializer(alert_template)
serializer = AlertTemplateSerializer(alert_template_agent_script)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data)
alert_template_server_script = baker.make(
"alerts.AlertTemplate", action=server_script
)
url = f"/alerts/templates/{alert_template_server_script.pk}/"
resp = self.client.get(url, format="json")
serializer = AlertTemplateSerializer(alert_template_server_script)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data)
alert_template_webhook = baker.make("alerts.AlertTemplate", action_rest=webhook)
url = f"/alerts/templates/{alert_template_webhook.pk}/"
resp = self.client.get(url, format="json")
serializer = AlertTemplateSerializer(alert_template_webhook)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, serializer.data)
@@ -362,7 +390,7 @@ class TestAlertTasks(TacticalTestCase):
not_snoozed = baker.make(
"alerts.Alert",
snoozed=True,
snooze_until=seq(datetime.now(), timedelta(days=15)),
snooze_until=seq(djangotime.now(), timedelta(days=15)),
_quantity=5,
)
@@ -370,7 +398,7 @@ class TestAlertTasks(TacticalTestCase):
snoozed = baker.make(
"alerts.Alert",
snoozed=True,
snooze_until=seq(datetime.now(), timedelta(days=-15)),
snooze_until=seq(djangotime.now(), timedelta(days=-15)),
_quantity=5,
)
@@ -388,11 +416,12 @@ class TestAlertTasks(TacticalTestCase):
)
def test_agent_gets_correct_alert_template(self):
core = get_core_settings()
# setup data
workstation = baker.make_recipe("agents.agent", monitoring_type="workstation")
server = baker.make_recipe("agents.agent", monitoring_type="server")
workstation = baker.make_recipe(
"agents.agent", monitoring_type=AgentMonType.WORKSTATION
)
server = baker.make_recipe("agents.agent", monitoring_type=AgentMonType.SERVER)
policy = baker.make("automation.Policy", active=True)
@@ -674,8 +703,6 @@ class TestAlertTasks(TacticalTestCase):
agent_template_email = Agent.objects.get(pk=agent_template_email.pk)
# have the two agents checkin
url = "/api/v3/checkin/"
agent_template_text.version = settings.LATEST_AGENT_VER
agent_template_text.last_seen = djangotime.now()
agent_template_text.save()
@@ -685,7 +712,7 @@ class TestAlertTasks(TacticalTestCase):
agent_template_email.save()
cache_db_fields_task()
handle_resolved_stuff()
resolve_alerts_task()
recovery_sms.assert_called_with(
pk=Alert.objects.get(agent=agent_template_text).pk
@@ -753,7 +780,7 @@ class TestAlertTasks(TacticalTestCase):
"alerts.AlertTemplate",
is_active=True,
check_always_email=True,
check_email_alert_severity=["warning"],
check_email_alert_severity=[AlertSeverity.WARNING],
)
agent_template_email.client.alert_template = alert_template_email
agent_template_email.client.save()
@@ -764,8 +791,12 @@ class TestAlertTasks(TacticalTestCase):
is_active=True,
check_always_alert=True,
check_always_text=True,
check_dashboard_alert_severity=["info", "warning", "error"],
check_text_alert_severity=["error"],
check_dashboard_alert_severity=[
AlertSeverity.INFO,
AlertSeverity.WARNING,
AlertSeverity.ERROR,
],
check_text_alert_severity=[AlertSeverity.ERROR],
)
agent_template_dashboard_text.client.alert_template = (
alert_template_dashboard_text
@@ -789,7 +820,7 @@ class TestAlertTasks(TacticalTestCase):
"checks.CheckResult",
assigned_check=check_agent,
agent=agent,
alert_severity="warning",
alert_severity=AlertSeverity.WARNING,
)
check_template_email = baker.make_recipe(
"checks.cpuload_check", agent=agent_template_email
@@ -840,8 +871,8 @@ class TestAlertTasks(TacticalTestCase):
)
# test agent with check that has alert settings
check_agent_result.alert_severity = "warning"
check_agent_result.status = "failing"
check_agent_result.alert_severity = AlertSeverity.WARNING
check_agent_result.status = CheckStatus.FAILING
Alert.handle_alert_failure(check_agent_result)
@@ -902,7 +933,7 @@ class TestAlertTasks(TacticalTestCase):
outage_sms.assert_not_called
# update check alert severity to error
check_template_dashboard_text_result.alert_severity = "error"
check_template_dashboard_text_result.alert_severity = AlertSeverity.ERROR
check_template_dashboard_text_result.save()
# now should trigger alert
@@ -1061,7 +1092,7 @@ class TestAlertTasks(TacticalTestCase):
"alerts.AlertTemplate",
is_active=True,
task_always_email=True,
task_email_alert_severity=["warning"],
task_email_alert_severity=[AlertSeverity.WARNING],
)
agent_template_email.client.alert_template = alert_template_email
agent_template_email.client.save()
@@ -1072,8 +1103,12 @@ class TestAlertTasks(TacticalTestCase):
is_active=True,
task_always_alert=True,
task_always_text=True,
task_dashboard_alert_severity=["info", "warning", "error"],
task_text_alert_severity=["error"],
task_dashboard_alert_severity=[
AlertSeverity.INFO,
AlertSeverity.WARNING,
AlertSeverity.ERROR,
],
task_text_alert_severity=[AlertSeverity.ERROR],
)
agent_template_dashboard_text.client.alert_template = (
alert_template_dashboard_text
@@ -1092,7 +1127,7 @@ class TestAlertTasks(TacticalTestCase):
email_alert=True,
text_alert=True,
dashboard_alert=True,
alert_severity="warning",
alert_severity=AlertSeverity.WARNING,
)
task_agent_result = baker.make(
"autotasks.TaskResult", agent=agent, task=task_agent
@@ -1100,7 +1135,7 @@ class TestAlertTasks(TacticalTestCase):
task_template_email = baker.make(
"autotasks.AutomatedTask",
agent=agent_template_email,
alert_severity="warning",
alert_severity=AlertSeverity.WARNING,
)
task_template_email_result = baker.make(
"autotasks.TaskResult", agent=agent_template_email, task=task_template_email
@@ -1108,7 +1143,7 @@ class TestAlertTasks(TacticalTestCase):
task_template_dashboard_text = baker.make(
"autotasks.AutomatedTask",
agent=agent_template_dashboard_text,
alert_severity="info",
alert_severity=AlertSeverity.INFO,
)
task_template_dashboard_text_result = baker.make(
"autotasks.TaskResult",
@@ -1118,13 +1153,15 @@ class TestAlertTasks(TacticalTestCase):
task_template_blank = baker.make(
"autotasks.AutomatedTask",
agent=agent_template_blank,
alert_severity="error",
alert_severity=AlertSeverity.ERROR,
)
task_template_blank_result = baker.make(
"autotasks.TaskResult", agent=agent_template_blank, task=task_template_blank
)
task_no_settings = baker.make(
"autotasks.AutomatedTask", agent=agent_no_settings, alert_severity="warning"
"autotasks.AutomatedTask",
agent=agent_no_settings,
alert_severity=AlertSeverity.WARNING,
)
task_no_settings_result = baker.make(
"autotasks.TaskResult", agent=agent_no_settings, task=task_no_settings
@@ -1202,7 +1239,7 @@ class TestAlertTasks(TacticalTestCase):
outage_sms.assert_not_called
# update task alert seveity to error
task_template_dashboard_text.alert_severity = "error"
task_template_dashboard_text.alert_severity = AlertSeverity.ERROR
task_template_dashboard_text.save()
# now should trigger alert
@@ -1360,7 +1397,7 @@ class TestAlertTasks(TacticalTestCase):
def test_alert_actions(
self, recovery_sms, recovery_email, outage_email, outage_sms, nats_cmd
):
from agents.models import AgentHistory
from agents.tasks import agent_outages_task
# Setup cmd mock
@@ -1386,9 +1423,12 @@ class TestAlertTasks(TacticalTestCase):
agent_script_actions=False,
action=failure_action,
action_timeout=30,
action_args=["hello", "world"],
action_env_vars=["hello=world", "foo=bar"],
resolved_action=resolved_action,
resolved_action_timeout=35,
resolved_action_args=["nice_arg"],
resolved_action_env_vars=["resolved=action", "env=vars"],
)
agent.client.alert_template = alert_template
agent.client.save()
@@ -1409,8 +1449,13 @@ class TestAlertTasks(TacticalTestCase):
data = {
"func": "runscriptfull",
"timeout": 30,
"script_args": [],
"script_args": ["hello", "world"],
"payload": {"code": failure_action.code, "shell": failure_action.shell},
"run_as_user": False,
"env_vars": ["hello=world", "foo=bar"],
"id": AgentHistory.objects.last().pk, # type: ignore
"nushell_enable_config": settings.NUSHELL_ENABLE_CONFIG,
"deno_default_permissions": settings.DENO_DEFAULT_PERMISSIONS,
}
nats_cmd.assert_called_with(data, timeout=30, wait=True)
@@ -1431,7 +1476,7 @@ class TestAlertTasks(TacticalTestCase):
agent.save()
cache_db_fields_task()
handle_resolved_stuff()
resolve_alerts_task()
# this is what data should be
data = {
@@ -1439,6 +1484,11 @@ class TestAlertTasks(TacticalTestCase):
"timeout": 35,
"script_args": ["nice_arg"],
"payload": {"code": resolved_action.code, "shell": resolved_action.shell},
"run_as_user": False,
"env_vars": ["resolved=action", "env=vars"],
"id": AgentHistory.objects.last().pk, # type: ignore
"nushell_enable_config": settings.NUSHELL_ENABLE_CONFIG,
"deno_default_permissions": settings.DENO_DEFAULT_PERMISSIONS,
}
nats_cmd.assert_called_with(data, timeout=35, wait=True)
@@ -1509,22 +1559,25 @@ class TestAlertPermissions(TacticalTestCase):
tasks = baker.make("autotasks.AutomatedTask", agent=cycle(agents), _quantity=3)
baker.make(
"alerts.Alert",
alert_type="task",
alert_type=AlertType.TASK,
agent=cycle(agents),
assigned_task=cycle(tasks),
_quantity=3,
)
baker.make(
"alerts.Alert",
alert_type="check",
alert_type=AlertType.CHECK,
agent=cycle(agents),
assigned_check=cycle(checks),
_quantity=3,
)
baker.make(
"alerts.Alert", alert_type="availability", agent=cycle(agents), _quantity=3
"alerts.Alert",
alert_type=AlertType.AVAILABILITY,
agent=cycle(agents),
_quantity=3,
)
baker.make("alerts.Alert", alert_type="custom", _quantity=4)
baker.make("alerts.Alert", alert_type=AlertType.CUSTOM, _quantity=4)
# test super user access
r = self.check_authorized_superuser("patch", f"{base_url}/")
@@ -1568,22 +1621,27 @@ class TestAlertPermissions(TacticalTestCase):
tasks = baker.make("autotasks.AutomatedTask", agent=cycle(agents), _quantity=3)
alert_tasks = baker.make(
"alerts.Alert",
alert_type="task",
alert_type=AlertType.TASK,
agent=cycle(agents),
assigned_task=cycle(tasks),
_quantity=3,
)
alert_checks = baker.make(
"alerts.Alert",
alert_type="check",
alert_type=AlertType.CHECK,
agent=cycle(agents),
assigned_check=cycle(checks),
_quantity=3,
)
alert_agents = baker.make(
"alerts.Alert", alert_type="availability", agent=cycle(agents), _quantity=3
"alerts.Alert",
alert_type=AlertType.AVAILABILITY,
agent=cycle(agents),
_quantity=3,
)
alert_custom = baker.make(
"alerts.Alert", alert_type=AlertType.CUSTOM, _quantity=4
)
alert_custom = baker.make("alerts.Alert", alert_type="custom", _quantity=4)
# alert task url
task_url = f"{base_url}/{alert_tasks[0].id}/" # for agent
@@ -1604,8 +1662,7 @@ class TestAlertPermissions(TacticalTestCase):
unauthorized_task_url,
]
for method in ["get", "put", "delete"]:
for method in ("get", "put", "delete"):
# test superuser access
for url in authorized_urls:
self.check_authorized_superuser(method, url)
@@ -1660,7 +1717,7 @@ class TestAlertPermissions(TacticalTestCase):
agent = baker.make_recipe("agents.agent")
alerts = baker.make(
"alerts.Alert",
alert_type="availability",
alert_type=AlertType.AVAILABILITY,
agent=agent,
resolved=False,
_quantity=3,
@@ -1674,7 +1731,7 @@ class TestAlertPermissions(TacticalTestCase):
# make sure only 1 alert is not resolved
self.assertEqual(
Alert.objects.filter(
alert_type="availability", agent=agent, resolved=False
alert_type=AlertType.AVAILABILITY, agent=agent, resolved=False
).count(),
1,
)
@@ -1684,7 +1741,7 @@ class TestAlertPermissions(TacticalTestCase):
check = baker.make_recipe("checks.diskspace_check", agent=agent)
alerts = baker.make(
"alerts.Alert",
alert_type="check",
alert_type=AlertType.CHECK,
assigned_check=check,
agent=agent,
resolved=False,
@@ -1699,7 +1756,7 @@ class TestAlertPermissions(TacticalTestCase):
# make sure only 1 alert is not resolved
self.assertEqual(
Alert.objects.filter(
alert_type="check", agent=agent, resolved=False
alert_type=AlertType.CHECK, agent=agent, resolved=False
).count(),
1,
)
@@ -1709,7 +1766,7 @@ class TestAlertPermissions(TacticalTestCase):
task = baker.make("autotasks.AutomatedTask", agent=agent)
alerts = baker.make(
"alerts.Alert",
alert_type="task",
alert_type=AlertType.TASK,
assigned_task=task,
agent=agent,
resolved=False,
@@ -1724,7 +1781,7 @@ class TestAlertPermissions(TacticalTestCase):
# make sure only 1 alert is not resolved
self.assertEqual(
Alert.objects.filter(
alert_type="task", agent=agent, resolved=False
alert_type=AlertType.TASK, agent=agent, resolved=False
).count(),
1,
)

View File

@@ -7,7 +7,7 @@ from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from tacticalrmm.utils import notify_error
from tacticalrmm.helpers import notify_error
from .models import Alert, AlertTemplate
from .permissions import AlertPerms, AlertTemplatePerms
@@ -23,15 +23,18 @@ class GetAddAlerts(APIView):
permission_classes = [IsAuthenticated, AlertPerms]
def patch(self, request):
# 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()
alerts = (
Alert.objects.filter_by_role(request.user) # type: ignore
.filter(resolved=False, snoozed=False, hidden=False)
.order_by("alert_time")[: int(request.data["top"])]
)
count = (
Alert.objects.filter_by_role(request.user) # type: ignore
.filter(resolved=False, snoozed=False, hidden=False)
.count()
)
return Response(
{
"alerts_count": count,
@@ -41,13 +44,13 @@ class GetAddAlerts(APIView):
elif any(
key
in [
in (
"timeFilter",
"clientFilter",
"severityFilter",
"resolvedFilter",
"snoozedFilter",
]
)
for key in request.data.keys()
):
clientFilter = Q()

View File

View File

@@ -1,11 +1,8 @@
import json
import os
from autotasks.models import TaskResult
from django.conf import settings
from django.utils import timezone as djangotime
from model_bakery import baker
from autotasks.models import TaskResult
from tacticalrmm.constants import CustomFieldModel, CustomFieldType, TaskStatus
from tacticalrmm.test import TacticalTestCase
@@ -70,24 +67,6 @@ class TestAPIv3(TacticalTestCase):
self.check_not_authenticated("get", url)
def test_sysinfo(self):
# TODO replace this with golang wmi sample data
url = "/api/v3/sysinfo/"
with open(
os.path.join(
settings.BASE_DIR, "tacticalrmm/test_data/wmi_python_agent.json"
)
) as f:
wmi_py = json.load(f)
payload = {"agent_id": self.agent.agent_id, "sysinfo": wmi_py}
r = self.client.patch(url, payload, format="json")
self.assertEqual(r.status_code, 200)
self.check_not_authenticated("patch", url)
def test_checkrunner_interval(self):
url = f"/api/v3/{self.agent.agent_id}/checkinterval/"
r = self.client.get(url, format="json")
@@ -98,9 +77,7 @@ class TestAPIv3(TacticalTestCase):
)
# add check to agent with check interval set
check = baker.make_recipe(
"checks.ping_check", agent=self.agent, run_interval=30
)
baker.make_recipe("checks.ping_check", agent=self.agent, run_interval=30)
r = self.client.get(url, format="json")
self.assertEqual(r.status_code, 200)
@@ -110,7 +87,7 @@ class TestAPIv3(TacticalTestCase):
)
# minimum check run interval is 15 seconds
check = baker.make_recipe("checks.ping_check", agent=self.agent, run_interval=5)
baker.make_recipe("checks.ping_check", agent=self.agent, run_interval=5)
r = self.client.get(url, format="json")
self.assertEqual(r.status_code, 200)
@@ -137,8 +114,6 @@ class TestAPIv3(TacticalTestCase):
self.assertEqual(len(r.json()["checks"]), 15)
def test_task_runner_get(self):
from autotasks.serializers import TaskGOGetSerializer
r = self.client.get("/api/v3/500/asdf9df9dfdf/taskrunner/")
self.assertEqual(r.status_code, 404)
@@ -152,8 +127,15 @@ class TestAPIv3(TacticalTestCase):
"script": script.id,
"script_args": ["test"],
"timeout": 30,
"env_vars": ["hello=world", "foo=bar"],
},
{
"type": "script",
"script": 3,
"script_args": [],
"timeout": 30,
"env_vars": ["hello=world", "foo=bar"],
},
{"type": "script", "script": 3, "script_args": [], "timeout": 30},
]
agent = baker.make_recipe("agents.agent")
@@ -163,7 +145,6 @@ class TestAPIv3(TacticalTestCase):
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(TaskGOGetSerializer(task).data, r.data)
def test_task_runner_results(self):
from agents.models import AgentCustomField
@@ -188,7 +169,9 @@ class TestAPIv3(TacticalTestCase):
r = self.client.patch(url, data)
self.assertEqual(r.status_code, 200)
self.assertTrue(TaskResult.objects.get(pk=task_result.pk).status == "passing")
self.assertTrue(
TaskResult.objects.get(pk=task_result.pk).status == TaskStatus.PASSING
)
# test failing task
data = {
@@ -200,15 +183,28 @@ class TestAPIv3(TacticalTestCase):
r = self.client.patch(url, data)
self.assertEqual(r.status_code, 200)
self.assertTrue(TaskResult.objects.get(pk=task_result.pk).status == "failing")
self.assertTrue(
TaskResult.objects.get(pk=task_result.pk).status == TaskStatus.FAILING
)
# test collector task
text = baker.make("core.CustomField", model="agent", type="text", name="Test")
text = baker.make(
"core.CustomField",
model=CustomFieldModel.AGENT,
type=CustomFieldType.TEXT,
name="Test",
)
boolean = baker.make(
"core.CustomField", model="agent", type="checkbox", name="Test1"
"core.CustomField",
model=CustomFieldModel.AGENT,
type=CustomFieldType.CHECKBOX,
name="Test1",
)
multiple = baker.make(
"core.CustomField", model="agent", type="multiple", name="Test2"
"core.CustomField",
model=CustomFieldModel.AGENT,
type=CustomFieldType.MULTIPLE,
name="Test2",
)
# test text fields
@@ -225,7 +221,9 @@ class TestAPIv3(TacticalTestCase):
r = self.client.patch(url, data)
self.assertEqual(r.status_code, 200)
self.assertTrue(TaskResult.objects.get(pk=task_result.pk).status == "failing")
self.assertTrue(
TaskResult.objects.get(pk=task_result.pk).status == TaskStatus.FAILING
)
# test saving to text field
data = {
@@ -237,7 +235,9 @@ class TestAPIv3(TacticalTestCase):
r = self.client.patch(url, data)
self.assertEqual(r.status_code, 200)
self.assertEqual(TaskResult.objects.get(pk=task_result.pk).status, "passing")
self.assertEqual(
TaskResult.objects.get(pk=task_result.pk).status, TaskStatus.PASSING
)
self.assertEqual(
AgentCustomField.objects.get(field=text, agent=task.agent).value,
"the last line",
@@ -256,7 +256,9 @@ class TestAPIv3(TacticalTestCase):
r = self.client.patch(url, data)
self.assertEqual(r.status_code, 200)
self.assertEqual(TaskResult.objects.get(pk=task_result.pk).status, "passing")
self.assertEqual(
TaskResult.objects.get(pk=task_result.pk).status, TaskStatus.PASSING
)
self.assertTrue(
AgentCustomField.objects.get(field=boolean, agent=task.agent).value
)
@@ -274,7 +276,9 @@ class TestAPIv3(TacticalTestCase):
r = self.client.patch(url, data)
self.assertEqual(r.status_code, 200)
self.assertEqual(TaskResult.objects.get(pk=task_result.pk).status, "passing")
self.assertEqual(
TaskResult.objects.get(pk=task_result.pk).status, TaskStatus.PASSING
)
self.assertEqual(
AgentCustomField.objects.get(field=multiple, agent=task.agent).value,
["this", "is", "an", "array"],
@@ -290,8 +294,16 @@ class TestAPIv3(TacticalTestCase):
r = self.client.patch(url, data)
self.assertEqual(r.status_code, 200)
self.assertEqual(TaskResult.objects.get(pk=task_result.pk).status, "passing")
self.assertEqual(
TaskResult.objects.get(pk=task_result.pk).status, TaskStatus.PASSING
)
self.assertEqual(
AgentCustomField.objects.get(field=multiple, agent=task.agent).value,
["this"],
)
def test_get_agent_config(self):
agent = baker.make_recipe("agents.online_agent")
url = f"/api/v3/{agent.agent_id}/config/"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)

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