Compare commits
	
		
			858 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					400b1a9e17 | ||
| 
						 | 
					0669a126ed | ||
| 
						 | 
					d5fc77e70a | ||
| 
						 | 
					079c987c44 | ||
| 
						 | 
					e4fb4ac28a | ||
| 
						 | 
					10fd07577f | ||
| 
						 | 
					83b4d8c686 | ||
| 
						 | 
					0a2547d65c | ||
| 
						 | 
					5ee2a3cb54 | ||
| 
						 | 
					e505d0768c | ||
| 
						 | 
					6d4fe84ddc | ||
| 
						 | 
					2e6c9795ec | ||
| 
						 | 
					c6b667f8b3 | ||
| 
						 | 
					ad4cddb4f3 | ||
| 
						 | 
					ddba83b993 | ||
| 
						 | 
					91c33b0431 | ||
| 
						 | 
					d1df40633a | ||
| 
						 | 
					7f9fc484e8 | ||
| 
						 | 
					ecf564648e | ||
| 
						 | 
					150e3190bc | ||
| 
						 | 
					63947346e9 | ||
| 
						 | 
					86816ce357 | ||
| 
						 | 
					0d34831df4 | ||
| 
						 | 
					c35da67401 | ||
| 
						 | 
					fb47022380 | ||
| 
						 | 
					46c5128418 | ||
| 
						 | 
					4a5bfee616 | ||
| 
						 | 
					f8314e0f8e | ||
| 
						 | 
					9624af4e67 | ||
| 
						 | 
					5bec4768e7 | ||
| 
						 | 
					3851b0943a | ||
| 
						 | 
					cc1f640a50 | ||
| 
						 | 
					ec0a2dc053 | ||
| 
						 | 
					a6166a1ad7 | ||
| 
						 | 
					41e3d1f490 | ||
| 
						 | 
					2cbecaa552 | ||
| 
						 | 
					8d543dcc7d | ||
| 
						 | 
					18b1afe34f | ||
| 
						 | 
					0f86bbfad8 | ||
| 
						 | 
					0d021a800a | ||
| 
						 | 
					038304384a | ||
| 
						 | 
					2c09ad6b91 | ||
| 
						 | 
					0bd09d03c1 | ||
| 
						 | 
					faa0e6c289 | ||
| 
						 | 
					c28d800d7f | ||
| 
						 | 
					4fd772ecd8 | ||
| 
						 | 
					5520a84062 | ||
| 
						 | 
					66c7123f7c | ||
| 
						 | 
					bacf4154fd | ||
| 
						 | 
					61790d2261 | ||
| 
						 | 
					899111a310 | ||
| 
						 | 
					3bfa35e1c7 | ||
| 
						 | 
					ebefcb7fc1 | ||
| 
						 | 
					ce11685371 | ||
| 
						 | 
					9edb848947 | ||
| 
						 | 
					f326096fad | ||
| 
						 | 
					46f0b23f4f | ||
| 
						 | 
					1c1d3bd619 | ||
| 
						 | 
					d894f92d5e | ||
| 
						 | 
					6c44191fe4 | ||
| 
						 | 
					0deb78a9af | ||
| 
						 | 
					9c15f4ba88 | ||
| 
						 | 
					4ba27ec1d6 | ||
| 
						 | 
					c8dd80530a | ||
| 
						 | 
					eda5ea7d1a | ||
| 
						 | 
					77a916e1a8 | ||
| 
						 | 
					7ba2a4b27b | ||
| 
						 | 
					d33f69720a | ||
| 
						 | 
					59c880dc36 | ||
| 
						 | 
					e5c355e8f9 | ||
| 
						 | 
					d36fadf3ca | ||
| 
						 | 
					b618cbdf7c | ||
| 
						 | 
					15ec7173aa | ||
| 
						 | 
					4166e92754 | ||
| 
						 | 
					85166b6e8b | ||
| 
						 | 
					5278599675 | ||
| 
						 | 
					18cac8ba5d | ||
| 
						 | 
					dfccbceea6 | ||
| 
						 | 
					fc4b651e46 | ||
| 
						 | 
					fb89922ecf | ||
| 
						 | 
					8ab23c8cd9 | ||
| 
						 | 
					787a2c5071 | ||
| 
						 | 
					da76a20345 | ||
| 
						 | 
					9688dbdb36 | ||
| 
						 | 
					6fa16e1a5e | ||
| 
						 | 
					71a2e3cfca | ||
| 
						 | 
					e9c0f7e200 | ||
| 
						 | 
					25154a4331 | ||
| 
						 | 
					22c152f600 | ||
| 
						 | 
					3eab61cbc3 | ||
| 
						 | 
					a029c1d0db | ||
| 
						 | 
					706757d215 | ||
| 
						 | 
					9054c233f4 | ||
| 
						 | 
					efb0748fc9 | ||
| 
						 | 
					751b0ef716 | ||
| 
						 | 
					716450b97e | ||
| 
						 | 
					2c289a4d8f | ||
| 
						 | 
					a4ad4c033f | ||
| 
						 | 
					511bca9d66 | ||
| 
						 | 
					ac3fb03b2d | ||
| 
						 | 
					282087d0f3 | ||
| 
						 | 
					781282599c | ||
| 
						 | 
					d611ab0ee2 | ||
| 
						 | 
					411cbdffee | ||
| 
						 | 
					cfd19e02a7 | ||
| 
						 | 
					717eeb3903 | ||
| 
						 | 
					a394fb8757 | ||
| 
						 | 
					2125a7ffdb | ||
| 
						 | 
					00c0a6ec60 | ||
| 
						 | 
					090bcf89ac | ||
| 
						 | 
					4a768dec48 | ||
| 
						 | 
					c8d72ddd3b | ||
| 
						 | 
					5cf618695f | ||
| 
						 | 
					8a1f497265 | ||
| 
						 | 
					acdf20f800 | ||
| 
						 | 
					dbd1003002 | ||
| 
						 | 
					48db3d3fcc | ||
| 
						 | 
					41ccd14f25 | ||
| 
						 | 
					60800df798 | ||
| 
						 | 
					9c36f2cbc5 | ||
| 
						 | 
					0b4fff907a | ||
| 
						 | 
					442f09d0fe | ||
| 
						 | 
					50af28b2aa | ||
| 
						 | 
					28ad74a68e | ||
| 
						 | 
					13cdbae38f | ||
| 
						 | 
					55c77df5ae | ||
| 
						 | 
					9b1d2fd985 | ||
| 
						 | 
					91b7ea0367 | ||
| 
						 | 
					96d3926d09 | ||
| 
						 | 
					c709b5a7eb | ||
| 
						 | 
					df82914005 | ||
| 
						 | 
					b1bdc38283 | ||
| 
						 | 
					beb1215329 | ||
| 
						 | 
					51784388b9 | ||
| 
						 | 
					dbbbd53a4d | ||
| 
						 | 
					f9d992c969 | ||
| 
						 | 
					29a4d61e90 | ||
| 
						 | 
					2667cdb26c | ||
| 
						 | 
					a1669a5104 | ||
| 
						 | 
					059f1bd63d | ||
| 
						 | 
					82ae5e442c | ||
| 
						 | 
					b10114cd7c | ||
| 
						 | 
					33f730aac4 | ||
| 
						 | 
					92fdfdb05c | ||
| 
						 | 
					fbaf3f3623 | ||
| 
						 | 
					5f400bc513 | ||
| 
						 | 
					0fc59645fc | ||
| 
						 | 
					e2dee272b8 | ||
| 
						 | 
					364cf362f4 | ||
| 
						 | 
					8394a263c4 | ||
| 
						 | 
					0e9aa26cfc | ||
| 
						 | 
					6a23d63266 | ||
| 
						 | 
					af2fc15964 | ||
| 
						 | 
					5919037a4a | ||
| 
						 | 
					a761dab229 | ||
| 
						 | 
					fa656e1f56 | ||
| 
						 | 
					77e141e84a | ||
| 
						 | 
					2439965fa8 | ||
| 
						 | 
					f66afbee90 | ||
| 
						 | 
					5a89d23a67 | ||
| 
						 | 
					07c8dad1c3 | ||
| 
						 | 
					beb8b18e98 | ||
| 
						 | 
					887bb5d7cc | ||
| 
						 | 
					4a9542d970 | ||
| 
						 | 
					c049d9d5ff | ||
| 
						 | 
					c2cc4389a0 | ||
| 
						 | 
					12b5011266 | ||
| 
						 | 
					6e3cad454c | ||
| 
						 | 
					8251bd028c | ||
| 
						 | 
					da87d452c2 | ||
| 
						 | 
					9bca0dfb3c | ||
| 
						 | 
					57904c4a97 | ||
| 
						 | 
					4e74d851e9 | ||
| 
						 | 
					e5c1f69b02 | ||
| 
						 | 
					9d390d064c | ||
| 
						 | 
					4994d7892c | ||
| 
						 | 
					1ea06e3c42 | ||
| 
						 | 
					a4b7a6dfc7 | ||
| 
						 | 
					7fe1cce606 | ||
| 
						 | 
					7e5abe32e0 | ||
| 
						 | 
					47caf7c142 | ||
| 
						 | 
					cf4d777344 | ||
| 
						 | 
					255927c346 | ||
| 
						 | 
					e8c5fc79a6 | ||
| 
						 | 
					b309b24d0b | ||
| 
						 | 
					13f4cca9d5 | ||
| 
						 | 
					b3c0273e0c | ||
| 
						 | 
					1df7fdf703 | ||
| 
						 | 
					cbf38309e2 | ||
| 
						 | 
					2ec7257dd7 | ||
| 
						 | 
					531aac6923 | ||
| 
						 | 
					59b4604c77 | ||
| 
						 | 
					52aa269af9 | ||
| 
						 | 
					8a03d9c498 | ||
| 
						 | 
					a36fc7ecfd | ||
| 
						 | 
					7b0c269bce | ||
| 
						 | 
					c10bf9b357 | ||
| 
						 | 
					0606642953 | ||
| 
						 | 
					d1b2cae201 | ||
| 
						 | 
					097e567122 | ||
| 
						 | 
					d22e1d6a24 | ||
| 
						 | 
					2827069bd9 | ||
| 
						 | 
					614e3bd2a0 | ||
| 
						 | 
					ff756a01d2 | ||
| 
						 | 
					db14606dbe | ||
| 
						 | 
					de0a69ede5 | ||
| 
						 | 
					5bf5065d9a | ||
| 
						 | 
					0235dadbf7 | ||
| 
						 | 
					203a15b447 | ||
| 
						 | 
					fe4dfe2194 | ||
| 
						 | 
					c2eb93abe0 | ||
| 
						 | 
					d32b834ae7 | ||
| 
						 | 
					cecf45a698 | ||
| 
						 | 
					69cd348cc3 | ||
| 
						 | 
					868025ffa3 | ||
| 
						 | 
					60126a8cc5 | ||
| 
						 | 
					8cfba49559 | ||
| 
						 | 
					168f053c6f | ||
| 
						 | 
					897e1d4539 | ||
| 
						 | 
					5ef6a0f4ea | ||
| 
						 | 
					eb80e32812 | ||
| 
						 | 
					620dadafe4 | ||
| 
						 | 
					e76fa878d2 | ||
| 
						 | 
					376b421eb9 | ||
| 
						 | 
					e1643aca80 | ||
| 
						 | 
					4e97c0c5c9 | ||
| 
						 | 
					2d51b122af | ||
| 
						 | 
					05b88a3c73 | ||
| 
						 | 
					3c087d49e9 | ||
| 
						 | 
					d81fcccf10 | ||
| 
						 | 
					ee3a7bbbfc | ||
| 
						 | 
					82d9e2fb16 | ||
| 
						 | 
					6ab39d6f70 | ||
| 
						 | 
					4aa413e697 | ||
| 
						 | 
					04b3fc54b0 | ||
| 
						 | 
					e4c5a4e886 | ||
| 
						 | 
					a0ee7a59eb | ||
| 
						 | 
					b4a05160df | ||
| 
						 | 
					1a437b3961 | ||
| 
						 | 
					bda8555190 | ||
| 
						 | 
					10ca38f91d | ||
| 
						 | 
					a468faad20 | ||
| 
						 | 
					7a20be4aff | ||
| 
						 | 
					06b974c8a4 | ||
| 
						 | 
					7284d9fcd8 | ||
| 
						 | 
					515394049a | ||
| 
						 | 
					35c8b4f535 | ||
| 
						 | 
					1a325a66b4 | ||
| 
						 | 
					7d82116fb9 | ||
| 
						 | 
					8a7bd4f21b | ||
| 
						 | 
					2e5a2ef12d | ||
| 
						 | 
					89aceda65a | ||
| 
						 | 
					39fd83aa16 | ||
| 
						 | 
					a23d811fe8 | ||
| 
						 | 
					a238779724 | ||
| 
						 | 
					3a848bc037 | ||
| 
						 | 
					0528ecb454 | ||
| 
						 | 
					141835593c | ||
| 
						 | 
					3d06200368 | ||
| 
						 | 
					729bef9a77 | ||
| 
						 | 
					94f33bd642 | ||
| 
						 | 
					7e010cdbca | ||
| 
						 | 
					8887bcd941 | ||
| 
						 | 
					56aeeee04c | ||
| 
						 | 
					98eb3c7287 | ||
| 
						 | 
					6819c1989b | ||
| 
						 | 
					7e01dd3e97 | ||
| 
						 | 
					ea4f2c3de8 | ||
| 
						 | 
					b2f63b8761 | ||
| 
						 | 
					65865101ce | ||
| 
						 | 
					c3637afe69 | ||
| 
						 | 
					ab543ddf0c | ||
| 
						 | 
					80595e76e7 | ||
| 
						 | 
					d49e68737a | ||
| 
						 | 
					712e15ba80 | ||
| 
						 | 
					986160e667 | ||
| 
						 | 
					1ae4e23db1 | ||
| 
						 | 
					bad646141c | ||
| 
						 | 
					7911235b68 | ||
| 
						 | 
					12dee4d14d | ||
| 
						 | 
					cba841beb8 | ||
| 
						 | 
					4e3ebf7078 | ||
| 
						 | 
					1c34969f64 | ||
| 
						 | 
					dc26cabacd | ||
| 
						 | 
					a7bffcd471 | ||
| 
						 | 
					6ae56ac2cc | ||
| 
						 | 
					03c087020c | ||
| 
						 | 
					857a1ab9c4 | ||
| 
						 | 
					64d9530e13 | ||
| 
						 | 
					5dac1efc30 | ||
| 
						 | 
					18bc74bc96 | ||
| 
						 | 
					f64efc63f8 | ||
| 
						 | 
					e84b897991 | ||
| 
						 | 
					519647ef93 | ||
| 
						 | 
					f694fe00e4 | ||
| 
						 | 
					0b951f27b6 | ||
| 
						 | 
					8aa082c9df | ||
| 
						 | 
					f2c5d47bd8 | ||
| 
						 | 
					ac7642cc15 | ||
| 
						 | 
					8f34865dab | ||
| 
						 | 
					c762d12a40 | ||
| 
						 | 
					fe1e71dc07 | ||
| 
						 | 
					85b0350ed4 | ||
| 
						 | 
					a980491455 | ||
| 
						 | 
					5798c0ccaa | ||
| 
						 | 
					742f49ca1f | ||
| 
						 | 
					5560fc805b | ||
| 
						 | 
					9d4f8a4e8c | ||
| 
						 | 
					b4d25d6285 | ||
| 
						 | 
					a504a376bd | ||
| 
						 | 
					f61ea6e90a | ||
| 
						 | 
					b2651df36f | ||
| 
						 | 
					b56c086841 | ||
| 
						 | 
					0b92fee42e | ||
| 
						 | 
					4343478c7b | ||
| 
						 | 
					94649cbfc7 | ||
| 
						 | 
					fb83f84d84 | ||
| 
						 | 
					e099a5a32e | ||
| 
						 | 
					84c2632d40 | ||
| 
						 | 
					3417ee25eb | ||
| 
						 | 
					6ada30102c | ||
| 
						 | 
					ac86ca7266 | ||
| 
						 | 
					bb1d3edf71 | ||
| 
						 | 
					97b9253017 | ||
| 
						 | 
					971c2180c9 | ||
| 
						 | 
					f96dc6991e | ||
| 
						 | 
					6855493b2f | ||
| 
						 | 
					ff0d1f7c42 | ||
| 
						 | 
					3ae5824761 | ||
| 
						 | 
					702e865715 | ||
| 
						 | 
					6bcf64c83f | ||
| 
						 | 
					18b270c9d0 | ||
| 
						 | 
					783376acb0 | ||
| 
						 | 
					81dab470d2 | ||
| 
						 | 
					a12f0feb66 | ||
| 
						 | 
					d3c99d9c1c | ||
| 
						 | 
					3eb3586c0f | ||
| 
						 | 
					fdde16cf56 | ||
| 
						 | 
					b8bc5596fd | ||
| 
						 | 
					47842a79c7 | ||
| 
						 | 
					391d5bc386 | ||
| 
						 | 
					ba8561e357 | ||
| 
						 | 
					6aa1170cef | ||
| 
						 | 
					6d4363e685 | ||
| 
						 | 
					6b02b1e1e8 | ||
| 
						 | 
					df3e68fbaf | ||
| 
						 | 
					58a5550989 | ||
| 
						 | 
					ccc9e44ace | ||
| 
						 | 
					f225c5cf9a | ||
| 
						 | 
					5c62c7992c | ||
| 
						 | 
					70b8f09ccb | ||
| 
						 | 
					abfeafa026 | ||
| 
						 | 
					aa029b005f | ||
| 
						 | 
					6cc55e8f36 | ||
| 
						 | 
					b753d2ca1e | ||
| 
						 | 
					1e50329c9e | ||
| 
						 | 
					4942811694 | ||
| 
						 | 
					59e37e0ccb | ||
| 
						 | 
					20aa86d8a9 | ||
| 
						 | 
					64c5ab7042 | ||
| 
						 | 
					d210f5171a | ||
| 
						 | 
					c7eee0f14d | ||
| 
						 | 
					221753b62e | ||
| 
						 | 
					d213e4d37f | ||
| 
						 | 
					f8695f21d3 | ||
| 
						 | 
					4ac1030289 | ||
| 
						 | 
					93c7117319 | ||
| 
						 | 
					974afd92ce | ||
| 
						 | 
					dd1d15f1a4 | ||
| 
						 | 
					be847baaed | ||
| 
						 | 
					2b819e6751 | ||
| 
						 | 
					66247cc005 | ||
| 
						 | 
					eafd38d3f2 | ||
| 
						 | 
					c4e590e7a0 | ||
| 
						 | 
					b92a594114 | ||
| 
						 | 
					9dfb16f6b8 | ||
| 
						 | 
					4b74866d85 | ||
| 
						 | 
					f532c85247 | ||
| 
						 | 
					b1cc00c1bc | ||
| 
						 | 
					5696aa49d5 | ||
| 
						 | 
					e12dc936fd | ||
| 
						 | 
					6d39a7fb75 | ||
| 
						 | 
					c87c312349 | ||
| 
						 | 
					e9c1886cdd | ||
| 
						 | 
					13e4b1a781 | ||
| 
						 | 
					3766fb14ef | ||
| 
						 | 
					29ee50e38b | ||
| 
						 | 
					d1ab69dc31 | ||
| 
						 | 
					e3c4a54193 | ||
| 
						 | 
					2abbd2e3cf | ||
| 
						 | 
					f9387a5851 | ||
| 
						 | 
					7a9fb74b54 | ||
| 
						 | 
					d754f3dd4c | ||
| 
						 | 
					f54fc9e990 | ||
| 
						 | 
					8952095da5 | ||
| 
						 | 
					597240d501 | ||
| 
						 | 
					7377906d02 | ||
| 
						 | 
					ce6da1bce3 | ||
| 
						 | 
					1bf8ff73f8 | ||
| 
						 | 
					564aaaf3df | ||
| 
						 | 
					64ba69b2d0 | ||
| 
						 | 
					ce5ada42af | ||
| 
						 | 
					1ce5973713 | ||
| 
						 | 
					b035b53092 | ||
| 
						 | 
					7d0e02358c | ||
| 
						 | 
					374ff0aeb5 | ||
| 
						 | 
					947a43111e | ||
| 
						 | 
					9970911249 | ||
| 
						 | 
					5fed81c27b | ||
| 
						 | 
					dce4f1a5ae | ||
| 
						 | 
					7e1fc32a1c | ||
| 
						 | 
					a69f14f504 | ||
| 
						 | 
					931069458d | ||
| 
						 | 
					a5259baab0 | ||
| 
						 | 
					8aaa27350d | ||
| 
						 | 
					6db6eb70da | ||
| 
						 | 
					ac74d2b7c2 | ||
| 
						 | 
					2b316aeae9 | ||
| 
						 | 
					aff96a45c6 | ||
| 
						 | 
					9ee246440f | ||
| 
						 | 
					e2f524ce7a | ||
| 
						 | 
					a58b054292 | ||
| 
						 | 
					ea9e5be1fc | ||
| 
						 | 
					760ea4727c | ||
| 
						 | 
					f57f2e53a0 | ||
| 
						 | 
					136a393a17 | ||
| 
						 | 
					8bbaab78b7 | ||
| 
						 | 
					067cd59637 | ||
| 
						 | 
					ce6ac7bf53 | ||
| 
						 | 
					99271c4477 | ||
| 
						 | 
					156142ed58 | ||
| 
						 | 
					4b5516c0eb | ||
| 
						 | 
					c3d8d2d240 | ||
| 
						 | 
					c29cf70025 | ||
| 
						 | 
					6ebce55be3 | ||
| 
						 | 
					01c4a85bc0 | ||
| 
						 | 
					12d4206d84 | ||
| 
						 | 
					946de18bea | ||
| 
						 | 
					904eb3538c | ||
| 
						 | 
					c851ca9328 | ||
| 
						 | 
					0ac415ad83 | ||
| 
						 | 
					b3ba34d980 | ||
| 
						 | 
					52740271d9 | ||
| 
						 | 
					c2e444249a | ||
| 
						 | 
					97310b091e | ||
| 
						 | 
					4dda9cc3a1 | ||
| 
						 | 
					a0538b57e2 | ||
| 
						 | 
					d7f394eeb6 | ||
| 
						 | 
					1bc4571d42 | ||
| 
						 | 
					22e878502a | ||
| 
						 | 
					03c1b6e30c | ||
| 
						 | 
					374a434d98 | ||
| 
						 | 
					f1e85ff0e9 | ||
| 
						 | 
					6b010f76ea | ||
| 
						 | 
					0c3e9f7824 | ||
| 
						 | 
					ccca578622 | ||
| 
						 | 
					56f7c18550 | ||
| 
						 | 
					d438f71bbb | ||
| 
						 | 
					ca5df24b6d | ||
| 
						 | 
					4a6c2d106f | ||
| 
						 | 
					cd25a9568b | ||
| 
						 | 
					f78a787adb | ||
| 
						 | 
					dc520fa77c | ||
| 
						 | 
					8f06d4dd9d | ||
| 
						 | 
					a7047183e1 | ||
| 
						 | 
					c0b145da24 | ||
| 
						 | 
					52e7fd6f72 | ||
| 
						 | 
					4bbe22b1c7 | ||
| 
						 | 
					4747ffc08b | ||
| 
						 | 
					9d07131fd6 | ||
| 
						 | 
					721126d3db | ||
| 
						 | 
					2b65f5e3dc | ||
| 
						 | 
					57f10cf387 | ||
| 
						 | 
					f60c8a173b | ||
| 
						 | 
					857cd690be | ||
| 
						 | 
					a407b60152 | ||
| 
						 | 
					2c3c55adc0 | ||
| 
						 | 
					f586b4da17 | ||
| 
						 | 
					0b7eb41049 | ||
| 
						 | 
					bd19c4e2bd | ||
| 
						 | 
					e8a73087d6 | ||
| 
						 | 
					dde4fd82f4 | ||
| 
						 | 
					0420c393f3 | ||
| 
						 | 
					c88dac6437 | ||
| 
						 | 
					cd450f55e2 | ||
| 
						 | 
					190ee7f9fb | ||
| 
						 | 
					fd057300cc | ||
| 
						 | 
					56791089c1 | ||
| 
						 | 
					e91cb32ca3 | ||
| 
						 | 
					9ab20df8d2 | ||
| 
						 | 
					050350501c | ||
| 
						 | 
					d078acdf73 | ||
| 
						 | 
					b786a688b5 | ||
| 
						 | 
					6b7fe40dd2 | ||
| 
						 | 
					6f6c422246 | ||
| 
						 | 
					d371ff4f60 | ||
| 
						 | 
					d1a8348912 | ||
| 
						 | 
					be956d3cb6 | ||
| 
						 | 
					ba5beb81b7 | ||
| 
						 | 
					106bbe5244 | ||
| 
						 | 
					f39d0e7ba2 | ||
| 
						 | 
					de7a1fd8ff | ||
| 
						 | 
					1ac2b25876 | ||
| 
						 | 
					9e014d1371 | ||
| 
						 | 
					93b274a113 | ||
| 
						 | 
					474c7ae873 | ||
| 
						 | 
					31690d4cad | ||
| 
						 | 
					bbfc7e7e49 | ||
| 
						 | 
					1c0aa55e7a | ||
| 
						 | 
					29778ca19e | ||
| 
						 | 
					9e87318cc5 | ||
| 
						 | 
					c645be6b70 | ||
| 
						 | 
					57fc5ac088 | ||
| 
						 | 
					924774f52a | ||
| 
						 | 
					446a7a0844 | ||
| 
						 | 
					5cfeed76d0 | ||
| 
						 | 
					de419319d8 | ||
| 
						 | 
					7a3d36899b | ||
| 
						 | 
					f5dbb363f4 | ||
| 
						 | 
					2bbc59a212 | ||
| 
						 | 
					3403d76aae | ||
| 
						 | 
					58399cedb6 | ||
| 
						 | 
					9bca7e9e11 | ||
| 
						 | 
					3a61430e44 | ||
| 
						 | 
					7d8c783a7d | ||
| 
						 | 
					a2e996b550 | ||
| 
						 | 
					cfc1c31050 | ||
| 
						 | 
					45106bf6f9 | ||
| 
						 | 
					6e3cfe491b | ||
| 
						 | 
					12f2158afd | ||
| 
						 | 
					6d78773c55 | ||
| 
						 | 
					43a62d4eb6 | ||
| 
						 | 
					cc08dfda96 | ||
| 
						 | 
					622e33588e | ||
| 
						 | 
					67980b58a0 | ||
| 
						 | 
					027e444955 | ||
| 
						 | 
					d838750389 | ||
| 
						 | 
					71d8bd5266 | ||
| 
						 | 
					ec4ae24bbd | ||
| 
						 | 
					1128149359 | ||
| 
						 | 
					bdfc6634ec | ||
| 
						 | 
					ca4d19667b | ||
| 
						 | 
					c71aa7baa7 | ||
| 
						 | 
					fd80ccd2c5 | ||
| 
						 | 
					9dc0b24399 | ||
| 
						 | 
					747954e6fb | ||
| 
						 | 
					274f4f227e | ||
| 
						 | 
					92197d8d49 | ||
| 
						 | 
					aee06920eb | ||
| 
						 | 
					5111b17d3c | ||
| 
						 | 
					2849d8f45d | ||
| 
						 | 
					bac60d9bd4 | ||
| 
						 | 
					9c797162f4 | ||
| 
						 | 
					09d184e2f8 | ||
| 
						 | 
					7bca618906 | ||
| 
						 | 
					67607103e9 | ||
| 
						 | 
					73c9956fe4 | ||
| 
						 | 
					b42f2ffe33 | ||
| 
						 | 
					30a3f185ef | ||
| 
						 | 
					4f1b41227f | ||
| 
						 | 
					83b9d13ec9 | ||
| 
						 | 
					cee7896c37 | ||
| 
						 | 
					0377009d2b | ||
| 
						 | 
					b472f3644e | ||
| 
						 | 
					5d8ea837c8 | ||
| 
						 | 
					82de6bc849 | ||
| 
						 | 
					cb4bc68c48 | ||
| 
						 | 
					3ce6b38247 | ||
| 
						 | 
					716c0fe979 | ||
| 
						 | 
					c993790b7a | ||
| 
						 | 
					aa32286531 | ||
| 
						 | 
					6f94abde00 | ||
| 
						 | 
					fa19538c9d | ||
| 
						 | 
					84c858b878 | ||
| 
						 | 
					865de142d4 | ||
| 
						 | 
					9118162553 | ||
| 
						 | 
					f4fc6ee9b4 | ||
| 
						 | 
					108c38d57b | ||
| 
						 | 
					a1d73eb830 | ||
| 
						 | 
					997906a610 | ||
| 
						 | 
					b6e5d120d3 | ||
| 
						 | 
					d469d0b435 | ||
| 
						 | 
					e9f823e000 | ||
| 
						 | 
					d7fb76ba74 | ||
| 
						 | 
					b7dde1a0d9 | ||
| 
						 | 
					15095d8c23 | ||
| 
						 | 
					dfbebc7606 | ||
| 
						 | 
					895309d93d | ||
| 
						 | 
					bcf50e821a | ||
| 
						 | 
					30195800dd | ||
| 
						 | 
					6532b0f149 | ||
| 
						 | 
					5e108e4057 | ||
| 
						 | 
					c2b2f4d222 | ||
| 
						 | 
					bc4329ad21 | ||
| 
						 | 
					aec6d1b2f6 | ||
| 
						 | 
					2baf119299 | ||
| 
						 | 
					6fe4c5a2ed | ||
| 
						 | 
					4abc8e41d8 | ||
| 
						 | 
					af694f1ce9 | ||
| 
						 | 
					7c3a5fcb83 | ||
| 
						 | 
					57f64b18c6 | ||
| 
						 | 
					4cccc7c2f8 | ||
| 
						 | 
					903a2d6a6e | ||
| 
						 | 
					34c674487a | ||
| 
						 | 
					d15a8c5af3 | ||
| 
						 | 
					3e0dec9383 | ||
| 
						 | 
					8b810aad81 | ||
| 
						 | 
					e676bcb4f4 | ||
| 
						 | 
					a7aed77764 | ||
| 
						 | 
					88875c0257 | ||
| 
						 | 
					f711a0c91a | ||
| 
						 | 
					d8a076cc6e | ||
| 
						 | 
					c900831ee9 | ||
| 
						 | 
					76a30c7ef4 | ||
| 
						 | 
					ae5d0b1d81 | ||
| 
						 | 
					cd5e87be34 | ||
| 
						 | 
					3e967f58d2 | ||
| 
						 | 
					1ea005ba7e | ||
| 
						 | 
					092772ba90 | ||
| 
						 | 
					b959854a76 | ||
| 
						 | 
					8ccb1ebe4f | ||
| 
						 | 
					91b3be6467 | ||
| 
						 | 
					d79d5feacc | ||
| 
						 | 
					5cc78ef9d5 | ||
| 
						 | 
					8639cd5a72 | ||
| 
						 | 
					021ddc17e7 | ||
| 
						 | 
					ee47b8d004 | ||
| 
						 | 
					55d267c935 | ||
| 
						 | 
					0fd0b9128d | ||
| 
						 | 
					d9cf505b50 | ||
| 
						 | 
					6079332dda | ||
| 
						 | 
					929ec20365 | ||
| 
						 | 
					d0cad3055f | ||
| 
						 | 
					4974a13bc0 | ||
| 
						 | 
					bd048df225 | ||
| 
						 | 
					ed83cbd574 | ||
| 
						 | 
					7230207853 | ||
| 
						 | 
					1ead8a72ab | ||
| 
						 | 
					36a2e9d931 | ||
| 
						 | 
					0f147a5518 | ||
| 
						 | 
					fce511a18b | ||
| 
						 | 
					64bb61b009 | ||
| 
						 | 
					c6eefec5ce | ||
| 
						 | 
					4c6f829c92 | ||
| 
						 | 
					8c5cdd2acb | ||
| 
						 | 
					e5357599c4 | ||
| 
						 | 
					3800f19966 | ||
| 
						 | 
					7336f84a4b | ||
| 
						 | 
					7bf4a5b2b5 | ||
| 
						 | 
					43a7b97218 | ||
| 
						 | 
					9f95c57a09 | ||
| 
						 | 
					8f6056ae66 | ||
| 
						 | 
					9bcac6b10e | ||
| 
						 | 
					86318e1b7d | ||
| 
						 | 
					a8a1458833 | ||
| 
						 | 
					942c1e2dfe | ||
| 
						 | 
					a6b6814eae | ||
| 
						 | 
					0af95aa9b1 | ||
| 
						 | 
					b4b9256867 | ||
| 
						 | 
					a6f1281a98 | ||
| 
						 | 
					b54480928a | ||
| 
						 | 
					741c74e267 | ||
| 
						 | 
					3061dba5ed | ||
| 
						 | 
					09f5f4027e | ||
| 
						 | 
					925695fd56 | ||
| 
						 | 
					3c758be856 | ||
| 
						 | 
					569b76a7e3 | ||
| 
						 | 
					dca69eff9c | ||
| 
						 | 
					6b8fedc675 | ||
| 
						 | 
					c42a379e7c | ||
| 
						 | 
					a40858adbf | ||
| 
						 | 
					19bc720bc9 | ||
| 
						 | 
					bf79ca30bb | ||
| 
						 | 
					75454895e5 | ||
| 
						 | 
					c81aa2d6fe | ||
| 
						 | 
					376f6369b8 | ||
| 
						 | 
					b1e67a1ed3 | ||
| 
						 | 
					7393a30bd1 | ||
| 
						 | 
					c934065f8e | ||
| 
						 | 
					56124d2b50 | ||
| 
						 | 
					e8a003ff8a | ||
| 
						 | 
					4c789225b2 | ||
| 
						 | 
					59dcdd5393 | ||
| 
						 | 
					b28316a4f2 | ||
| 
						 | 
					4f44671acd | ||
| 
						 | 
					b5eed69712 | ||
| 
						 | 
					b79aacb2a7 | ||
| 
						 | 
					8a2eb7b058 | ||
| 
						 | 
					7316d076a2 | ||
| 
						 | 
					479d3bcb40 | ||
| 
						 | 
					f2358f1530 | ||
| 
						 | 
					47d9e1b966 | ||
| 
						 | 
					c53657d693 | ||
| 
						 | 
					f19ce59e00 | ||
| 
						 | 
					076f3e05d6 | ||
| 
						 | 
					7d017f9494 | ||
| 
						 | 
					675de4e420 | ||
| 
						 | 
					418a709c6c | ||
| 
						 | 
					1d7dd1b754 | ||
| 
						 | 
					3fa70d6d2b | ||
| 
						 | 
					9c67f52161 | ||
| 
						 | 
					9f2f23fa96 | ||
| 
						 | 
					46d955691a | ||
| 
						 | 
					3f8800187d | ||
| 
						 | 
					ebbe90dfa8 | ||
| 
						 | 
					074f898160 | ||
| 
						 | 
					a0e1783e18 | ||
| 
						 | 
					fc83e11d8b | ||
| 
						 | 
					f43627b170 | ||
| 
						 | 
					8964441f44 | ||
| 
						 | 
					cfd7a0c621 | ||
| 
						 | 
					15a422873e | ||
| 
						 | 
					d1f5583cd7 | ||
| 
						 | 
					08f07c6f3e | ||
| 
						 | 
					35a08debc3 | ||
| 
						 | 
					a3424c480f | ||
| 
						 | 
					118ced0a43 | ||
| 
						 | 
					6d355ef0cd | ||
| 
						 | 
					a8aa5ac231 | ||
| 
						 | 
					df6bc0b3c9 | ||
| 
						 | 
					6b965b765c | ||
| 
						 | 
					d7aea6b5ba | ||
| 
						 | 
					1e9a46855d | ||
| 
						 | 
					91e9c18110 | ||
| 
						 | 
					8ffa6088d7 | ||
| 
						 | 
					52d2f8364f | ||
| 
						 | 
					1f679af6fa | ||
| 
						 | 
					1ba92cdcd5 | ||
| 
						 | 
					45c60ba5f5 | ||
| 
						 | 
					d3eef45608 | ||
| 
						 | 
					1960c113d4 | ||
| 
						 | 
					63d6b4a1c9 | ||
| 
						 | 
					9f47bb1252 | ||
| 
						 | 
					df4fea31d0 | ||
| 
						 | 
					98ef1484c8 | ||
| 
						 | 
					c4ef9960b9 | ||
| 
						 | 
					6b6f7744aa | ||
| 
						 | 
					9192fa0fe2 | ||
| 
						 | 
					3c7c2dc1a5 | ||
| 
						 | 
					5c176a1af0 | ||
| 
						 | 
					6d03a1cc76 | ||
| 
						 | 
					1cf10edef1 | ||
| 
						 | 
					6a97c63bf4 | ||
| 
						 | 
					15f9612bfa | ||
| 
						 | 
					9a7c90b194 | ||
| 
						 | 
					91f2708a87 | ||
| 
						 | 
					7bf3ecd89d | ||
| 
						 | 
					4768581631 | ||
| 
						 | 
					aa4cd10e13 | ||
| 
						 | 
					066396916d | ||
| 
						 | 
					34ae57e6fe | ||
| 
						 | 
					107c2b50e2 | ||
| 
						 | 
					a832765203 | ||
| 
						 | 
					977fee82b5 | ||
| 
						 | 
					8c74cbc1c6 | ||
| 
						 | 
					b38eec5039 | ||
| 
						 | 
					6c20b932fa | ||
| 
						 | 
					deb24c638f | ||
| 
						 | 
					40fcdb4d28 | ||
| 
						 | 
					f3e44cf458 | ||
| 
						 | 
					498748217d | ||
| 
						 | 
					483bf331fa | ||
| 
						 | 
					9d62b4acdd | ||
| 
						 | 
					c9deef6e76 | ||
| 
						 | 
					8ba6f8b0e1 | ||
| 
						 | 
					824cbdc84b | ||
| 
						 | 
					448c59ea88 | ||
| 
						 | 
					91b858bf33 | ||
| 
						 | 
					c12bede980 | ||
| 
						 | 
					71e9fa3d16 | ||
| 
						 | 
					6800b9aaae | ||
| 
						 | 
					77d44f25f9 | ||
| 
						 | 
					ab6227828b | ||
| 
						 | 
					719ba56c59 | ||
| 
						 | 
					dacedf4018 | ||
| 
						 | 
					2526fa3c47 | ||
| 
						 | 
					7e2295c382 | ||
| 
						 | 
					6ef02004ff | ||
| 
						 | 
					0e60d062e9 | ||
| 
						 | 
					80a94f97c4 | ||
| 
						 | 
					c18bc5fe67 | ||
| 
						 | 
					02b98a2429 | ||
| 
						 | 
					0383aeaa87 | ||
| 
						 | 
					15a41d532e | ||
| 
						 | 
					0f49725789 | ||
| 
						 | 
					1db6733e66 | ||
| 
						 | 
					0343ee4f6b | ||
| 
						 | 
					2c37d2233a | ||
| 
						 | 
					0cb8ccfddd | ||
| 
						 | 
					41c0e85d00 | ||
| 
						 | 
					35b1a39ed8 | ||
| 
						 | 
					61a577ba70 | ||
| 
						 | 
					a1e32584fa | ||
| 
						 | 
					28e0ee536d | ||
| 
						 | 
					9d64a9c038 | ||
| 
						 | 
					702ba969c2 | ||
| 
						 | 
					6dde8ee2b8 | ||
| 
						 | 
					018420310c | ||
| 
						 | 
					6d49d34033 | ||
| 
						 | 
					1fbd403164 | ||
| 
						 | 
					13f544d2be | ||
| 
						 | 
					3c9e64de81 | ||
| 
						 | 
					5a9bafbc32 | ||
| 
						 | 
					b89d96b66f | ||
| 
						 | 
					b7176191ac | ||
| 
						 | 
					453c5f47c2 | ||
| 
						 | 
					eea62e1263 | ||
| 
						 | 
					4fb2a0f1ca | ||
| 
						 | 
					1d102ef096 | ||
| 
						 | 
					bf3c65778e | ||
| 
						 | 
					df7fe3e6b4 | ||
| 
						 | 
					b657468b62 | ||
| 
						 | 
					4edc0058d3 | ||
| 
						 | 
					2c3b35293b | ||
| 
						 | 
					be0c9a4d46 | ||
| 
						 | 
					dd4140558e | ||
| 
						 | 
					71c2519b8e | ||
| 
						 | 
					badfc26aed | ||
| 
						 | 
					b2bc3adb3d | ||
| 
						 | 
					5ccf408fd6 | ||
| 
						 | 
					da185875bb | ||
| 
						 | 
					af16912541 | ||
| 
						 | 
					1bf9e2a5e6 | ||
| 
						 | 
					5a572651ff | ||
| 
						 | 
					5a191e387f | ||
| 
						 | 
					18f29f5790 | ||
| 
						 | 
					054a73e0f8 | ||
| 
						 | 
					14824db7b0 | ||
| 
						 | 
					721c48ea88 | ||
| 
						 | 
					ed7bfcfb58 | ||
| 
						 | 
					773a40a126 | ||
| 
						 | 
					961252ef26 | ||
| 
						 | 
					a2650f3c47 | ||
| 
						 | 
					d71ee194e1 | ||
| 
						 | 
					22e1a4cf41 | ||
| 
						 | 
					a50bf901d3 | ||
| 
						 | 
					c9469635b5 | ||
| 
						 | 
					36df3278e5 | ||
| 
						 | 
					cb2258aaa8 | ||
| 
						 | 
					0391d9eb7e | ||
| 
						 | 
					12698b4c20 | ||
| 
						 | 
					f7b9d459ab | ||
| 
						 | 
					65ab14e68b | ||
| 
						 | 
					93a5dd5de4 | ||
| 
						 | 
					61807bdaaa | ||
| 
						 | 
					a1a5d1adba | ||
| 
						 | 
					9dd4aefea5 | ||
| 
						 | 
					db4540089a | ||
| 
						 | 
					24c899c91a | ||
| 
						 | 
					ade1a73966 | ||
| 
						 | 
					fb9ec2b040 | ||
| 
						 | 
					3a683812e9 | ||
| 
						 | 
					6d317603c9 | ||
| 
						 | 
					5a3d2d196c | ||
| 
						 | 
					e740c4d980 | ||
| 
						 | 
					253e4596e2 | ||
| 
						 | 
					87d05223af | ||
| 
						 | 
					babf6366e8 | 
@@ -1,11 +1,11 @@
 | 
			
		||||
# pulls community scripts from git repo
 | 
			
		||||
FROM python:3.10.6-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.6-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 /
 | 
			
		||||
 
 | 
			
		||||
@@ -216,6 +216,7 @@ services:
 | 
			
		||||
      - "443:4443"
 | 
			
		||||
    volumes:
 | 
			
		||||
      - tactical-data-dev:/opt/tactical
 | 
			
		||||
      - ..:/workspace:cached
 | 
			
		||||
 | 
			
		||||
volumes:
 | 
			
		||||
  tactical-data-dev: null
 | 
			
		||||
 
 | 
			
		||||
@@ -33,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
 | 
			
		||||
@@ -49,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
 | 
			
		||||
@@ -64,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': {
 | 
			
		||||
@@ -78,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'
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -87,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
 | 
			
		||||
@@ -104,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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -120,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
 | 
			
		||||
@@ -127,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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
								
							@@ -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.
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										17
									
								
								.github/workflows/ci-tests.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										17
									
								
								.github/workflows/ci-tests.yml
									
									
									
									
										vendored
									
									
								
							@@ -14,22 +14,23 @@ jobs:
 | 
			
		||||
    name: Tests
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
        python-version: ["3.10.6"]
 | 
			
		||||
        python-version: ["3.11.8"]
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v3
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
 | 
			
		||||
      - uses: harmon758/postgresql-action@v1
 | 
			
		||||
        with:
 | 
			
		||||
          postgresql version: "14"
 | 
			
		||||
          postgresql version: "15"
 | 
			
		||||
          postgresql db: "pipeline"
 | 
			
		||||
          postgresql user: "pipeline"
 | 
			
		||||
          postgresql password: "pipeline123456"
 | 
			
		||||
 | 
			
		||||
      - name: Setup Python ${{ matrix.python-version }}
 | 
			
		||||
        uses: actions/setup-python@v3
 | 
			
		||||
        uses: actions/setup-python@v4
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: ${{ matrix.python-version }}
 | 
			
		||||
          check-latest: true
 | 
			
		||||
 | 
			
		||||
      - name: Install redis
 | 
			
		||||
        run: |
 | 
			
		||||
@@ -56,6 +57,14 @@ jobs:
 | 
			
		||||
              exit 1
 | 
			
		||||
          fi
 | 
			
		||||
 | 
			
		||||
      - 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"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										70
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										70
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							@@ -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', '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
 | 
			
		||||
							
								
								
									
										20
									
								
								.github/workflows/docker-build-push.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										20
									
								
								.github/workflows/docker-build-push.yml
									
									
									
									
										vendored
									
									
								
							@@ -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:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -56,3 +56,6 @@ daphne.sock.lock
 | 
			
		||||
.pytest_cache
 | 
			
		||||
coverage.xml
 | 
			
		||||
setup_dev.yml
 | 
			
		||||
11env/
 | 
			
		||||
query_schema.json
 | 
			
		||||
gunicorn_config.py
 | 
			
		||||
							
								
								
									
										30
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										30
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							@@ -1,29 +1,14 @@
 | 
			
		||||
{
 | 
			
		||||
  "python.defaultInterpreterPath": "api/tacticalrmm/env/bin/python",
 | 
			
		||||
  "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"
 | 
			
		||||
    "reportGeneralTypeIssues": "none",
 | 
			
		||||
    "reportOptionalMemberAccess": "none",
 | 
			
		||||
  },
 | 
			
		||||
  "python.analysis.typeCheckingMode": "basic",
 | 
			
		||||
  "python.linting.enabled": true,
 | 
			
		||||
  "python.linting.mypyEnabled": true,
 | 
			
		||||
  "python.linting.mypyArgs": [
 | 
			
		||||
    "--ignore-missing-imports",
 | 
			
		||||
    "--follow-imports=silent",
 | 
			
		||||
    "--show-column-numbers",
 | 
			
		||||
    "--strict"
 | 
			
		||||
  ],
 | 
			
		||||
  "python.linting.ignorePatterns": [
 | 
			
		||||
    "**/site-packages/**/*.py",
 | 
			
		||||
    ".vscode/*.py",
 | 
			
		||||
    "**env/**"
 | 
			
		||||
  ],
 | 
			
		||||
  "python.formatting.provider": "black",
 | 
			
		||||
  "mypy.targets": ["api/tacticalrmm"],
 | 
			
		||||
  "mypy.runUsingActiveInterpreter": true,
 | 
			
		||||
  "editor.bracketPairColorization.enabled": true,
 | 
			
		||||
  "editor.guides.bracketPairs": true,
 | 
			
		||||
  "editor.formatOnSave": true,
 | 
			
		||||
@@ -32,7 +17,6 @@
 | 
			
		||||
    "**/docker/**/docker-compose*.yml": "dockercompose"
 | 
			
		||||
  },
 | 
			
		||||
  "files.watcherExclude": {
 | 
			
		||||
    "files.watcherExclude": {
 | 
			
		||||
      "**/.git/objects/**": true,
 | 
			
		||||
      "**/.git/subtree-cache/**": true,
 | 
			
		||||
      "**/node_modules/": true,
 | 
			
		||||
@@ -51,23 +35,25 @@
 | 
			
		||||
      "**/*.parquet*": true,
 | 
			
		||||
      "**/*.pyc": true,
 | 
			
		||||
      "**/*.zip": true
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "go.useLanguageServer": true,
 | 
			
		||||
  "[go]": {
 | 
			
		||||
    "editor.codeActionsOnSave": {
 | 
			
		||||
      "source.organizeImports": false
 | 
			
		||||
      "source.organizeImports": "never"
 | 
			
		||||
    },
 | 
			
		||||
    "editor.snippetSuggestions": "none"
 | 
			
		||||
  },
 | 
			
		||||
  "[go.mod]": {
 | 
			
		||||
    "editor.codeActionsOnSave": {
 | 
			
		||||
      "source.organizeImports": true
 | 
			
		||||
      "source.organizeImports": "explicit"
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "gopls": {
 | 
			
		||||
    "usePlaceholders": true,
 | 
			
		||||
    "completeUnimported": true,
 | 
			
		||||
    "staticcheck": true
 | 
			
		||||
  },
 | 
			
		||||
  "[python]": {
 | 
			
		||||
    "editor.defaultFormatter": "ms-python.black-formatter"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										16
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								README.md
									
									
									
									
									
								
							@@ -8,6 +8,7 @@ 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://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,10 +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 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 (M1, M2)
 | 
			
		||||
 | 
			
		||||
- 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
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
---
 | 
			
		||||
user: "tactical"
 | 
			
		||||
python_ver: "3.10.6"
 | 
			
		||||
go_ver: "1.18.5"
 | 
			
		||||
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"
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,7 @@ http {
 | 
			
		||||
        server_tokens off;
 | 
			
		||||
        tcp_nopush on;
 | 
			
		||||
        types_hash_max_size 2048;
 | 
			
		||||
        server_names_hash_bucket_size 64;
 | 
			
		||||
        server_names_hash_bucket_size 256;
 | 
			
		||||
        include /etc/nginx/mime.types;
 | 
			
		||||
        default_type application/octet-stream;
 | 
			
		||||
        ssl_protocols TLSv1.2 TLSv1.3;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,2 +0,0 @@
 | 
			
		||||
deb https://nginx.org/packages/debian/ bullseye nginx
 | 
			
		||||
deb-src https://nginx.org/packages/debian/ bullseye nginx
 | 
			
		||||
@@ -1,4 +1,13 @@
 | 
			
		||||
---
 | 
			
		||||
- 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
 | 
			
		||||
@@ -32,11 +41,15 @@
 | 
			
		||||
  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-amd64.tar.gz"
 | 
			
		||||
    src: "https://go.dev/dl/go{{ go_ver }}.linux-{{ goarch }}.tar.gz"
 | 
			
		||||
    dest: /usr/local
 | 
			
		||||
    remote_src: yes
 | 
			
		||||
 | 
			
		||||
@@ -102,7 +115,7 @@
 | 
			
		||||
  tags: postgres
 | 
			
		||||
  become: yes
 | 
			
		||||
  ansible.builtin.copy:
 | 
			
		||||
    content: "deb http://apt.postgresql.org/pub/repos/apt bullseye-pgdg main"
 | 
			
		||||
    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
 | 
			
		||||
@@ -119,7 +132,7 @@
 | 
			
		||||
  tags: postgres
 | 
			
		||||
  become: yes
 | 
			
		||||
  ansible.builtin.apt:
 | 
			
		||||
    pkg: postgresql-14
 | 
			
		||||
    pkg: postgresql-15
 | 
			
		||||
    state: present
 | 
			
		||||
    update_cache: yes
 | 
			
		||||
 | 
			
		||||
@@ -131,7 +144,7 @@
 | 
			
		||||
    enabled: yes
 | 
			
		||||
    state: started
 | 
			
		||||
 | 
			
		||||
- name: setup database
 | 
			
		||||
- name: setup trmm database
 | 
			
		||||
  tags: postgres
 | 
			
		||||
  become: yes
 | 
			
		||||
  become_user: postgres
 | 
			
		||||
@@ -144,6 +157,23 @@
 | 
			
		||||
      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
 | 
			
		||||
@@ -193,7 +223,7 @@
 | 
			
		||||
- 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-amd64.tar.gz"
 | 
			
		||||
    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
 | 
			
		||||
 | 
			
		||||
@@ -202,7 +232,7 @@
 | 
			
		||||
  become: yes
 | 
			
		||||
  ansible.builtin.copy:
 | 
			
		||||
    remote_src: yes
 | 
			
		||||
    src: "{{ nats_tmp.path }}/nats-server-v{{ nats_server_ver.stdout }}-linux-amd64/nats-server"
 | 
			
		||||
    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 }}"
 | 
			
		||||
@@ -218,7 +248,7 @@
 | 
			
		||||
- name: download nodejs setup
 | 
			
		||||
  tags: nodejs
 | 
			
		||||
  ansible.builtin.get_url:
 | 
			
		||||
    url: https://deb.nodesource.com/setup_16.x
 | 
			
		||||
    url: https://deb.nodesource.com/setup_18.x
 | 
			
		||||
    dest: "{{ nodejs_tmp.path }}/setup_node.sh"
 | 
			
		||||
    mode: "0755"
 | 
			
		||||
 | 
			
		||||
@@ -299,14 +329,14 @@
 | 
			
		||||
  tags: nginx
 | 
			
		||||
  become: yes
 | 
			
		||||
  ansible.builtin.apt_key:
 | 
			
		||||
    url: https://nginx.org/packages/keys/nginx_signing.key
 | 
			
		||||
    url: https://nginx.org/keys/nginx_signing.key
 | 
			
		||||
    state: present
 | 
			
		||||
 | 
			
		||||
- name: add nginx repo
 | 
			
		||||
  tags: nginx
 | 
			
		||||
  become: yes
 | 
			
		||||
  ansible.builtin.copy:
 | 
			
		||||
    src: nginx.repo
 | 
			
		||||
  ansible.builtin.template:
 | 
			
		||||
    src: nginx.repo.j2
 | 
			
		||||
    dest: /etc/apt/sources.list.d/nginx.list
 | 
			
		||||
    owner: "root"
 | 
			
		||||
    group: "root"
 | 
			
		||||
@@ -382,12 +412,16 @@
 | 
			
		||||
    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/nats-api"
 | 
			
		||||
    src: "{{ backend_dir }}/natsapi/bin/{{ natsapi }}"
 | 
			
		||||
    dest: /usr/local/bin/nats-api
 | 
			
		||||
    owner: "{{ user }}"
 | 
			
		||||
    group: "{{ user }}"
 | 
			
		||||
@@ -407,7 +441,7 @@
 | 
			
		||||
  tags: pip
 | 
			
		||||
  ansible.builtin.shell:
 | 
			
		||||
    chdir: "{{ backend_dir }}/api"
 | 
			
		||||
    cmd: python3.10 -m venv env
 | 
			
		||||
    cmd: python3.11 -m venv env
 | 
			
		||||
 | 
			
		||||
- name: update pip to latest
 | 
			
		||||
  tags: pip
 | 
			
		||||
@@ -473,39 +507,6 @@
 | 
			
		||||
    - { src: nats-server.systemd.j2, dest: /etc/systemd/system/nats.service }
 | 
			
		||||
    - { src: mesh.systemd.j2, dest: /etc/systemd/system/meshcentral.service }
 | 
			
		||||
 | 
			
		||||
- name: import mongodb repo signing key
 | 
			
		||||
  tags: mongo
 | 
			
		||||
  become: yes
 | 
			
		||||
  ansible.builtin.apt_key:
 | 
			
		||||
    url: https://www.mongodb.org/static/pgp/server-4.4.asc
 | 
			
		||||
    state: present
 | 
			
		||||
 | 
			
		||||
- name: setup mongodb repo
 | 
			
		||||
  tags: mongo
 | 
			
		||||
  become: yes
 | 
			
		||||
  ansible.builtin.copy:
 | 
			
		||||
    content: "deb https://repo.mongodb.org/apt/debian buster/mongodb-org/4.4 main"
 | 
			
		||||
    dest: /etc/apt/sources.list.d/mongodb-org-4.4.list
 | 
			
		||||
    owner: root
 | 
			
		||||
    group: root
 | 
			
		||||
    mode: "0644"
 | 
			
		||||
 | 
			
		||||
- name: install mongodb
 | 
			
		||||
  tags: mongo
 | 
			
		||||
  become: yes
 | 
			
		||||
  ansible.builtin.apt:
 | 
			
		||||
    pkg: mongodb-org
 | 
			
		||||
    state: present
 | 
			
		||||
    update_cache: yes
 | 
			
		||||
 | 
			
		||||
- name: ensure mongodb enabled and started
 | 
			
		||||
  tags: mongo
 | 
			
		||||
  become: yes
 | 
			
		||||
  ansible.builtin.service:
 | 
			
		||||
    name: mongod
 | 
			
		||||
    enabled: yes
 | 
			
		||||
    state: started
 | 
			
		||||
 | 
			
		||||
- name: get mesh_ver
 | 
			
		||||
  tags: mesh
 | 
			
		||||
  ansible.builtin.shell: grep "^MESH_VER" {{ settings_file }} | awk -F'[= "]' '{print $5}'
 | 
			
		||||
 
 | 
			
		||||
@@ -2,10 +2,6 @@ SECRET_KEY = "{{ django_secret }}"
 | 
			
		||||
DEBUG = True
 | 
			
		||||
ALLOWED_HOSTS = ['{{ api }}']
 | 
			
		||||
ADMIN_URL = "admin/"
 | 
			
		||||
CORS_ORIGIN_WHITELIST = [
 | 
			
		||||
    "http://{{ rmm }}:8080",
 | 
			
		||||
    "https://{{ rmm }}:8080",
 | 
			
		||||
]
 | 
			
		||||
CORS_ORIGIN_ALLOW_ALL = True
 | 
			
		||||
DATABASES = {
 | 
			
		||||
    'default': {
 | 
			
		||||
@@ -17,9 +13,8 @@ DATABASES = {
 | 
			
		||||
        'PORT': '5432',
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
REDIS_HOST    = "localhost"
 | 
			
		||||
ADMIN_ENABLED = True
 | 
			
		||||
CERT_FILE = "{{ fullchain_src }}"
 | 
			
		||||
KEY_FILE = "{{ privkey_src }}"
 | 
			
		||||
CERT_FILE = "{{ fullchain_dest }}"
 | 
			
		||||
KEY_FILE = "{{ privkey_dest }}"
 | 
			
		||||
MESH_USERNAME = "{{ mesh_user }}"
 | 
			
		||||
MESH_SITE = "https://{{ mesh }}"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "settings": {
 | 
			
		||||
    "Cert": "{{ mesh }}",
 | 
			
		||||
    "MongoDb": "mongodb://127.0.0.1:27017",
 | 
			
		||||
    "MongoDbName": "meshcentral",
 | 
			
		||||
    "WANonly": true,
 | 
			
		||||
    "Minify": 1,
 | 
			
		||||
    "Port": 4430,
 | 
			
		||||
@@ -10,19 +8,25 @@
 | 
			
		||||
    "RedirPort": 800,
 | 
			
		||||
    "AllowLoginToken": true,
 | 
			
		||||
    "AllowFraming": true,
 | 
			
		||||
    "AgentPong": 300,
 | 
			
		||||
    "AgentPing": 35,
 | 
			
		||||
    "AllowHighQualityDesktop": true,
 | 
			
		||||
    "TlsOffload": "127.0.0.1",
 | 
			
		||||
    "agentCoreDump": false,
 | 
			
		||||
    "Compression": true,
 | 
			
		||||
    "WsCompression": true,
 | 
			
		||||
    "AgentWsCompression": true,
 | 
			
		||||
    "MaxInvalidLogin": { "time": 5, "count": 5, "coolofftime": 30 }
 | 
			
		||||
    "MaxInvalidLogin": { "time": 5, "count": 5, "coolofftime": 30 },
 | 
			
		||||
    "postgres": {
 | 
			
		||||
      "user": "{{ mesh_db_user }}",
 | 
			
		||||
      "password": "{{ mesh_db_passwd }}",
 | 
			
		||||
      "port": "5432",
 | 
			
		||||
      "host": "localhost"
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "domains": {
 | 
			
		||||
    "": {
 | 
			
		||||
      "Title": "Tactical RMM",
 | 
			
		||||
      "Title2": "Tactical RMM",
 | 
			
		||||
      "Title": "Tactical RMM Dev",
 | 
			
		||||
      "Title2": "Tactical RMM Dev",
 | 
			
		||||
      "NewAccounts": false,
 | 
			
		||||
      "CertUrl": "https://{{ mesh }}:443/",
 | 
			
		||||
      "GeoLocation": true,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
[Unit]
 | 
			
		||||
Description=MeshCentral Server
 | 
			
		||||
After=network.target mongod.service nginx.service
 | 
			
		||||
After=network.target postgresql.service nginx.service
 | 
			
		||||
 | 
			
		||||
[Service]
 | 
			
		||||
Type=simple
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								ansible/roles/trmm_dev/templates/nginx.repo.j2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								ansible/roles/trmm_dev/templates/nginx.repo.j2
									
									
									
									
									
										Normal 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
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
DEV_URL = "http://{{ api }}:8000"
 | 
			
		||||
DEV_HOST = "{{ rmm }}"
 | 
			
		||||
DEV_HOST = "0.0.0.0"
 | 
			
		||||
DEV_PORT = "8080"
 | 
			
		||||
USE_HTTPS = false
 | 
			
		||||
@@ -13,6 +13,8 @@
 | 
			
		||||
    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"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										12
									
								
								api/tacticalrmm/.flake8
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								api/tacticalrmm/.flake8
									
									
									
									
									
										Normal 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/*
 | 
			
		||||
@@ -3,6 +3,7 @@ import uuid
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
 | 
			
		||||
from accounts.models import User
 | 
			
		||||
from tacticalrmm.helpers import make_random_password
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
@@ -17,7 +18,7 @@ class Command(BaseCommand):
 | 
			
		||||
        User.objects.create_user(
 | 
			
		||||
            username=uuid.uuid4().hex,
 | 
			
		||||
            is_installer_user=True,
 | 
			
		||||
            password=User.objects.make_random_password(60),
 | 
			
		||||
            password=make_random_password(len=60),
 | 
			
		||||
            block_dashboard_login=True,
 | 
			
		||||
        )
 | 
			
		||||
        self.stdout.write("Installer user has been created")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,11 @@
 | 
			
		||||
import os
 | 
			
		||||
import subprocess
 | 
			
		||||
 | 
			
		||||
import pyotp
 | 
			
		||||
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):
 | 
			
		||||
@@ -21,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")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,5 @@
 | 
			
		||||
from getpass import getpass
 | 
			
		||||
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
 | 
			
		||||
from accounts.models import User
 | 
			
		||||
@@ -17,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!"))
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -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),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -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),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -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),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -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",
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -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),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -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),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
from typing import Optional
 | 
			
		||||
 | 
			
		||||
from allauth.socialaccount.models import SocialAccount
 | 
			
		||||
from django.contrib.auth.models import AbstractUser
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from django.db import models
 | 
			
		||||
@@ -31,7 +32,7 @@ class User(AbstractUser, BaseAuditModel):
 | 
			
		||||
        on_delete=models.SET_NULL,
 | 
			
		||||
    )
 | 
			
		||||
    default_agent_tbl_tab = models.CharField(
 | 
			
		||||
        max_length=50, choices=AgentTableTabs.choices, default=AgentTableTabs.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(
 | 
			
		||||
@@ -39,6 +40,10 @@ class User(AbstractUser, BaseAuditModel):
 | 
			
		||||
    )
 | 
			
		||||
    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)
 | 
			
		||||
@@ -60,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
 | 
			
		||||
@@ -91,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)
 | 
			
		||||
@@ -105,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)
 | 
			
		||||
@@ -116,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)
 | 
			
		||||
@@ -181,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):
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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:
 | 
			
		||||
 
 | 
			
		||||
@@ -11,19 +11,20 @@ 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")
 | 
			
		||||
@@ -40,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
 | 
			
		||||
@@ -50,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"}
 | 
			
		||||
@@ -197,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)
 | 
			
		||||
 | 
			
		||||
@@ -297,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()
 | 
			
		||||
@@ -383,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):
 | 
			
		||||
 
 | 
			
		||||
@@ -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()),
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,10 @@
 | 
			
		||||
from typing import TYPE_CHECKING
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
    from django.http import HttpRequest
 | 
			
		||||
 | 
			
		||||
    from accounts.models import User
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -16,3 +18,7 @@ def is_root_user(*, request: "HttpRequest", user: "User") -> bool:
 | 
			
		||||
        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")
 | 
			
		||||
 
 | 
			
		||||
@@ -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 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 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,
 | 
			
		||||
@@ -22,15 +41,16 @@ from .serializers import (
 | 
			
		||||
    UserSerializer,
 | 
			
		||||
    UserUISerializer,
 | 
			
		||||
)
 | 
			
		||||
from accounts.utils import is_root_user
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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():
 | 
			
		||||
@@ -41,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):
 | 
			
		||||
@@ -68,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)
 | 
			
		||||
 | 
			
		||||
@@ -82,14 +114,20 @@ class LoginView(KnoxLoginView):
 | 
			
		||||
            login(request, user)
 | 
			
		||||
 | 
			
		||||
            # save ip information
 | 
			
		||||
            client_ip, _ = 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}
 | 
			
		||||
@@ -97,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)
 | 
			
		||||
 | 
			
		||||
@@ -110,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
 | 
			
		||||
@@ -134,6 +263,7 @@ class GetAddUsers(APIView):
 | 
			
		||||
            user.role = role
 | 
			
		||||
 | 
			
		||||
        user.save()
 | 
			
		||||
        sync_mesh_perms_task.delay()
 | 
			
		||||
        return Response(user.username)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -154,6 +284,7 @@ class GetUpdateDeleteUser(APIView):
 | 
			
		||||
        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")
 | 
			
		||||
 | 
			
		||||
@@ -163,12 +294,13 @@ class GetUpdateDeleteUser(APIView):
 | 
			
		||||
            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"])
 | 
			
		||||
@@ -195,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()
 | 
			
		||||
@@ -206,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):
 | 
			
		||||
@@ -245,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")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -267,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")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -290,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.")
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,6 @@ 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):
 | 
			
		||||
@@ -48,7 +47,7 @@ class SendCMD(AsyncJsonWebsocketConsumer):
 | 
			
		||||
        await self.send_json({"ret": ret})
 | 
			
		||||
 | 
			
		||||
    async def disconnect(self, _):
 | 
			
		||||
        await self.close()
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def _has_perm(self, perm: str) -> bool:
 | 
			
		||||
        if self.user.is_superuser or (
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ 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
 | 
			
		||||
 
 | 
			
		||||
@@ -5,14 +5,13 @@ 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, handle_resolved_stuff
 | 
			
		||||
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()
 | 
			
		||||
 | 
			
		||||
@@ -30,4 +29,3 @@ class Command(BaseCommand):
 | 
			
		||||
            agent.save(update_fields=["last_seen"])
 | 
			
		||||
 | 
			
		||||
        cache_db_fields_task()
 | 
			
		||||
        handle_resolved_stuff()
 | 
			
		||||
 
 | 
			
		||||
@@ -27,6 +27,7 @@ from tacticalrmm.constants import (
 | 
			
		||||
    EvtLogFailWhen,
 | 
			
		||||
    EvtLogNames,
 | 
			
		||||
    EvtLogTypes,
 | 
			
		||||
    GoArch,
 | 
			
		||||
    PAAction,
 | 
			
		||||
    ScriptShell,
 | 
			
		||||
    TaskSyncStatus,
 | 
			
		||||
@@ -47,10 +48,12 @@ from tacticalrmm.demo_data import (
 | 
			
		||||
    temp_dir_stdout,
 | 
			
		||||
    wmi_deb,
 | 
			
		||||
    wmi_pi,
 | 
			
		||||
    wmi_mac,
 | 
			
		||||
    disks_mac,
 | 
			
		||||
)
 | 
			
		||||
from winupdate.models import WinUpdate, WinUpdatePolicy
 | 
			
		||||
 | 
			
		||||
AGENTS_TO_GENERATE = 20
 | 
			
		||||
AGENTS_TO_GENERATE = 250
 | 
			
		||||
 | 
			
		||||
SVCS = settings.BASE_DIR.joinpath("tacticalrmm/test_data/winsvcs.json")
 | 
			
		||||
WMI_1 = settings.BASE_DIR.joinpath("tacticalrmm/test_data/wmi1.json")
 | 
			
		||||
@@ -72,7 +75,6 @@ class Command(BaseCommand):
 | 
			
		||||
        return "".join(random.choice(chars) for _ in range(length))
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args, **kwargs) -> None:
 | 
			
		||||
 | 
			
		||||
        user = User.objects.first()
 | 
			
		||||
        if user:
 | 
			
		||||
            user.totp_key = "ABSA234234"
 | 
			
		||||
@@ -177,6 +179,8 @@ class Command(BaseCommand):
 | 
			
		||||
            "WSUS",
 | 
			
		||||
            "DESKTOP-12345",
 | 
			
		||||
            "LAPTOP-55443",
 | 
			
		||||
            "db-aws-01",
 | 
			
		||||
            "Karens-MacBook-Air.local",
 | 
			
		||||
        )
 | 
			
		||||
        descriptions = ("Bob's computer", "Primary DC", "File Server", "Karen's Laptop")
 | 
			
		||||
        modes = AgentMonType.values
 | 
			
		||||
@@ -194,6 +198,7 @@ class Command(BaseCommand):
 | 
			
		||||
 | 
			
		||||
        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"
 | 
			
		||||
 | 
			
		||||
        public_ips = ("65.234.22.4", "74.123.43.5", "44.21.134.45")
 | 
			
		||||
 | 
			
		||||
@@ -289,7 +294,6 @@ class Command(BaseCommand):
 | 
			
		||||
        show_tmp_dir_script.save()
 | 
			
		||||
 | 
			
		||||
        for count_agents in range(AGENTS_TO_GENERATE):
 | 
			
		||||
 | 
			
		||||
            client = random.choice(clients)
 | 
			
		||||
 | 
			
		||||
            if client == clients[0]:
 | 
			
		||||
@@ -313,18 +317,25 @@ class Command(BaseCommand):
 | 
			
		||||
                mode = AgentMonType.SERVER
 | 
			
		||||
                # pi arm
 | 
			
		||||
                if plat_pick == 7:
 | 
			
		||||
                    agent.goarch = "arm"
 | 
			
		||||
                    agent.goarch = GoArch.ARM32
 | 
			
		||||
                    agent.wmi_detail = wmi_pi
 | 
			
		||||
                    agent.disks = disks_linux_pi
 | 
			
		||||
                    agent.operating_system = linux_pi_os
 | 
			
		||||
                else:
 | 
			
		||||
                    agent.goarch = "amd64"
 | 
			
		||||
                    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.plat = AgentPlat.WINDOWS
 | 
			
		||||
                agent.goarch = "amd64"
 | 
			
		||||
                agent.goarch = GoArch.AMD64
 | 
			
		||||
                mode = random.choice(modes)
 | 
			
		||||
                agent.wmi_detail = random.choice(wmi_details)
 | 
			
		||||
                agent.services = services
 | 
			
		||||
@@ -334,8 +345,8 @@ class Command(BaseCommand):
 | 
			
		||||
                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(40)
 | 
			
		||||
            agent.description = random.choice(descriptions)
 | 
			
		||||
@@ -810,7 +821,6 @@ class Command(BaseCommand):
 | 
			
		||||
                pick = random.randint(1, 10)
 | 
			
		||||
 | 
			
		||||
                if pick == 5 or pick == 3:
 | 
			
		||||
 | 
			
		||||
                    reboot_time = django_now + djangotime.timedelta(
 | 
			
		||||
                        minutes=random.randint(1000, 500000)
 | 
			
		||||
                    )
 | 
			
		||||
 
 | 
			
		||||
@@ -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()
 | 
			
		||||
							
								
								
									
										631
									
								
								api/tacticalrmm/agents/migrations/0056_alter_agent_time_zone.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										631
									
								
								api/tacticalrmm/agents/migrations/0056_alter_agent_time_zone.py
									
									
									
									
									
										Normal 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,
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -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")},
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										633
									
								
								api/tacticalrmm/agents/migrations/0058_alter_agent_time_zone.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										633
									
								
								api/tacticalrmm/agents/migrations/0058_alter_agent_time_zone.py
									
									
									
									
									
										Normal 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,
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -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),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -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),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -1,13 +1,13 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
import logging
 | 
			
		||||
import re
 | 
			
		||||
from collections import Counter
 | 
			
		||||
from distutils.version import LooseVersion
 | 
			
		||||
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 django.conf import settings
 | 
			
		||||
from django.contrib.postgres.fields import ArrayField
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
@@ -15,15 +15,18 @@ from django.db import models
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from nats.errors import TimeoutError
 | 
			
		||||
from packaging import version as pyver
 | 
			
		||||
from packaging.version import Version as LooseVersion
 | 
			
		||||
 | 
			
		||||
from agents.utils import get_agent_url
 | 
			
		||||
from checks.models import CheckResult
 | 
			
		||||
from core.models import TZ_CHOICES
 | 
			
		||||
from core.utils import get_core_settings, send_command_with_mesh
 | 
			
		||||
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,
 | 
			
		||||
@@ -37,7 +40,7 @@ from tacticalrmm.constants import (
 | 
			
		||||
    PAAction,
 | 
			
		||||
    PAStatus,
 | 
			
		||||
)
 | 
			
		||||
from tacticalrmm.helpers import get_nats_ports
 | 
			
		||||
from tacticalrmm.helpers import has_script_actions, has_webhook, setup_nats_options
 | 
			
		||||
from tacticalrmm.models import PermissionQuerySet
 | 
			
		||||
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
@@ -51,6 +54,8 @@ if TYPE_CHECKING:
 | 
			
		||||
# type helpers
 | 
			
		||||
Disk = Union[Dict[str, Any], str]
 | 
			
		||||
 | 
			
		||||
logger = logging.getLogger("trmm")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Agent(BaseAuditModel):
 | 
			
		||||
    class Meta:
 | 
			
		||||
@@ -121,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
 | 
			
		||||
@@ -130,8 +156,8 @@ 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:
 | 
			
		||||
@@ -198,8 +224,9 @@ class Agent(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
    @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):
 | 
			
		||||
@@ -213,8 +240,6 @@ class Agent(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
    @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):
 | 
			
		||||
@@ -232,12 +257,12 @@ class Agent(BaseAuditModel):
 | 
			
		||||
                alert_severity = (
 | 
			
		||||
                    check.check_result.alert_severity
 | 
			
		||||
                    if check.check_type
 | 
			
		||||
                    in [
 | 
			
		||||
                    in (
 | 
			
		||||
                        CheckType.MEMORY,
 | 
			
		||||
                        CheckType.CPU_LOAD,
 | 
			
		||||
                        CheckType.DISK_SPACE,
 | 
			
		||||
                        CheckType.SCRIPT,
 | 
			
		||||
                    ]
 | 
			
		||||
                    )
 | 
			
		||||
                    else check.alert_severity
 | 
			
		||||
                )
 | 
			
		||||
                if alert_severity == AlertSeverity.ERROR:
 | 
			
		||||
@@ -257,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:
 | 
			
		||||
@@ -269,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"]
 | 
			
		||||
@@ -333,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:
 | 
			
		||||
@@ -344,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]
 | 
			
		||||
@@ -361,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"
 | 
			
		||||
 | 
			
		||||
@@ -401,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:
 | 
			
		||||
@@ -423,7 +483,6 @@ class Agent(BaseAuditModel):
 | 
			
		||||
    def get_checks_with_policies(
 | 
			
		||||
        self, exclude_overridden: bool = False
 | 
			
		||||
    ) -> "List[Check]":
 | 
			
		||||
 | 
			
		||||
        if exclude_overridden:
 | 
			
		||||
            checks = (
 | 
			
		||||
                list(
 | 
			
		||||
@@ -438,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:
 | 
			
		||||
@@ -455,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:
 | 
			
		||||
@@ -479,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",
 | 
			
		||||
@@ -492,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:
 | 
			
		||||
@@ -517,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
 | 
			
		||||
 | 
			
		||||
@@ -533,8 +596,8 @@ class Agent(BaseAuditModel):
 | 
			
		||||
        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)
 | 
			
		||||
@@ -544,6 +607,7 @@ class Agent(BaseAuditModel):
 | 
			
		||||
            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",
 | 
			
		||||
@@ -554,6 +618,9 @@ class Agent(BaseAuditModel):
 | 
			
		||||
                "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:
 | 
			
		||||
@@ -589,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")
 | 
			
		||||
 | 
			
		||||
@@ -621,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:
 | 
			
		||||
@@ -683,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 (
 | 
			
		||||
@@ -787,23 +851,12 @@ class Agent(BaseAuditModel):
 | 
			
		||||
            cache.set(cache_key, tasks, 600)
 | 
			
		||||
            return tasks
 | 
			
		||||
 | 
			
		||||
    def _do_nats_debug(self, agent: "Agent", message: str) -> None:
 | 
			
		||||
        DebugLog.error(agent=agent, log_type=DebugLogType.AGENT_ISSUES, message=message)
 | 
			
		||||
 | 
			
		||||
    async def nats_cmd(
 | 
			
		||||
        self, data: Dict[Any, Any], timeout: int = 30, wait: bool = True
 | 
			
		||||
    ) -> Any:
 | 
			
		||||
        nats_std_port, _ = get_nats_ports()
 | 
			
		||||
        options = {
 | 
			
		||||
            "servers": f"tls://{settings.ALLOWED_HOSTS[0]}:{nats_std_port}",
 | 
			
		||||
            "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"
 | 
			
		||||
 | 
			
		||||
@@ -819,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
 | 
			
		||||
@@ -873,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)
 | 
			
		||||
@@ -884,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)
 | 
			
		||||
@@ -898,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:
 | 
			
		||||
@@ -1009,6 +1064,9 @@ class AgentCustomField(models.Model):
 | 
			
		||||
        default=list,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        unique_together = (("agent", "field"),)
 | 
			
		||||
 | 
			
		||||
    def __str__(self) -> str:
 | 
			
		||||
        return self.field.name
 | 
			
		||||
 | 
			
		||||
@@ -1018,16 +1076,16 @@ class AgentCustomField(models.Model):
 | 
			
		||||
            return cast(List[str], self.multiple_value)
 | 
			
		||||
        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 [
 | 
			
		||||
        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 == CustomFieldType.MULTIPLE:
 | 
			
		||||
@@ -1041,6 +1099,7 @@ class AgentCustomField(models.Model):
 | 
			
		||||
class AgentHistory(models.Model):
 | 
			
		||||
    objects = PermissionQuerySet.as_manager()
 | 
			
		||||
 | 
			
		||||
    id = models.BigAutoField(primary_key=True)
 | 
			
		||||
    agent = models.ForeignKey(
 | 
			
		||||
        Agent,
 | 
			
		||||
        related_name="history",
 | 
			
		||||
@@ -1063,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}"
 | 
			
		||||
 
 | 
			
		||||
@@ -47,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(
 | 
			
		||||
@@ -96,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(
 | 
			
		||||
@@ -122,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")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
import pytz
 | 
			
		||||
from rest_framework import serializers
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.constants import AGENT_STATUS_ONLINE
 | 
			
		||||
from tacticalrmm.constants import AGENT_STATUS_ONLINE, ALL_TIMEZONES
 | 
			
		||||
from winupdate.serializers import WinUpdatePolicySerializer
 | 
			
		||||
 | 
			
		||||
from .models import Agent, AgentCustomField, AgentHistory, Note
 | 
			
		||||
@@ -71,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
 | 
			
		||||
@@ -95,26 +94,27 @@ class AgentTableSerializer(serializers.ModelSerializer):
 | 
			
		||||
    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 == 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 == AGENT_STATUS_ONLINE
 | 
			
		||||
@@ -154,6 +154,8 @@ class AgentTableSerializer(serializers.ModelSerializer):
 | 
			
		||||
            "local_ips",
 | 
			
		||||
            "make_model",
 | 
			
		||||
            "physical_disks",
 | 
			
		||||
            "custom_fields",
 | 
			
		||||
            "serial_number",
 | 
			
		||||
        ]
 | 
			
		||||
        depth = 2
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
import datetime as dt
 | 
			
		||||
import random
 | 
			
		||||
from time import sleep
 | 
			
		||||
from typing import TYPE_CHECKING, Optional
 | 
			
		||||
 | 
			
		||||
@@ -13,10 +12,13 @@ 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
 | 
			
		||||
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
    from django.db.models.query import QuerySet
 | 
			
		||||
@@ -46,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"])
 | 
			
		||||
@@ -55,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"])
 | 
			
		||||
@@ -67,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)
 | 
			
		||||
@@ -91,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"])
 | 
			
		||||
@@ -100,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"])
 | 
			
		||||
@@ -112,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:
 | 
			
		||||
@@ -125,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 == 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
 | 
			
		||||
@@ -154,6 +152,7 @@ def run_script_email_results_task(
 | 
			
		||||
    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)
 | 
			
		||||
@@ -165,6 +164,7 @@ def run_script_email_results_task(
 | 
			
		||||
        wait=True,
 | 
			
		||||
        history_pk=history_pk,
 | 
			
		||||
        run_as_user=run_as_user,
 | 
			
		||||
        env_vars=env_vars,
 | 
			
		||||
    )
 | 
			
		||||
    if r == "timeout":
 | 
			
		||||
        DebugLog.error(
 | 
			
		||||
@@ -175,7 +175,7 @@ def run_script_email_results_task(
 | 
			
		||||
        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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										61
									
								
								api/tacticalrmm/agents/tests/test_agent_save.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								api/tacticalrmm/agents/tests/test_agent_save.py
									
									
									
									
									
										Normal 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()
 | 
			
		||||
@@ -264,7 +264,7 @@ class TestAgentUpdate(TacticalTestCase):
 | 
			
		||||
        agents = baker.make_recipe("agents.agent", _quantity=5)
 | 
			
		||||
        other_agents = baker.make_recipe("agents.agent", _quantity=7)
 | 
			
		||||
 | 
			
		||||
        url = f"/agents/update/"
 | 
			
		||||
        url = "/agents/update/"
 | 
			
		||||
 | 
			
		||||
        data = {
 | 
			
		||||
            "agent_ids": [agent.agent_id for agent in agents]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
from unittest.mock import patch, AsyncMock
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
 | 
			
		||||
from agents.utils import generate_linux_install, get_agent_url
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
 
 | 
			
		||||
@@ -2,9 +2,9 @@ import json
 | 
			
		||||
import os
 | 
			
		||||
from itertools import cycle
 | 
			
		||||
from typing import TYPE_CHECKING
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
from unittest.mock import PropertyMock, patch
 | 
			
		||||
from zoneinfo import ZoneInfo
 | 
			
		||||
 | 
			
		||||
import pytz
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from model_bakery import baker
 | 
			
		||||
@@ -540,6 +540,7 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
            "args": [],
 | 
			
		||||
            "timeout": 15,
 | 
			
		||||
            "run_as_user": False,
 | 
			
		||||
            "env_vars": ["hello=world", "foo=bar"],
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
@@ -555,6 +556,7 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
            wait=True,
 | 
			
		||||
            history_pk=hist.pk,
 | 
			
		||||
            run_as_user=False,
 | 
			
		||||
            env_vars=["hello=world", "foo=bar"],
 | 
			
		||||
        )
 | 
			
		||||
        run_script.reset_mock()
 | 
			
		||||
 | 
			
		||||
@@ -567,16 +569,20 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
            "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()
 | 
			
		||||
 | 
			
		||||
@@ -584,13 +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
 | 
			
		||||
@@ -600,6 +609,7 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
            "args": ["hello", "world"],
 | 
			
		||||
            "timeout": 22,
 | 
			
		||||
            "run_as_user": True,
 | 
			
		||||
            "env_vars": ["hello=world", "foo=bar"],
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
@@ -614,6 +624,7 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
            timeout=25,
 | 
			
		||||
            history_pk=hist.pk,
 | 
			
		||||
            run_as_user=True,
 | 
			
		||||
            env_vars=["hello=world", "foo=bar"],
 | 
			
		||||
        )
 | 
			
		||||
        run_script.reset_mock()
 | 
			
		||||
 | 
			
		||||
@@ -629,6 +640,7 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
            "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")
 | 
			
		||||
@@ -644,6 +656,7 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
            wait=True,
 | 
			
		||||
            history_pk=hist.pk,
 | 
			
		||||
            run_as_user=False,
 | 
			
		||||
            env_vars=["hello=world", "foo=bar"],
 | 
			
		||||
        )
 | 
			
		||||
        run_script.reset_mock()
 | 
			
		||||
 | 
			
		||||
@@ -662,6 +675,7 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
            "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")
 | 
			
		||||
@@ -677,6 +691,7 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
            wait=True,
 | 
			
		||||
            history_pk=hist.pk,
 | 
			
		||||
            run_as_user=False,
 | 
			
		||||
            env_vars=["hello=world", "foo=bar"],
 | 
			
		||||
        )
 | 
			
		||||
        run_script.reset_mock()
 | 
			
		||||
 | 
			
		||||
@@ -697,6 +712,7 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
            "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")
 | 
			
		||||
@@ -712,6 +728,7 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
            wait=True,
 | 
			
		||||
            history_pk=hist.pk,
 | 
			
		||||
            run_as_user=False,
 | 
			
		||||
            env_vars=["hello=world", "foo=bar"],
 | 
			
		||||
        )
 | 
			
		||||
        run_script.reset_mock()
 | 
			
		||||
 | 
			
		||||
@@ -729,6 +746,7 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
            "args": ["hello", "world"],
 | 
			
		||||
            "timeout": 22,
 | 
			
		||||
            "run_as_user": False,
 | 
			
		||||
            "env_vars": ["hello=world", "foo=bar"],
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
@@ -744,11 +762,73 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
            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/"
 | 
			
		||||
 | 
			
		||||
@@ -836,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)
 | 
			
		||||
@@ -848,7 +927,7 @@ 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
 | 
			
		||||
@@ -992,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")
 | 
			
		||||
 | 
			
		||||
@@ -1003,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"},
 | 
			
		||||
@@ -1120,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")
 | 
			
		||||
@@ -1183,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)
 | 
			
		||||
 | 
			
		||||
@@ -1272,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
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -15,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()),
 | 
			
		||||
@@ -42,4 +43,6 @@ urlpatterns = [
 | 
			
		||||
    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),
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
import tempfile
 | 
			
		||||
import urllib.parse
 | 
			
		||||
from io import StringIO
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.http import FileResponse
 | 
			
		||||
@@ -33,7 +34,6 @@ def generate_linux_install(
 | 
			
		||||
    api: str,
 | 
			
		||||
    download_url: str,
 | 
			
		||||
) -> FileResponse:
 | 
			
		||||
 | 
			
		||||
    match arch:
 | 
			
		||||
        case "amd64":
 | 
			
		||||
            arch_id = MeshAgentIdent.LINUX64
 | 
			
		||||
@@ -54,9 +54,7 @@ def generate_linux_install(
 | 
			
		||||
        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,
 | 
			
		||||
@@ -71,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"
 | 
			
		||||
        )
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +1,17 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
import datetime as dt
 | 
			
		||||
import os
 | 
			
		||||
import random
 | 
			
		||||
import string
 | 
			
		||||
import time
 | 
			
		||||
from io import StringIO
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.db.models import Count, Exists, OuterRef, Prefetch, Q
 | 
			
		||||
from django.db.models import Exists, OuterRef, Prefetch, Q
 | 
			
		||||
from django.http import HttpResponse
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from django.utils.dateparse import parse_datetime
 | 
			
		||||
from meshctrl.utils import get_login_token
 | 
			
		||||
from packaging import version as pyver
 | 
			
		||||
from rest_framework import serializers
 | 
			
		||||
@@ -19,15 +21,17 @@ from rest_framework.permissions import IsAuthenticated
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
 | 
			
		||||
from core.tasks import sync_mesh_perms_task
 | 
			
		||||
from core.utils import (
 | 
			
		||||
    get_core_settings,
 | 
			
		||||
    get_mesh_ws_url,
 | 
			
		||||
    remove_mesh_agent,
 | 
			
		||||
    token_is_valid,
 | 
			
		||||
    wake_on_lan,
 | 
			
		||||
)
 | 
			
		||||
from logs.models import AuditLog, DebugLog, PendingAction
 | 
			
		||||
from scripts.models import Script
 | 
			
		||||
from scripts.tasks import handle_bulk_command_task, handle_bulk_script_task
 | 
			
		||||
from scripts.tasks import bulk_command_task, bulk_script_task
 | 
			
		||||
from tacticalrmm.constants import (
 | 
			
		||||
    AGENT_DEFER,
 | 
			
		||||
    AGENT_STATUS_OFFLINE,
 | 
			
		||||
@@ -40,7 +44,6 @@ from tacticalrmm.constants import (
 | 
			
		||||
    DebugLogType,
 | 
			
		||||
    EvtLogNames,
 | 
			
		||||
    PAAction,
 | 
			
		||||
    PAStatus,
 | 
			
		||||
)
 | 
			
		||||
from tacticalrmm.helpers import date_is_in_past, notify_error
 | 
			
		||||
from tacticalrmm.permissions import (
 | 
			
		||||
@@ -49,7 +52,7 @@ from tacticalrmm.permissions import (
 | 
			
		||||
    _has_perm_on_site,
 | 
			
		||||
)
 | 
			
		||||
from tacticalrmm.utils import get_default_timezone, reload_nats
 | 
			
		||||
from winupdate.models import WinUpdate
 | 
			
		||||
from winupdate.models import WinUpdate, WinUpdatePolicy
 | 
			
		||||
from winupdate.serializers import WinUpdatePolicySerializer
 | 
			
		||||
from winupdate.tasks import bulk_check_for_updates_task, bulk_install_updates_task
 | 
			
		||||
 | 
			
		||||
@@ -58,11 +61,11 @@ from .permissions import (
 | 
			
		||||
    AgentHistoryPerms,
 | 
			
		||||
    AgentNotesPerms,
 | 
			
		||||
    AgentPerms,
 | 
			
		||||
    AgentWOLPerms,
 | 
			
		||||
    EvtLogPerms,
 | 
			
		||||
    InstallAgentPerms,
 | 
			
		||||
    ManageProcPerms,
 | 
			
		||||
    MeshPerms,
 | 
			
		||||
    PingAgentPerms,
 | 
			
		||||
    RebootAgentPerms,
 | 
			
		||||
    RecoverAgentPerms,
 | 
			
		||||
    RunBulkPerms,
 | 
			
		||||
@@ -134,19 +137,17 @@ class GetAgents(APIView):
 | 
			
		||||
                        "checkresults",
 | 
			
		||||
                        queryset=CheckResult.objects.select_related("assigned_check"),
 | 
			
		||||
                    ),
 | 
			
		||||
                )
 | 
			
		||||
                .annotate(
 | 
			
		||||
                    pending_actions_count=Count(
 | 
			
		||||
                        "pendingactions",
 | 
			
		||||
                        filter=Q(pendingactions__status=PAStatus.PENDING),
 | 
			
		||||
                    )
 | 
			
		||||
                    Prefetch(
 | 
			
		||||
                        "custom_fields",
 | 
			
		||||
                        queryset=AgentCustomField.objects.select_related("field"),
 | 
			
		||||
                    ),
 | 
			
		||||
                )
 | 
			
		||||
                .annotate(
 | 
			
		||||
                    has_patches_pending=Exists(
 | 
			
		||||
                        WinUpdate.objects.filter(
 | 
			
		||||
                            agent_id=OuterRef("pk"), action="approve", installed=False
 | 
			
		||||
                        )
 | 
			
		||||
                    )
 | 
			
		||||
                    ),
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            serializer = AgentTableSerializer(agents, many=True)
 | 
			
		||||
@@ -189,7 +190,36 @@ class GetUpdateDeleteAgent(APIView):
 | 
			
		||||
 | 
			
		||||
    # get agent details
 | 
			
		||||
    def get(self, request, agent_id):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=agent_id)
 | 
			
		||||
        from checks.models import Check, CheckResult
 | 
			
		||||
 | 
			
		||||
        agent = get_object_or_404(
 | 
			
		||||
            Agent.objects.select_related(
 | 
			
		||||
                "site__server_policy",
 | 
			
		||||
                "site__workstation_policy",
 | 
			
		||||
                "site__client__server_policy",
 | 
			
		||||
                "site__client__workstation_policy",
 | 
			
		||||
                "policy",
 | 
			
		||||
                "alert_template",
 | 
			
		||||
            ).prefetch_related(
 | 
			
		||||
                Prefetch(
 | 
			
		||||
                    "agentchecks",
 | 
			
		||||
                    queryset=Check.objects.select_related("script"),
 | 
			
		||||
                ),
 | 
			
		||||
                Prefetch(
 | 
			
		||||
                    "checkresults",
 | 
			
		||||
                    queryset=CheckResult.objects.select_related("assigned_check"),
 | 
			
		||||
                ),
 | 
			
		||||
                Prefetch(
 | 
			
		||||
                    "custom_fields",
 | 
			
		||||
                    queryset=AgentCustomField.objects.select_related("field"),
 | 
			
		||||
                ),
 | 
			
		||||
                Prefetch(
 | 
			
		||||
                    "winupdatepolicy",
 | 
			
		||||
                    queryset=WinUpdatePolicy.objects.select_related("agent", "policy"),
 | 
			
		||||
                ),
 | 
			
		||||
            ),
 | 
			
		||||
            agent_id=agent_id,
 | 
			
		||||
        )
 | 
			
		||||
        return Response(AgentSerializer(agent).data)
 | 
			
		||||
 | 
			
		||||
    # edit agent
 | 
			
		||||
@@ -209,9 +239,7 @@ class GetUpdateDeleteAgent(APIView):
 | 
			
		||||
            p_serializer.save()
 | 
			
		||||
 | 
			
		||||
        if "custom_fields" in request.data.keys():
 | 
			
		||||
 | 
			
		||||
            for field in request.data["custom_fields"]:
 | 
			
		||||
 | 
			
		||||
                custom_field = field
 | 
			
		||||
                custom_field["agent"] = agent.pk
 | 
			
		||||
 | 
			
		||||
@@ -231,6 +259,7 @@ class GetUpdateDeleteAgent(APIView):
 | 
			
		||||
                    serializer.is_valid(raise_exception=True)
 | 
			
		||||
                    serializer.save()
 | 
			
		||||
 | 
			
		||||
        sync_mesh_perms_task.delay()
 | 
			
		||||
        return Response("The agent was updated successfully")
 | 
			
		||||
 | 
			
		||||
    # uninstall agent
 | 
			
		||||
@@ -239,11 +268,9 @@ class GetUpdateDeleteAgent(APIView):
 | 
			
		||||
 | 
			
		||||
        code = "foo"  # stub for windows
 | 
			
		||||
        if agent.plat == AgentPlat.LINUX:
 | 
			
		||||
            with open(settings.LINUX_AGENT_SCRIPT, "r") as f:
 | 
			
		||||
                code = f.read()
 | 
			
		||||
            code = Path(settings.LINUX_AGENT_SCRIPT).read_text()
 | 
			
		||||
        elif agent.plat == AgentPlat.DARWIN:
 | 
			
		||||
            with open(settings.MAC_UNINSTALL, "r") as f:
 | 
			
		||||
                code = f.read()
 | 
			
		||||
            code = Path(settings.MAC_UNINSTALL).read_text()
 | 
			
		||||
 | 
			
		||||
        asyncio.run(agent.nats_cmd({"func": "uninstall", "code": code}, wait=False))
 | 
			
		||||
        name = agent.hostname
 | 
			
		||||
@@ -255,9 +282,10 @@ class GetUpdateDeleteAgent(APIView):
 | 
			
		||||
            asyncio.run(remove_mesh_agent(uri, mesh_id))
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            DebugLog.error(
 | 
			
		||||
                message=f"Unable to remove agent {name} from meshcentral database: {str(e)}",
 | 
			
		||||
                message=f"Unable to remove agent {name} from meshcentral database: {e}",
 | 
			
		||||
                log_type=DebugLogType.AGENT_ISSUES,
 | 
			
		||||
            )
 | 
			
		||||
        sync_mesh_perms_task.delay()
 | 
			
		||||
        return Response(f"{name} will now be uninstalled.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -273,7 +301,7 @@ class AgentProcesses(APIView):
 | 
			
		||||
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=agent_id)
 | 
			
		||||
        r = asyncio.run(agent.nats_cmd(data={"func": "procs"}, timeout=5))
 | 
			
		||||
        if r == "timeout" or r == "natsdown":
 | 
			
		||||
        if r in ("timeout", "natsdown"):
 | 
			
		||||
            return notify_error("Unable to contact the agent")
 | 
			
		||||
        return Response(r)
 | 
			
		||||
 | 
			
		||||
@@ -284,7 +312,7 @@ class AgentProcesses(APIView):
 | 
			
		||||
            agent.nats_cmd({"func": "killproc", "procpid": int(pid)}, timeout=15)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        if r == "timeout" or r == "natsdown":
 | 
			
		||||
        if r in ("timeout", "natsdown"):
 | 
			
		||||
            return notify_error("Unable to contact the agent")
 | 
			
		||||
        elif r != "ok":
 | 
			
		||||
            return notify_error(r)
 | 
			
		||||
@@ -300,13 +328,13 @@ class AgentMeshCentral(APIView):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=agent_id)
 | 
			
		||||
        core = get_core_settings()
 | 
			
		||||
 | 
			
		||||
        if not core.mesh_disable_auto_login:
 | 
			
		||||
            token = get_login_token(
 | 
			
		||||
                key=core.mesh_token, user=f"user//{core.mesh_username}"
 | 
			
		||||
            )
 | 
			
		||||
            token_param = f"login={token}&"
 | 
			
		||||
        else:
 | 
			
		||||
            token_param = ""
 | 
			
		||||
        user = (
 | 
			
		||||
            request.user.mesh_user_id
 | 
			
		||||
            if core.sync_mesh_with_trmm
 | 
			
		||||
            else f"user//{core.mesh_api_superuser}"
 | 
			
		||||
        )
 | 
			
		||||
        token = get_login_token(key=core.mesh_token, user=user)
 | 
			
		||||
        token_param = f"login={token}&"
 | 
			
		||||
 | 
			
		||||
        control = f"{core.mesh_site}/?{token_param}gotonode={agent.mesh_node_id}&viewmode=11&hide=31"
 | 
			
		||||
        terminal = f"{core.mesh_site}/?{token_param}gotonode={agent.mesh_node_id}&viewmode=12&hide=31"
 | 
			
		||||
@@ -376,7 +404,7 @@ def update_agents(request):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view(["GET"])
 | 
			
		||||
@permission_classes([IsAuthenticated, PingAgentPerms])
 | 
			
		||||
@permission_classes([IsAuthenticated, AgentPerms])
 | 
			
		||||
def ping(request, agent_id):
 | 
			
		||||
    agent = get_object_or_404(Agent, agent_id=agent_id)
 | 
			
		||||
    status = AGENT_STATUS_OFFLINE
 | 
			
		||||
@@ -416,7 +444,7 @@ def get_event_log(request, agent_id, logtype, days):
 | 
			
		||||
        },
 | 
			
		||||
    }
 | 
			
		||||
    r = asyncio.run(agent.nats_cmd(data, timeout=timeout + 2))
 | 
			
		||||
    if r == "timeout" or r == "natsdown":
 | 
			
		||||
    if r in ("timeout", "natsdown"):
 | 
			
		||||
        return notify_error("Unable to contact the agent")
 | 
			
		||||
 | 
			
		||||
    return Response(r)
 | 
			
		||||
@@ -466,8 +494,22 @@ def send_raw_cmd(request, agent_id):
 | 
			
		||||
    return Response(r)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Shutdown(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, RebootAgentPerms]
 | 
			
		||||
 | 
			
		||||
    # shutdown
 | 
			
		||||
    def post(self, request, agent_id):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=agent_id)
 | 
			
		||||
        r = asyncio.run(agent.nats_cmd({"func": "shutdown"}, timeout=10))
 | 
			
		||||
        if r != "ok":
 | 
			
		||||
            return notify_error("Unable to contact the agent")
 | 
			
		||||
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Reboot(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, RebootAgentPerms]
 | 
			
		||||
 | 
			
		||||
    # reboot now
 | 
			
		||||
    def post(self, request, agent_id):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=agent_id)
 | 
			
		||||
@@ -543,6 +585,20 @@ def install_agent(request):
 | 
			
		||||
    from agents.utils import get_agent_url
 | 
			
		||||
    from core.utils import token_is_valid
 | 
			
		||||
 | 
			
		||||
    insecure = getattr(settings, "TRMM_INSECURE", False)
 | 
			
		||||
 | 
			
		||||
    if insecure and request.data["installMethod"] in {"exe", "powershell"}:
 | 
			
		||||
        return notify_error(
 | 
			
		||||
            "Not available in insecure mode. Please use the 'Manual' method."
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    # TODO rework this ghetto validation hack
 | 
			
		||||
    # https://github.com/amidaware/tacticalrmm/issues/1461
 | 
			
		||||
    try:
 | 
			
		||||
        int(request.data["expires"])
 | 
			
		||||
    except ValueError:
 | 
			
		||||
        return notify_error("Please enter a valid number of hours")
 | 
			
		||||
 | 
			
		||||
    client_id = request.data["client"]
 | 
			
		||||
    site_id = request.data["site"]
 | 
			
		||||
    version = settings.LATEST_AGENT_VER
 | 
			
		||||
@@ -556,7 +612,7 @@ def install_agent(request):
 | 
			
		||||
 | 
			
		||||
    if request.data["installMethod"] in {"bash", "mac"} and not is_valid:
 | 
			
		||||
        return notify_error(
 | 
			
		||||
            "Missing code signing token, or token is no longer valid. Please read the docs for more info."
 | 
			
		||||
            "Linux/Mac agents require code signing. Please see https://docs.tacticalrmm.com/code_signing/ for more info."
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    inno = f"tacticalagent-v{version}-{plat}-{goarch}"
 | 
			
		||||
@@ -568,7 +624,7 @@ def install_agent(request):
 | 
			
		||||
    installer_user = User.objects.filter(is_installer_user=True).first()
 | 
			
		||||
 | 
			
		||||
    _, token = AuthToken.objects.create(
 | 
			
		||||
        user=installer_user, expiry=dt.timedelta(hours=request.data["expires"])
 | 
			
		||||
        user=installer_user, expiry=dt.timedelta(hours=int(request.data["expires"]))
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    install_flags = [
 | 
			
		||||
@@ -603,7 +659,6 @@ def install_agent(request):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    elif request.data["installMethod"] == "bash":
 | 
			
		||||
 | 
			
		||||
        from agents.utils import generate_linux_install
 | 
			
		||||
 | 
			
		||||
        return generate_linux_install(
 | 
			
		||||
@@ -639,6 +694,9 @@ def install_agent(request):
 | 
			
		||||
            if int(request.data["power"]):
 | 
			
		||||
                cmd.append("--power")
 | 
			
		||||
 | 
			
		||||
            if insecure:
 | 
			
		||||
                cmd.append("--insecure")
 | 
			
		||||
 | 
			
		||||
            resp["cmd"] = " ".join(str(i) for i in cmd)
 | 
			
		||||
        else:
 | 
			
		||||
            install_flags.insert(0, f"sudo ./{inno}")
 | 
			
		||||
@@ -647,17 +705,15 @@ def install_agent(request):
 | 
			
		||||
            resp["cmd"] = (
 | 
			
		||||
                dl + f" && chmod +x {inno} && " + " ".join(str(i) for i in cmd)
 | 
			
		||||
            )
 | 
			
		||||
            if insecure:
 | 
			
		||||
                resp["cmd"] += " --insecure"
 | 
			
		||||
 | 
			
		||||
        resp["url"] = download_url
 | 
			
		||||
 | 
			
		||||
        return Response(resp)
 | 
			
		||||
 | 
			
		||||
    elif request.data["installMethod"] == "powershell":
 | 
			
		||||
 | 
			
		||||
        ps = os.path.join(settings.BASE_DIR, "core/installer.ps1")
 | 
			
		||||
 | 
			
		||||
        with open(ps, "r") as f:
 | 
			
		||||
            text = f.read()
 | 
			
		||||
        text = Path(settings.BASE_DIR / "core" / "installer.ps1").read_text()
 | 
			
		||||
 | 
			
		||||
        replace_dict = {
 | 
			
		||||
            "innosetupchange": inno,
 | 
			
		||||
@@ -675,27 +731,9 @@ def install_agent(request):
 | 
			
		||||
        for i, j in replace_dict.items():
 | 
			
		||||
            text = text.replace(i, j)
 | 
			
		||||
 | 
			
		||||
        file_name = "rmm-installer.ps1"
 | 
			
		||||
        ps1 = os.path.join(settings.EXE_DIR, file_name)
 | 
			
		||||
 | 
			
		||||
        if os.path.exists(ps1):
 | 
			
		||||
            try:
 | 
			
		||||
                os.remove(ps1)
 | 
			
		||||
            except Exception as e:
 | 
			
		||||
                DebugLog.error(message=str(e))
 | 
			
		||||
 | 
			
		||||
        with open(ps1, "w") as f:
 | 
			
		||||
            f.write(text)
 | 
			
		||||
 | 
			
		||||
        if settings.DEBUG:
 | 
			
		||||
            with open(ps1, "r") as f:
 | 
			
		||||
                response = HttpResponse(f.read(), content_type="text/plain")
 | 
			
		||||
                response["Content-Disposition"] = f"inline; filename={file_name}"
 | 
			
		||||
                return response
 | 
			
		||||
        else:
 | 
			
		||||
            response = HttpResponse()
 | 
			
		||||
            response["Content-Disposition"] = f"attachment; filename={file_name}"
 | 
			
		||||
            response["X-Accel-Redirect"] = f"/private/exe/{file_name}"
 | 
			
		||||
        with StringIO(text) as fp:
 | 
			
		||||
            response = HttpResponse(fp.read(), content_type="text/plain")
 | 
			
		||||
            response["Content-Disposition"] = "attachment; filename=rmm-installer.ps1"
 | 
			
		||||
            return response
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -728,7 +766,12 @@ def run_script(request, agent_id):
 | 
			
		||||
    output = request.data["output"]
 | 
			
		||||
    args = request.data["args"]
 | 
			
		||||
    run_as_user: bool = request.data["run_as_user"]
 | 
			
		||||
    env_vars: list[str] = request.data["env_vars"]
 | 
			
		||||
    req_timeout = int(request.data["timeout"]) + 3
 | 
			
		||||
    run_on_server: bool | None = request.data.get("run_on_server")
 | 
			
		||||
 | 
			
		||||
    if run_on_server and not get_core_settings().server_scripts_enabled:
 | 
			
		||||
        return notify_error("This feature is disabled.")
 | 
			
		||||
 | 
			
		||||
    AuditLog.audit_script_run(
 | 
			
		||||
        username=request.user.username,
 | 
			
		||||
@@ -745,6 +788,29 @@ def run_script(request, agent_id):
 | 
			
		||||
    )
 | 
			
		||||
    history_pk = hist.pk
 | 
			
		||||
 | 
			
		||||
    if run_on_server:
 | 
			
		||||
        from core.utils import run_server_script
 | 
			
		||||
 | 
			
		||||
        r = run_server_script(
 | 
			
		||||
            body=script.script_body,
 | 
			
		||||
            args=script.parse_script_args(agent, script.shell, args),
 | 
			
		||||
            env_vars=script.parse_script_env_vars(agent, script.shell, env_vars),
 | 
			
		||||
            shell=script.shell,
 | 
			
		||||
            timeout=req_timeout,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        ret = {
 | 
			
		||||
            "stdout": r[0],
 | 
			
		||||
            "stderr": r[1],
 | 
			
		||||
            "execution_time": "{:.4f}".format(r[2]),
 | 
			
		||||
            "retcode": r[3],
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        hist.script_results = {**ret, "id": history_pk}
 | 
			
		||||
        hist.save(update_fields=["script_results"])
 | 
			
		||||
 | 
			
		||||
        return Response(ret)
 | 
			
		||||
 | 
			
		||||
    if output == "wait":
 | 
			
		||||
        r = agent.run_script(
 | 
			
		||||
            scriptpk=script.pk,
 | 
			
		||||
@@ -753,6 +819,7 @@ def run_script(request, agent_id):
 | 
			
		||||
            wait=True,
 | 
			
		||||
            history_pk=history_pk,
 | 
			
		||||
            run_as_user=run_as_user,
 | 
			
		||||
            env_vars=env_vars,
 | 
			
		||||
        )
 | 
			
		||||
        return Response(r)
 | 
			
		||||
 | 
			
		||||
@@ -766,7 +833,9 @@ def run_script(request, agent_id):
 | 
			
		||||
            nats_timeout=req_timeout,
 | 
			
		||||
            emails=emails,
 | 
			
		||||
            args=args,
 | 
			
		||||
            history_pk=history_pk,
 | 
			
		||||
            run_as_user=run_as_user,
 | 
			
		||||
            env_vars=env_vars,
 | 
			
		||||
        )
 | 
			
		||||
    elif output == "collector":
 | 
			
		||||
        from core.models import CustomField
 | 
			
		||||
@@ -778,6 +847,7 @@ def run_script(request, agent_id):
 | 
			
		||||
            wait=True,
 | 
			
		||||
            history_pk=history_pk,
 | 
			
		||||
            run_as_user=run_as_user,
 | 
			
		||||
            env_vars=env_vars,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        custom_field = CustomField.objects.get(pk=request.data["custom_field"])
 | 
			
		||||
@@ -807,6 +877,7 @@ def run_script(request, agent_id):
 | 
			
		||||
            wait=True,
 | 
			
		||||
            history_pk=history_pk,
 | 
			
		||||
            run_as_user=run_as_user,
 | 
			
		||||
            env_vars=env_vars,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        Note.objects.create(agent=agent, user=request.user, note=r)
 | 
			
		||||
@@ -818,6 +889,7 @@ def run_script(request, agent_id):
 | 
			
		||||
            timeout=req_timeout,
 | 
			
		||||
            history_pk=history_pk,
 | 
			
		||||
            run_as_user=run_as_user,
 | 
			
		||||
            env_vars=env_vars,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    return Response(f"{script.name} will now be run on {agent.hostname}")
 | 
			
		||||
@@ -933,7 +1005,7 @@ def bulk(request):
 | 
			
		||||
    agents: list[int] = [agent.pk for agent in q]
 | 
			
		||||
 | 
			
		||||
    if not agents:
 | 
			
		||||
        return notify_error("No agents where found meeting the selected criteria")
 | 
			
		||||
        return notify_error("No agents were found meeting the selected criteria")
 | 
			
		||||
 | 
			
		||||
    AuditLog.audit_bulk_action(
 | 
			
		||||
        request.user,
 | 
			
		||||
@@ -942,36 +1014,53 @@ def bulk(request):
 | 
			
		||||
        debug_info={"ip": request._client_ip},
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    ht = "Check the History tab on the agent to view the results."
 | 
			
		||||
 | 
			
		||||
    if request.data["mode"] == "command":
 | 
			
		||||
        if request.data["shell"] == "custom" and request.data["custom_shell"]:
 | 
			
		||||
            shell = request.data["custom_shell"]
 | 
			
		||||
        else:
 | 
			
		||||
            shell = request.data["shell"]
 | 
			
		||||
 | 
			
		||||
        handle_bulk_command_task.delay(
 | 
			
		||||
            agents,
 | 
			
		||||
            request.data["cmd"],
 | 
			
		||||
            shell,
 | 
			
		||||
            request.data["timeout"],
 | 
			
		||||
            request.user.username[:50],
 | 
			
		||||
            request.data["run_as_user"],
 | 
			
		||||
        bulk_command_task.delay(
 | 
			
		||||
            agent_pks=agents,
 | 
			
		||||
            cmd=request.data["cmd"],
 | 
			
		||||
            shell=shell,
 | 
			
		||||
            timeout=request.data["timeout"],
 | 
			
		||||
            username=request.user.username[:50],
 | 
			
		||||
            run_as_user=request.data["run_as_user"],
 | 
			
		||||
        )
 | 
			
		||||
        return Response(f"Command will now be run on {len(agents)} agents")
 | 
			
		||||
        return Response(f"Command will now be run on {len(agents)} agents. {ht}")
 | 
			
		||||
 | 
			
		||||
    elif request.data["mode"] == "script":
 | 
			
		||||
        script = get_object_or_404(Script, pk=request.data["script"])
 | 
			
		||||
        handle_bulk_script_task.delay(
 | 
			
		||||
            script.pk,
 | 
			
		||||
            agents,
 | 
			
		||||
            request.data["args"],
 | 
			
		||||
            request.data["timeout"],
 | 
			
		||||
            request.user.username[:50],
 | 
			
		||||
            request.data["run_as_user"],
 | 
			
		||||
 | 
			
		||||
        # prevent API from breaking for those who haven't updated payload
 | 
			
		||||
        try:
 | 
			
		||||
            custom_field_pk = request.data["custom_field"]
 | 
			
		||||
            collector_all_output = request.data["collector_all_output"]
 | 
			
		||||
            save_to_agent_note = request.data["save_to_agent_note"]
 | 
			
		||||
        except KeyError:
 | 
			
		||||
            custom_field_pk = None
 | 
			
		||||
            collector_all_output = False
 | 
			
		||||
            save_to_agent_note = False
 | 
			
		||||
 | 
			
		||||
        bulk_script_task.delay(
 | 
			
		||||
            script_pk=script.pk,
 | 
			
		||||
            agent_pks=agents,
 | 
			
		||||
            args=request.data["args"],
 | 
			
		||||
            timeout=request.data["timeout"],
 | 
			
		||||
            username=request.user.username[:50],
 | 
			
		||||
            run_as_user=request.data["run_as_user"],
 | 
			
		||||
            env_vars=request.data["env_vars"],
 | 
			
		||||
            custom_field_pk=custom_field_pk,
 | 
			
		||||
            collector_all_output=collector_all_output,
 | 
			
		||||
            save_to_agent_note=save_to_agent_note,
 | 
			
		||||
        )
 | 
			
		||||
        return Response(f"{script.name} will now be run on {len(agents)} agents")
 | 
			
		||||
 | 
			
		||||
        return Response(f"{script.name} will now be run on {len(agents)} agents. {ht}")
 | 
			
		||||
 | 
			
		||||
    elif request.data["mode"] == "patch":
 | 
			
		||||
 | 
			
		||||
        if request.data["patchMode"] == "install":
 | 
			
		||||
            bulk_install_updates_task.delay(agents)
 | 
			
		||||
            return Response(
 | 
			
		||||
@@ -987,7 +1076,6 @@ def bulk(request):
 | 
			
		||||
@api_view(["POST"])
 | 
			
		||||
@permission_classes([IsAuthenticated, AgentPerms])
 | 
			
		||||
def agent_maintenance(request):
 | 
			
		||||
 | 
			
		||||
    if request.data["type"] == "Client":
 | 
			
		||||
        if not _has_perm_on_client(request.user, request.data["id"]):
 | 
			
		||||
            raise PermissionDenied()
 | 
			
		||||
@@ -1014,10 +1102,10 @@ def agent_maintenance(request):
 | 
			
		||||
    if count:
 | 
			
		||||
        action = "disabled" if not request.data["action"] else "enabled"
 | 
			
		||||
        return Response(f"Maintenance mode has been {action} on {count} agents")
 | 
			
		||||
    else:
 | 
			
		||||
        return Response(
 | 
			
		||||
            f"No agents have been put in maintenance mode. You might not have permissions to the resources."
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    return Response(
 | 
			
		||||
        "No agents have been put in maintenance mode. You might not have permissions to the resources."
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view(["GET"])
 | 
			
		||||
@@ -1049,3 +1137,113 @@ class AgentHistoryView(APIView):
 | 
			
		||||
            history = AgentHistory.objects.filter_by_role(request.user)  # type: ignore
 | 
			
		||||
        ctx = {"default_tz": get_default_timezone()}
 | 
			
		||||
        return Response(AgentHistorySerializer(history, many=True, context=ctx).data)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ScriptRunHistory(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, AgentHistoryPerms]
 | 
			
		||||
 | 
			
		||||
    class OutputSerializer(serializers.ModelSerializer):
 | 
			
		||||
        script_name = serializers.ReadOnlyField(source="script.name")
 | 
			
		||||
        agent_id = serializers.ReadOnlyField(source="agent.agent_id")
 | 
			
		||||
 | 
			
		||||
        class Meta:
 | 
			
		||||
            model = AgentHistory
 | 
			
		||||
            fields = (
 | 
			
		||||
                "id",
 | 
			
		||||
                "time",
 | 
			
		||||
                "username",
 | 
			
		||||
                "script",
 | 
			
		||||
                "script_results",
 | 
			
		||||
                "agent",
 | 
			
		||||
                "script_name",
 | 
			
		||||
                "agent_id",
 | 
			
		||||
            )
 | 
			
		||||
            read_only_fields = fields
 | 
			
		||||
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
        date_range_filter = Q()
 | 
			
		||||
        script_name_filter = Q()
 | 
			
		||||
 | 
			
		||||
        start = request.query_params.get("start", None)
 | 
			
		||||
        end = request.query_params.get("end", None)
 | 
			
		||||
        limit = request.query_params.get("limit", None)
 | 
			
		||||
        script_name = request.query_params.get("scriptname", None)
 | 
			
		||||
        if start and end:
 | 
			
		||||
            start_dt = parse_datetime(start)
 | 
			
		||||
            end_dt = parse_datetime(end) + djangotime.timedelta(days=1)
 | 
			
		||||
            date_range_filter = Q(time__range=[start_dt, end_dt])
 | 
			
		||||
 | 
			
		||||
        if script_name:
 | 
			
		||||
            script_name_filter = Q(script__name=script_name)
 | 
			
		||||
 | 
			
		||||
        AGENT_R_DEFER = (
 | 
			
		||||
            "agent__wmi_detail",
 | 
			
		||||
            "agent__services",
 | 
			
		||||
            "agent__created_by",
 | 
			
		||||
            "agent__created_time",
 | 
			
		||||
            "agent__modified_by",
 | 
			
		||||
            "agent__modified_time",
 | 
			
		||||
            "agent__disks",
 | 
			
		||||
            "agent__operating_system",
 | 
			
		||||
            "agent__mesh_node_id",
 | 
			
		||||
            "agent__description",
 | 
			
		||||
            "agent__patches_last_installed",
 | 
			
		||||
            "agent__time_zone",
 | 
			
		||||
            "agent__alert_template_id",
 | 
			
		||||
            "agent__policy_id",
 | 
			
		||||
            "agent__site_id",
 | 
			
		||||
            "agent__version",
 | 
			
		||||
            "agent__plat",
 | 
			
		||||
            "agent__goarch",
 | 
			
		||||
            "agent__hostname",
 | 
			
		||||
            "agent__last_seen",
 | 
			
		||||
            "agent__public_ip",
 | 
			
		||||
            "agent__total_ram",
 | 
			
		||||
            "agent__boot_time",
 | 
			
		||||
            "agent__logged_in_username",
 | 
			
		||||
            "agent__last_logged_in_user",
 | 
			
		||||
            "agent__monitoring_type",
 | 
			
		||||
            "agent__overdue_email_alert",
 | 
			
		||||
            "agent__overdue_text_alert",
 | 
			
		||||
            "agent__overdue_dashboard_alert",
 | 
			
		||||
            "agent__offline_time",
 | 
			
		||||
            "agent__overdue_time",
 | 
			
		||||
            "agent__check_interval",
 | 
			
		||||
            "agent__needs_reboot",
 | 
			
		||||
            "agent__choco_installed",
 | 
			
		||||
            "agent__maintenance_mode",
 | 
			
		||||
            "agent__block_policy_inheritance",
 | 
			
		||||
        )
 | 
			
		||||
        hists = (
 | 
			
		||||
            AgentHistory.objects.filter(type=AgentHistoryType.SCRIPT_RUN)
 | 
			
		||||
            .select_related("agent")
 | 
			
		||||
            .select_related("script")
 | 
			
		||||
            .defer(*AGENT_R_DEFER)
 | 
			
		||||
            .filter(date_range_filter)
 | 
			
		||||
            .filter(script_name_filter)
 | 
			
		||||
            .order_by("-time")
 | 
			
		||||
        )
 | 
			
		||||
        if limit:
 | 
			
		||||
            try:
 | 
			
		||||
                lim = int(limit)
 | 
			
		||||
            except KeyError:
 | 
			
		||||
                return notify_error("Invalid limit")
 | 
			
		||||
            hists = hists[:lim]
 | 
			
		||||
 | 
			
		||||
        ret = self.OutputSerializer(hists, many=True).data
 | 
			
		||||
        return Response(ret)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view(["POST"])
 | 
			
		||||
@permission_classes([IsAuthenticated, AgentWOLPerms])
 | 
			
		||||
def wol(request, agent_id):
 | 
			
		||||
    agent = get_object_or_404(
 | 
			
		||||
        Agent.objects.defer(*AGENT_DEFER),
 | 
			
		||||
        agent_id=agent_id,
 | 
			
		||||
    )
 | 
			
		||||
    try:
 | 
			
		||||
        uri = get_mesh_ws_url()
 | 
			
		||||
        asyncio.run(wake_on_lan(uri=uri, mesh_node_id=agent.mesh_node_id))
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        return notify_error(str(e))
 | 
			
		||||
    return Response(f"Wake-on-LAN sent to {agent.hostname}")
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -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,
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
from __future__ import annotations
 | 
			
		||||
 | 
			
		||||
import re
 | 
			
		||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union, cast
 | 
			
		||||
 | 
			
		||||
from django.contrib.postgres.fields import ArrayField
 | 
			
		||||
@@ -8,15 +7,20 @@ from django.db import models
 | 
			
		||||
from django.db.models.fields import BooleanField, PositiveIntegerField
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
@@ -94,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()
 | 
			
		||||
@@ -105,6 +118,9 @@ 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=AlertType.AVAILABILITY, resolved=False
 | 
			
		||||
        ).exists():
 | 
			
		||||
@@ -117,7 +133,7 @@ class Alert(models.Model):
 | 
			
		||||
                    agent=agent,
 | 
			
		||||
                    alert_type=AlertType.AVAILABILITY,
 | 
			
		||||
                    severity=AlertSeverity.ERROR,
 | 
			
		||||
                    message=f"{agent.hostname} in {agent.client.name}\\{agent.site.name} is overdue.",
 | 
			
		||||
                    message=f"{agent.hostname} in {agent.client.name}, {agent.site.name} is overdue.",
 | 
			
		||||
                    hidden=True,
 | 
			
		||||
                ),
 | 
			
		||||
            )
 | 
			
		||||
@@ -153,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(
 | 
			
		||||
@@ -169,15 +187,17 @@ class Alert(models.Model):
 | 
			
		||||
                    assigned_check=check,
 | 
			
		||||
                    agent=agent,
 | 
			
		||||
                    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,
 | 
			
		||||
                    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,
 | 
			
		||||
@@ -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):
 | 
			
		||||
@@ -308,7 +332,7 @@ class Alert(models.Model):
 | 
			
		||||
                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,12 +351,12 @@ class Alert(models.Model):
 | 
			
		||||
            alert_severity = (
 | 
			
		||||
                instance.assigned_check.alert_severity
 | 
			
		||||
                if instance.assigned_check.check_type
 | 
			
		||||
                not in [
 | 
			
		||||
                not in {
 | 
			
		||||
                    CheckType.MEMORY,
 | 
			
		||||
                    CheckType.CPU_LOAD,
 | 
			
		||||
                    CheckType.DISK_SPACE,
 | 
			
		||||
                    CheckType.SCRIPT,
 | 
			
		||||
                ]
 | 
			
		||||
                }
 | 
			
		||||
                else instance.alert_severity
 | 
			
		||||
            )
 | 
			
		||||
            agent = instance.agent
 | 
			
		||||
@@ -341,28 +365,25 @@ class Alert(models.Model):
 | 
			
		||||
            if alert_template:
 | 
			
		||||
                dashboard_severities = (
 | 
			
		||||
                    alert_template.check_dashboard_alert_severity
 | 
			
		||||
                    if alert_template.check_dashboard_alert_severity
 | 
			
		||||
                    else [
 | 
			
		||||
                    or [
 | 
			
		||||
                        AlertSeverity.ERROR,
 | 
			
		||||
                        AlertSeverity.WARNING,
 | 
			
		||||
                        AlertSeverity.INFO,
 | 
			
		||||
                    ]
 | 
			
		||||
                )
 | 
			
		||||
                email_severities = (
 | 
			
		||||
                    alert_template.check_email_alert_severity
 | 
			
		||||
                    if alert_template.check_email_alert_severity
 | 
			
		||||
                    else [AlertSeverity.ERROR, AlertSeverity.WARNING]
 | 
			
		||||
                )
 | 
			
		||||
                text_severities = (
 | 
			
		||||
                    alert_template.check_text_alert_severity
 | 
			
		||||
                    if alert_template.check_text_alert_severity
 | 
			
		||||
                    else [AlertSeverity.ERROR, AlertSeverity.WARNING]
 | 
			
		||||
                )
 | 
			
		||||
                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
 | 
			
		||||
@@ -380,26 +401,23 @@ class Alert(models.Model):
 | 
			
		||||
 | 
			
		||||
            # set alert_template settings
 | 
			
		||||
            if alert_template:
 | 
			
		||||
                dashboard_severities = (
 | 
			
		||||
                    alert_template.task_dashboard_alert_severity
 | 
			
		||||
                    if alert_template.task_dashboard_alert_severity
 | 
			
		||||
                    else [AlertSeverity.ERROR, AlertSeverity.WARNING]
 | 
			
		||||
                )
 | 
			
		||||
                email_severities = (
 | 
			
		||||
                    alert_template.task_email_alert_severity
 | 
			
		||||
                    if alert_template.task_email_alert_severity
 | 
			
		||||
                    else [AlertSeverity.ERROR, AlertSeverity.WARNING]
 | 
			
		||||
                )
 | 
			
		||||
                text_severities = (
 | 
			
		||||
                    alert_template.task_text_alert_severity
 | 
			
		||||
                    if alert_template.task_text_alert_severity
 | 
			
		||||
                    else [AlertSeverity.ERROR, AlertSeverity.WARNING]
 | 
			
		||||
                )
 | 
			
		||||
                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
 | 
			
		||||
@@ -417,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
 | 
			
		||||
@@ -428,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
 | 
			
		||||
            ):
 | 
			
		||||
@@ -443,34 +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,
 | 
			
		||||
                run_as_user=False,
 | 
			
		||||
            )
 | 
			
		||||
        # 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):
 | 
			
		||||
@@ -481,26 +563,37 @@ class Alert(models.Model):
 | 
			
		||||
                alert.action_run = djangotime.now()
 | 
			
		||||
                alert.save()
 | 
			
		||||
            else:
 | 
			
		||||
                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",
 | 
			
		||||
                )
 | 
			
		||||
                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):
 | 
			
		||||
@@ -516,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
 | 
			
		||||
@@ -539,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 (
 | 
			
		||||
@@ -557,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
 | 
			
		||||
@@ -572,28 +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,
 | 
			
		||||
                run_as_user=False,
 | 
			
		||||
            )
 | 
			
		||||
            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):
 | 
			
		||||
@@ -606,40 +792,36 @@ class Alert(models.Model):
 | 
			
		||||
                alert.resolved_action_run = djangotime.now()
 | 
			
		||||
                alert.save()
 | 
			
		||||
            else:
 | 
			
		||||
                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",
 | 
			
		||||
                )
 | 
			
		||||
                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=DebugLogType.SCRIPTING, message=str(e))
 | 
			
		||||
                    continue
 | 
			
		||||
 | 
			
		||||
            else:
 | 
			
		||||
                temp_args.append(arg)
 | 
			
		||||
            temp_args.append(temp_arg)
 | 
			
		||||
 | 
			
		||||
        return temp_args
 | 
			
		||||
 | 
			
		||||
@@ -648,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",
 | 
			
		||||
@@ -655,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",
 | 
			
		||||
@@ -669,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
 | 
			
		||||
@@ -701,7 +919,8 @@ 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(
 | 
			
		||||
@@ -725,7 +944,8 @@ 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(
 | 
			
		||||
@@ -749,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)
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ from typing import TYPE_CHECKING
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from rest_framework import permissions
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.constants import AlertTemplateActionType
 | 
			
		||||
from tacticalrmm.permissions import _has_perm, _has_perm_on_agent
 | 
			
		||||
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
@@ -32,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"]
 | 
			
		||||
@@ -52,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")
 | 
			
		||||
 
 | 
			
		||||
@@ -3,12 +3,12 @@ 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")
 | 
			
		||||
@@ -26,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()
 | 
			
		||||
 
 | 
			
		||||
@@ -1,16 +1,21 @@
 | 
			
		||||
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 django.conf import settings
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from model_bakery import baker, seq
 | 
			
		||||
 | 
			
		||||
from alerts.tasks import cache_agents_alert_template
 | 
			
		||||
from autotasks.models import TaskResult
 | 
			
		||||
from core.tasks import cache_db_fields_task, handle_resolved_stuff
 | 
			
		||||
from core.utils import get_core_settings
 | 
			
		||||
from tacticalrmm.constants import AgentMonType, AlertSeverity, AlertType, CheckStatus
 | 
			
		||||
from tacticalrmm.constants import (
 | 
			
		||||
    AgentMonType,
 | 
			
		||||
    AlertSeverity,
 | 
			
		||||
    AlertType,
 | 
			
		||||
    CheckStatus,
 | 
			
		||||
    URLActionType,
 | 
			
		||||
)
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
 | 
			
		||||
from .models import Alert, AlertTemplate
 | 
			
		||||
@@ -28,6 +33,7 @@ class TestAlertsViews(TacticalTestCase):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        self.setup_coresettings()
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    def test_get_alerts(self):
 | 
			
		||||
        url = "/alerts/"
 | 
			
		||||
 | 
			
		||||
@@ -39,14 +45,14 @@ class TestAlertsViews(TacticalTestCase):
 | 
			
		||||
        alerts = baker.make(
 | 
			
		||||
            "alerts.Alert",
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            alert_time=seq(datetime.now(), timedelta(days=15)),
 | 
			
		||||
            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)),
 | 
			
		||||
            alert_time=seq(djangotime.now(), timedelta(days=15)),
 | 
			
		||||
            severity=AlertSeverity.ERROR,
 | 
			
		||||
            _quantity=7,
 | 
			
		||||
        )
 | 
			
		||||
@@ -55,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(
 | 
			
		||||
@@ -63,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,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@@ -120,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",
 | 
			
		||||
@@ -275,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)
 | 
			
		||||
 | 
			
		||||
@@ -363,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,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@@ -371,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,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@@ -389,7 +416,6 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_agent_gets_correct_alert_template(self):
 | 
			
		||||
 | 
			
		||||
        core = get_core_settings()
 | 
			
		||||
        # setup data
 | 
			
		||||
        workstation = baker.make_recipe(
 | 
			
		||||
@@ -677,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()
 | 
			
		||||
@@ -688,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
 | 
			
		||||
@@ -1373,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
 | 
			
		||||
@@ -1399,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()
 | 
			
		||||
@@ -1422,9 +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)
 | 
			
		||||
@@ -1445,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 = {
 | 
			
		||||
@@ -1454,6 +1485,10 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
            "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)
 | 
			
		||||
@@ -1627,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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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()
 | 
			
		||||
 
 | 
			
		||||
@@ -77,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)
 | 
			
		||||
@@ -89,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)
 | 
			
		||||
@@ -129,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")
 | 
			
		||||
@@ -296,3 +301,9 @@ class TestAPIv3(TacticalTestCase):
 | 
			
		||||
            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)
 | 
			
		||||
 
 | 
			
		||||
@@ -19,4 +19,5 @@ urlpatterns = [
 | 
			
		||||
    path("superseded/", views.SupersededWinUpdate.as_view()),
 | 
			
		||||
    path("<int:pk>/chocoresult/", views.ChocoResult.as_view()),
 | 
			
		||||
    path("<int:pk>/<str:agentid>/histresult/", views.AgentHistoryResult.as_view()),
 | 
			
		||||
    path("<str:agentid>/config/", views.AgentConfig.as_view()),
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										33
									
								
								api/tacticalrmm/apiv3/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								api/tacticalrmm/apiv3/utils.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
			
		||||
import random
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.structs import AgentCheckInConfig
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_agent_config() -> AgentCheckInConfig:
 | 
			
		||||
    return AgentCheckInConfig(
 | 
			
		||||
        checkin_hello=random.randint(*getattr(settings, "CHECKIN_HELLO", (30, 60))),
 | 
			
		||||
        checkin_agentinfo=random.randint(
 | 
			
		||||
            *getattr(settings, "CHECKIN_AGENTINFO", (200, 400))
 | 
			
		||||
        ),
 | 
			
		||||
        checkin_winsvc=random.randint(
 | 
			
		||||
            *getattr(settings, "CHECKIN_WINSVC", (2400, 3000))
 | 
			
		||||
        ),
 | 
			
		||||
        checkin_pubip=random.randint(*getattr(settings, "CHECKIN_PUBIP", (300, 500))),
 | 
			
		||||
        checkin_disks=random.randint(*getattr(settings, "CHECKIN_DISKS", (1000, 2000))),
 | 
			
		||||
        checkin_sw=random.randint(*getattr(settings, "CHECKIN_SW", (2800, 3500))),
 | 
			
		||||
        checkin_wmi=random.randint(*getattr(settings, "CHECKIN_WMI", (3000, 4000))),
 | 
			
		||||
        checkin_syncmesh=random.randint(
 | 
			
		||||
            *getattr(settings, "CHECKIN_SYNCMESH", (800, 1200))
 | 
			
		||||
        ),
 | 
			
		||||
        limit_data=getattr(settings, "LIMIT_DATA", False),
 | 
			
		||||
        install_nushell=getattr(settings, "INSTALL_NUSHELL", False),
 | 
			
		||||
        install_nushell_version=getattr(settings, "INSTALL_NUSHELL_VERSION", ""),
 | 
			
		||||
        install_nushell_url=getattr(settings, "INSTALL_NUSHELL_URL", ""),
 | 
			
		||||
        nushell_enable_config=getattr(settings, "NUSHELL_ENABLE_CONFIG", False),
 | 
			
		||||
        install_deno=getattr(settings, "INSTALL_DENO", False),
 | 
			
		||||
        install_deno_version=getattr(settings, "INSTALL_DENO_VERSION", ""),
 | 
			
		||||
        install_deno_url=getattr(settings, "INSTALL_DENO_URL", ""),
 | 
			
		||||
        deno_default_permissions=getattr(settings, "DENO_DEFAULT_PERMISSIONS", ""),
 | 
			
		||||
    )
 | 
			
		||||
@@ -12,13 +12,16 @@ from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
 | 
			
		||||
from accounts.models import User
 | 
			
		||||
from agents.models import Agent, AgentHistory
 | 
			
		||||
from agents.models import Agent, AgentHistory, Note
 | 
			
		||||
from agents.serializers import AgentHistorySerializer
 | 
			
		||||
from alerts.tasks import cache_agents_alert_template
 | 
			
		||||
from apiv3.utils import get_agent_config
 | 
			
		||||
from autotasks.models import AutomatedTask, TaskResult
 | 
			
		||||
from autotasks.serializers import TaskGOGetSerializer, TaskResultSerializer
 | 
			
		||||
from checks.constants import CHECK_DEFER, CHECK_RESULT_DEFER
 | 
			
		||||
from checks.models import Check, CheckResult
 | 
			
		||||
from checks.serializers import CheckRunnerGetSerializer
 | 
			
		||||
from core.tasks import sync_mesh_perms_task
 | 
			
		||||
from core.utils import (
 | 
			
		||||
    download_mesh_agent,
 | 
			
		||||
    get_core_settings,
 | 
			
		||||
@@ -30,23 +33,25 @@ from logs.models import DebugLog, PendingAction
 | 
			
		||||
from software.models import InstalledSoftware
 | 
			
		||||
from tacticalrmm.constants import (
 | 
			
		||||
    AGENT_DEFER,
 | 
			
		||||
    TRMM_MAX_REQUEST_SIZE,
 | 
			
		||||
    AgentHistoryType,
 | 
			
		||||
    AgentMonType,
 | 
			
		||||
    AgentPlat,
 | 
			
		||||
    AuditActionType,
 | 
			
		||||
    AuditObjType,
 | 
			
		||||
    CheckStatus,
 | 
			
		||||
    CustomFieldModel,
 | 
			
		||||
    DebugLogType,
 | 
			
		||||
    GoArch,
 | 
			
		||||
    MeshAgentIdent,
 | 
			
		||||
    PAStatus,
 | 
			
		||||
)
 | 
			
		||||
from tacticalrmm.helpers import notify_error
 | 
			
		||||
from tacticalrmm.helpers import make_random_password, notify_error
 | 
			
		||||
from tacticalrmm.utils import reload_nats
 | 
			
		||||
from winupdate.models import WinUpdate, WinUpdatePolicy
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CheckIn(APIView):
 | 
			
		||||
 | 
			
		||||
    authentication_classes = [TokenAuthentication]
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
@@ -254,9 +259,7 @@ class CheckRunner(APIView):
 | 
			
		||||
                check.check_result.last_run
 | 
			
		||||
                < djangotime.now()
 | 
			
		||||
                - djangotime.timedelta(
 | 
			
		||||
                    seconds=check.run_interval
 | 
			
		||||
                    if check.run_interval
 | 
			
		||||
                    else agent.check_interval
 | 
			
		||||
                    seconds=check.run_interval or agent.check_interval
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
        ]
 | 
			
		||||
@@ -340,6 +343,12 @@ class TaskRunner(APIView):
 | 
			
		||||
            AutomatedTask.objects.select_related("custom_field"), pk=pk
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        content_length = request.META.get("CONTENT_LENGTH")
 | 
			
		||||
        if content_length and int(content_length) > TRMM_MAX_REQUEST_SIZE:
 | 
			
		||||
            request.data["stdout"] = ""
 | 
			
		||||
            request.data["stderr"] = "Content truncated due to excessive request size."
 | 
			
		||||
            request.data["retcode"] = 1
 | 
			
		||||
 | 
			
		||||
        # get task result or create if doesn't exist
 | 
			
		||||
        try:
 | 
			
		||||
            task_result = (
 | 
			
		||||
@@ -358,7 +367,7 @@ class TaskRunner(APIView):
 | 
			
		||||
 | 
			
		||||
        AgentHistory.objects.create(
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            type=AuditActionType.TASK_RUN,
 | 
			
		||||
            type=AgentHistoryType.TASK_RUN,
 | 
			
		||||
            command=task.name,
 | 
			
		||||
            script_results=request.data,
 | 
			
		||||
        )
 | 
			
		||||
@@ -366,7 +375,6 @@ class TaskRunner(APIView):
 | 
			
		||||
        # check if task is a collector and update the custom field
 | 
			
		||||
        if task.custom_field:
 | 
			
		||||
            if not task_result.stderr:
 | 
			
		||||
 | 
			
		||||
                task_result.save_collector_results()
 | 
			
		||||
 | 
			
		||||
                status = CheckStatus.PASSING
 | 
			
		||||
@@ -429,8 +437,8 @@ class MeshExe(APIView):
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            return download_mesh_agent(dl_url)
 | 
			
		||||
        except:
 | 
			
		||||
            return notify_error("Unable to download mesh agent exe")
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            return notify_error(f"Unable to download mesh agent: {e}")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NewAgent(APIView):
 | 
			
		||||
@@ -460,7 +468,7 @@ class NewAgent(APIView):
 | 
			
		||||
        user = User.objects.create_user(  # type: ignore
 | 
			
		||||
            username=request.data["agent_id"],
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            password=User.objects.make_random_password(60),  # type: ignore
 | 
			
		||||
            password=make_random_password(len=60),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        token = Token.objects.create(user=user)
 | 
			
		||||
@@ -484,6 +492,8 @@ class NewAgent(APIView):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        ret = {"pk": agent.pk, "token": token.key}
 | 
			
		||||
        sync_mesh_perms_task.delay()
 | 
			
		||||
        cache_agents_alert_template.delay()
 | 
			
		||||
        return Response(ret)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -516,7 +526,7 @@ class Installer(APIView):
 | 
			
		||||
        ver = request.data["version"]
 | 
			
		||||
        if (
 | 
			
		||||
            pyver.parse(ver) < pyver.parse(settings.LATEST_AGENT_VER)
 | 
			
		||||
            and not "-dev" in settings.LATEST_AGENT_VER
 | 
			
		||||
            and "-dev" not in settings.LATEST_AGENT_VER
 | 
			
		||||
        ):
 | 
			
		||||
            return notify_error(
 | 
			
		||||
                f"Old installer detected (version {ver} ). Latest version is {settings.LATEST_AGENT_VER} Please generate a new installer from the RMM"
 | 
			
		||||
@@ -562,10 +572,56 @@ class AgentHistoryResult(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
    def patch(self, request, agentid, pk):
 | 
			
		||||
        content_length = request.META.get("CONTENT_LENGTH")
 | 
			
		||||
        if content_length and int(content_length) > TRMM_MAX_REQUEST_SIZE:
 | 
			
		||||
 | 
			
		||||
            request.data["script_results"]["stdout"] = ""
 | 
			
		||||
            request.data["script_results"][
 | 
			
		||||
                "stderr"
 | 
			
		||||
            ] = "Content truncated due to excessive request size."
 | 
			
		||||
            request.data["script_results"]["retcode"] = 1
 | 
			
		||||
 | 
			
		||||
        hist = get_object_or_404(
 | 
			
		||||
            AgentHistory.objects.filter(agent__agent_id=agentid), pk=pk
 | 
			
		||||
            AgentHistory.objects.select_related("custom_field").filter(
 | 
			
		||||
                agent__agent_id=agentid
 | 
			
		||||
            ),
 | 
			
		||||
            pk=pk,
 | 
			
		||||
        )
 | 
			
		||||
        s = AgentHistorySerializer(instance=hist, data=request.data, partial=True)
 | 
			
		||||
        s.is_valid(raise_exception=True)
 | 
			
		||||
        s.save()
 | 
			
		||||
 | 
			
		||||
        if hist.custom_field:
 | 
			
		||||
            if hist.custom_field.model == CustomFieldModel.AGENT:
 | 
			
		||||
                field = hist.custom_field.get_or_create_field_value(hist.agent)
 | 
			
		||||
            elif hist.custom_field.model == CustomFieldModel.CLIENT:
 | 
			
		||||
                field = hist.custom_field.get_or_create_field_value(hist.agent.client)
 | 
			
		||||
            elif hist.custom_field.model == CustomFieldModel.SITE:
 | 
			
		||||
                field = hist.custom_field.get_or_create_field_value(hist.agent.site)
 | 
			
		||||
 | 
			
		||||
            r = request.data["script_results"]["stdout"]
 | 
			
		||||
            value = (
 | 
			
		||||
                r.strip()
 | 
			
		||||
                if hist.collector_all_output
 | 
			
		||||
                else r.strip().split("\n")[-1].strip()
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            field.save_to_field(value)
 | 
			
		||||
 | 
			
		||||
        if hist.save_to_agent_note:
 | 
			
		||||
            Note.objects.create(
 | 
			
		||||
                agent=hist.agent,
 | 
			
		||||
                user=request.user,
 | 
			
		||||
                note=request.data["script_results"]["stdout"],
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AgentConfig(APIView):
 | 
			
		||||
    authentication_classes = [TokenAuthentication]
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, agentid):
 | 
			
		||||
        ret = get_agent_config()
 | 
			
		||||
        return Response(ret._to_dict())
 | 
			
		||||
 
 | 
			
		||||
@@ -47,7 +47,7 @@ class Policy(BaseAuditModel):
 | 
			
		||||
        old_policy: Optional[Policy] = (
 | 
			
		||||
            type(self).objects.get(pk=self.pk) if self.pk else None
 | 
			
		||||
        )
 | 
			
		||||
        super(Policy, self).save(old_model=old_policy, *args, **kwargs)
 | 
			
		||||
        super().save(old_model=old_policy, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
        # check if alert template was changes and cache on agents
 | 
			
		||||
        if old_policy:
 | 
			
		||||
@@ -68,10 +68,7 @@ class Policy(BaseAuditModel):
 | 
			
		||||
        cache.delete_many_pattern("site_server_*")
 | 
			
		||||
        cache.delete_many_pattern("agent_*")
 | 
			
		||||
 | 
			
		||||
        super(Policy, self).delete(
 | 
			
		||||
            *args,
 | 
			
		||||
            **kwargs,
 | 
			
		||||
        )
 | 
			
		||||
        super().delete(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def __str__(self) -> str:
 | 
			
		||||
        return self.name
 | 
			
		||||
@@ -216,16 +213,15 @@ class Policy(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def get_policy_tasks(agent: "Agent") -> "List[AutomatedTask]":
 | 
			
		||||
 | 
			
		||||
        # List of all tasks to be applied
 | 
			
		||||
        tasks = list()
 | 
			
		||||
        tasks = []
 | 
			
		||||
 | 
			
		||||
        # Get policies applied to agent and agent site and client
 | 
			
		||||
        policies = agent.get_agent_policies()
 | 
			
		||||
 | 
			
		||||
        processed_policies = list()
 | 
			
		||||
        processed_policies = []
 | 
			
		||||
 | 
			
		||||
        for _, policy in policies.items():
 | 
			
		||||
        for policy in policies.values():
 | 
			
		||||
            if policy and policy.active and policy.pk not in processed_policies:
 | 
			
		||||
                processed_policies.append(policy.pk)
 | 
			
		||||
                for task in policy.autotasks.all():
 | 
			
		||||
@@ -235,7 +231,6 @@ class Policy(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def get_policy_checks(agent: "Agent") -> "List[Check]":
 | 
			
		||||
 | 
			
		||||
        # Get checks added to agent directly
 | 
			
		||||
        agent_checks = list(agent.agentchecks.all())
 | 
			
		||||
 | 
			
		||||
@@ -244,12 +239,12 @@ class Policy(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
        # Used to hold the policies that will be applied and the order in which they are applied
 | 
			
		||||
        # Enforced policies are applied first
 | 
			
		||||
        enforced_checks = list()
 | 
			
		||||
        policy_checks = list()
 | 
			
		||||
        enforced_checks = []
 | 
			
		||||
        policy_checks = []
 | 
			
		||||
 | 
			
		||||
        processed_policies = list()
 | 
			
		||||
        processed_policies = []
 | 
			
		||||
 | 
			
		||||
        for _, policy in policies.items():
 | 
			
		||||
        for policy in policies.values():
 | 
			
		||||
            if policy and policy.active and policy.pk not in processed_policies:
 | 
			
		||||
                processed_policies.append(policy.pk)
 | 
			
		||||
                if policy.enforced:
 | 
			
		||||
@@ -263,24 +258,24 @@ class Policy(BaseAuditModel):
 | 
			
		||||
            return []
 | 
			
		||||
 | 
			
		||||
        # Sorted Checks already added
 | 
			
		||||
        added_diskspace_checks: List[str] = list()
 | 
			
		||||
        added_ping_checks: List[str] = list()
 | 
			
		||||
        added_winsvc_checks: List[str] = list()
 | 
			
		||||
        added_script_checks: List[int] = list()
 | 
			
		||||
        added_eventlog_checks: List[List[str]] = list()
 | 
			
		||||
        added_cpuload_checks: List[int] = list()
 | 
			
		||||
        added_memory_checks: List[int] = list()
 | 
			
		||||
        added_diskspace_checks: List[str] = []
 | 
			
		||||
        added_ping_checks: List[str] = []
 | 
			
		||||
        added_winsvc_checks: List[str] = []
 | 
			
		||||
        added_script_checks: List[int] = []
 | 
			
		||||
        added_eventlog_checks: List[List[str]] = []
 | 
			
		||||
        added_cpuload_checks: List[int] = []
 | 
			
		||||
        added_memory_checks: List[int] = []
 | 
			
		||||
 | 
			
		||||
        # Lists all agent and policy checks that will be returned
 | 
			
		||||
        diskspace_checks: "List[Check]" = list()
 | 
			
		||||
        ping_checks: "List[Check]" = list()
 | 
			
		||||
        winsvc_checks: "List[Check]" = list()
 | 
			
		||||
        script_checks: "List[Check]" = list()
 | 
			
		||||
        eventlog_checks: "List[Check]" = list()
 | 
			
		||||
        cpuload_checks: "List[Check]" = list()
 | 
			
		||||
        memory_checks: "List[Check]" = list()
 | 
			
		||||
        diskspace_checks: "List[Check]" = []
 | 
			
		||||
        ping_checks: "List[Check]" = []
 | 
			
		||||
        winsvc_checks: "List[Check]" = []
 | 
			
		||||
        script_checks: "List[Check]" = []
 | 
			
		||||
        eventlog_checks: "List[Check]" = []
 | 
			
		||||
        cpuload_checks: "List[Check]" = []
 | 
			
		||||
        memory_checks: "List[Check]" = []
 | 
			
		||||
 | 
			
		||||
        overridden_checks: List[int] = list()
 | 
			
		||||
        overridden_checks: List[int] = []
 | 
			
		||||
 | 
			
		||||
        # Loop over checks in with enforced policies first, then non-enforced policies
 | 
			
		||||
        for check in enforced_checks + agent_checks + policy_checks:
 | 
			
		||||
 
 | 
			
		||||
@@ -7,5 +7,5 @@ class AutomationPolicyPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view) -> bool:
 | 
			
		||||
        if r.method == "GET":
 | 
			
		||||
            return _has_perm(r, "can_list_automation_policies")
 | 
			
		||||
        else:
 | 
			
		||||
            return _has_perm(r, "can_manage_automation_policies")
 | 
			
		||||
 | 
			
		||||
        return _has_perm(r, "can_manage_automation_policies")
 | 
			
		||||
 
 | 
			
		||||
@@ -87,7 +87,7 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
            "copyId": policy.pk,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resp = self.client.post(f"/automation/policies/", data, format="json")
 | 
			
		||||
        resp = self.client.post("/automation/policies/", data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        copied_policy = Policy.objects.get(name=data["name"])
 | 
			
		||||
@@ -126,7 +126,7 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
        resp = self.client.put(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        cache_alert_template.called_once()
 | 
			
		||||
        cache_alert_template.assert_called_once()
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("put", url)
 | 
			
		||||
 | 
			
		||||
@@ -221,7 +221,6 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    def test_get_policy_task_status(self):
 | 
			
		||||
 | 
			
		||||
        # policy with a task
 | 
			
		||||
        policy = baker.make("automation.Policy")
 | 
			
		||||
        agent = baker.make_recipe("agents.agent", policy=policy)
 | 
			
		||||
@@ -240,7 +239,6 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
    @patch("automation.tasks.run_win_policy_autotasks_task.delay")
 | 
			
		||||
    def test_run_win_task(self, mock_task):
 | 
			
		||||
 | 
			
		||||
        policy = baker.make("automation.Policy")
 | 
			
		||||
        # create managed policy tasks
 | 
			
		||||
        task = baker.make_recipe("autotasks.task", policy=policy)
 | 
			
		||||
@@ -283,7 +281,6 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
    def test_update_patch_policy(self):
 | 
			
		||||
 | 
			
		||||
        # test policy doesn't exist
 | 
			
		||||
        resp = self.client.put("/automation/patchpolicy/500/", format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 404)
 | 
			
		||||
@@ -396,7 +393,6 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
        self.setup_coresettings()
 | 
			
		||||
 | 
			
		||||
    def test_policy_related(self):
 | 
			
		||||
 | 
			
		||||
        # Get Site and Client from an agent in list
 | 
			
		||||
        clients = baker.make("clients.Client", _quantity=5)
 | 
			
		||||
        sites = baker.make("clients.Site", client=cycle(clients), _quantity=25)
 | 
			
		||||
@@ -447,7 +443,6 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(len(resp.data["agents"]), 2)
 | 
			
		||||
 | 
			
		||||
    def test_getting_agent_policy_checks(self):
 | 
			
		||||
 | 
			
		||||
        # setup data
 | 
			
		||||
        policy = baker.make("automation.Policy", active=True)
 | 
			
		||||
        self.create_checks(parent=policy)
 | 
			
		||||
@@ -536,7 +531,6 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_policy_exclusions(self):
 | 
			
		||||
 | 
			
		||||
        # setup data
 | 
			
		||||
        policy = baker.make("automation.Policy", active=True)
 | 
			
		||||
        baker.make_recipe("checks.memory_check", policy=policy)
 | 
			
		||||
 
 | 
			
		||||
@@ -84,7 +84,6 @@ class GetUpdateDeletePolicy(APIView):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PolicyAutoTask(APIView):
 | 
			
		||||
 | 
			
		||||
    # get status of all tasks
 | 
			
		||||
    def get(self, request, task):
 | 
			
		||||
        tasks = TaskResult.objects.filter(task=task)
 | 
			
		||||
@@ -108,7 +107,6 @@ class PolicyCheck(APIView):
 | 
			
		||||
 | 
			
		||||
class OverviewPolicy(APIView):
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
 | 
			
		||||
        clients = (
 | 
			
		||||
            Client.objects.filter_by_role(request.user)
 | 
			
		||||
            .select_related("workstation_policy", "server_policy")
 | 
			
		||||
@@ -127,7 +125,6 @@ class OverviewPolicy(APIView):
 | 
			
		||||
 | 
			
		||||
class GetRelated(APIView):
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
 | 
			
		||||
        policy = (
 | 
			
		||||
            Policy.objects.filter(pk=pk)
 | 
			
		||||
            .prefetch_related(
 | 
			
		||||
@@ -146,6 +143,7 @@ class GetRelated(APIView):
 | 
			
		||||
 | 
			
		||||
class UpdatePatchPolicy(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, AutomationPolicyPerms]
 | 
			
		||||
 | 
			
		||||
    # create new patch policy
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        policy = get_object_or_404(Policy, pk=request.data["policy"])
 | 
			
		||||
@@ -179,7 +177,6 @@ class UpdatePatchPolicy(APIView):
 | 
			
		||||
class ResetPatchPolicy(APIView):
 | 
			
		||||
    # bulk reset agent patch policy
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
 | 
			
		||||
        if "client" in request.data:
 | 
			
		||||
            if not _has_perm_on_client(request.user, request.data["client"]):
 | 
			
		||||
                raise PermissionDenied()
 | 
			
		||||
 
 | 
			
		||||
@@ -7,10 +7,4 @@ class Command(BaseCommand):
 | 
			
		||||
    help = "Checks for orphaned tasks on all agents and removes them"
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args, **kwargs):
 | 
			
		||||
        remove_orphaned_win_tasks.s()
 | 
			
		||||
 | 
			
		||||
        self.stdout.write(
 | 
			
		||||
            self.style.SUCCESS(
 | 
			
		||||
                "The task has been initiated. Check the Debug Log in the UI for progress."
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
        remove_orphaned_win_tasks()
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,33 @@
 | 
			
		||||
from django.db import migrations
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def migrate_env_vars(apps, schema_editor):
 | 
			
		||||
    AutomatedTask = apps.get_model("autotasks", "AutomatedTask")
 | 
			
		||||
    for task in AutomatedTask.objects.iterator(chunk_size=30):
 | 
			
		||||
        try:
 | 
			
		||||
            tmp = []
 | 
			
		||||
            if isinstance(task.actions, list) and task.actions:
 | 
			
		||||
                for t in task.actions:
 | 
			
		||||
                    if isinstance(t, dict):
 | 
			
		||||
                        if t["type"] == "script":
 | 
			
		||||
                            try:
 | 
			
		||||
                                t["env_vars"]
 | 
			
		||||
                            except KeyError:
 | 
			
		||||
                                t["env_vars"] = []
 | 
			
		||||
                        tmp.append(t)
 | 
			
		||||
            if tmp:
 | 
			
		||||
                task.actions = tmp
 | 
			
		||||
                task.save(update_fields=["actions"])
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            print(f"ERROR: {e}")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("autotasks", "0037_alter_taskresult_retcode"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.RunPython(migrate_env_vars),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
# Generated by Django 4.2.7 on 2023-11-23 04:39
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('autotasks', '0038_add_missing_env_vars'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='automatedtask',
 | 
			
		||||
            name='task_type',
 | 
			
		||||
            field=models.CharField(choices=[('daily', 'Daily'), ('weekly', 'Weekly'), ('monthly', 'Monthly'), ('monthlydow', 'Monthly Day of Week'), ('checkfailure', 'On Check Failure'), ('manual', 'Manual'), ('runonce', 'Run Once'), ('onboarding', 'Onboarding'), ('scheduled', 'Scheduled')], default='manual', max_length=100),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -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 = [
 | 
			
		||||
        ("autotasks", "0039_alter_automatedtask_task_type"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="taskresult",
 | 
			
		||||
            name="id",
 | 
			
		||||
            field=models.BigAutoField(primary_key=True, serialize=False),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -1,9 +1,10 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
import logging
 | 
			
		||||
import random
 | 
			
		||||
import string
 | 
			
		||||
from contextlib import suppress
 | 
			
		||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
 | 
			
		||||
 | 
			
		||||
import pytz
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from django.core.validators import MaxValueValidator, MinValueValidator
 | 
			
		||||
from django.db import models
 | 
			
		||||
@@ -13,12 +14,11 @@ from django.db.utils import DatabaseError
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
 | 
			
		||||
from core.utils import get_core_settings
 | 
			
		||||
from logs.models import BaseAuditModel, DebugLog
 | 
			
		||||
from logs.models import BaseAuditModel
 | 
			
		||||
from tacticalrmm.constants import (
 | 
			
		||||
    FIELDS_TRIGGER_TASK_UPDATE_AGENT,
 | 
			
		||||
    POLICY_TASK_FIELDS_TO_COPY,
 | 
			
		||||
    AlertSeverity,
 | 
			
		||||
    DebugLogType,
 | 
			
		||||
    TaskStatus,
 | 
			
		||||
    TaskSyncStatus,
 | 
			
		||||
    TaskType,
 | 
			
		||||
@@ -30,6 +30,7 @@ if TYPE_CHECKING:
 | 
			
		||||
    from agents.models import Agent
 | 
			
		||||
    from checks.models import Check
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.helpers import has_script_actions, has_webhook
 | 
			
		||||
from tacticalrmm.models import PermissionQuerySet
 | 
			
		||||
from tacticalrmm.utils import (
 | 
			
		||||
    bitdays_to_string,
 | 
			
		||||
@@ -45,6 +46,9 @@ def generate_task_name() -> str:
 | 
			
		||||
    return "TacticalRMM_" + "".join(random.choice(chars) for i in range(35))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
logger = logging.getLogger("trmm")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AutomatedTask(BaseAuditModel):
 | 
			
		||||
    objects = PermissionQuerySet.as_manager()
 | 
			
		||||
 | 
			
		||||
@@ -70,7 +74,7 @@ class AutomatedTask(BaseAuditModel):
 | 
			
		||||
        on_delete=models.SET_NULL,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # format -> [{"type": "script", "script": 1, "name": "Script Name", "timeout": 90, "script_args": []}, {"type": "cmd", "command": "whoami", "timeout": 90}]
 | 
			
		||||
    # format -> [{"type": "script", "script": 1, "name": "Script Name", "timeout": 90, "script_args": [], "env_vars": []}, {"type": "cmd", "command": "whoami", "timeout": 90}]
 | 
			
		||||
    actions = JSONField(default=list)
 | 
			
		||||
    assigned_check = models.ForeignKey(
 | 
			
		||||
        "checks.Check",
 | 
			
		||||
@@ -141,7 +145,6 @@ class AutomatedTask(BaseAuditModel):
 | 
			
		||||
        return self.name
 | 
			
		||||
 | 
			
		||||
    def save(self, *args, **kwargs) -> None:
 | 
			
		||||
 | 
			
		||||
        # if task is a policy task clear cache on everything
 | 
			
		||||
        if self.policy:
 | 
			
		||||
            cache.delete_many_pattern("site_*_tasks")
 | 
			
		||||
@@ -149,7 +152,7 @@ class AutomatedTask(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
        # get old task if exists
 | 
			
		||||
        old_task = AutomatedTask.objects.get(pk=self.pk) if self.pk else None
 | 
			
		||||
        super(AutomatedTask, self).save(old_model=old_task, *args, **kwargs)
 | 
			
		||||
        super().save(old_model=old_task, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
        # check if fields were updated that require a sync to the agent and set status to notsynced
 | 
			
		||||
        if old_task:
 | 
			
		||||
@@ -167,16 +170,12 @@ class AutomatedTask(BaseAuditModel):
 | 
			
		||||
                        )
 | 
			
		||||
 | 
			
		||||
    def delete(self, *args, **kwargs):
 | 
			
		||||
 | 
			
		||||
        # if task is a policy task clear cache on everything
 | 
			
		||||
        if self.policy:
 | 
			
		||||
            cache.delete_many_pattern("site_*_tasks")
 | 
			
		||||
            cache.delete_many_pattern("agent_*_tasks")
 | 
			
		||||
 | 
			
		||||
        super(AutomatedTask, self).delete(
 | 
			
		||||
            *args,
 | 
			
		||||
            **kwargs,
 | 
			
		||||
        )
 | 
			
		||||
        super().delete(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def schedule(self) -> Optional[str]:
 | 
			
		||||
@@ -210,6 +209,9 @@ class AutomatedTask(BaseAuditModel):
 | 
			
		||||
            weeks = bitweeks_to_string(self.monthly_weeks_of_month)
 | 
			
		||||
            days = bitdays_to_string(self.run_time_bit_weekdays)
 | 
			
		||||
            return f"Runs on {months} on {weeks} on {days} at {run_time_nice}"
 | 
			
		||||
        elif self.task_type == TaskType.ONBOARDING:
 | 
			
		||||
            return "Onboarding: Runs once on task creation."
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def fields_that_trigger_task_update_on_agent(self) -> List[str]:
 | 
			
		||||
@@ -225,80 +227,68 @@ class AutomatedTask(BaseAuditModel):
 | 
			
		||||
    def create_policy_task(
 | 
			
		||||
        self, policy: "Policy", assigned_check: "Optional[Check]" = None
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        ### Copies certain properties on this task (self) to a new task and sets it to the supplied Policy
 | 
			
		||||
        fields_to_copy = POLICY_TASK_FIELDS_TO_COPY
 | 
			
		||||
 | 
			
		||||
        # Copies certain properties on this task (self) to a new task and sets it to the supplied Policy
 | 
			
		||||
        task = AutomatedTask.objects.create(
 | 
			
		||||
            policy=policy,
 | 
			
		||||
            assigned_check=assigned_check,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        for field in fields_to_copy:
 | 
			
		||||
        for field in POLICY_TASK_FIELDS_TO_COPY:
 | 
			
		||||
            setattr(task, field, getattr(self, field))
 | 
			
		||||
 | 
			
		||||
        task.save()
 | 
			
		||||
 | 
			
		||||
    # agent version >= 1.8.0
 | 
			
		||||
    def generate_nats_task_payload(
 | 
			
		||||
        self, agent: "Optional[Agent]" = None, editing: bool = False
 | 
			
		||||
    ) -> Dict[str, Any]:
 | 
			
		||||
    def generate_nats_task_payload(self) -> Dict[str, Any]:
 | 
			
		||||
        task = {
 | 
			
		||||
            "pk": self.pk,
 | 
			
		||||
            "type": "rmm",
 | 
			
		||||
            "name": self.win_task_name,
 | 
			
		||||
            "overwrite_task": editing,
 | 
			
		||||
            "overwrite_task": True,
 | 
			
		||||
            "enabled": self.enabled,
 | 
			
		||||
            "trigger": self.task_type
 | 
			
		||||
            if self.task_type != TaskType.CHECK_FAILURE
 | 
			
		||||
            else TaskType.MANUAL,
 | 
			
		||||
            "multiple_instances": self.task_instance_policy
 | 
			
		||||
            if self.task_instance_policy
 | 
			
		||||
            else 0,
 | 
			
		||||
            "delete_expired_task_after": self.remove_if_not_scheduled
 | 
			
		||||
            if self.expire_date
 | 
			
		||||
            else False,
 | 
			
		||||
            "start_when_available": self.run_asap_after_missed
 | 
			
		||||
            if self.task_type != TaskType.RUN_ONCE
 | 
			
		||||
            else True,
 | 
			
		||||
            "trigger": (
 | 
			
		||||
                self.task_type
 | 
			
		||||
                if self.task_type != TaskType.CHECK_FAILURE
 | 
			
		||||
                else TaskType.MANUAL
 | 
			
		||||
            ),
 | 
			
		||||
            "multiple_instances": self.task_instance_policy or 0,
 | 
			
		||||
            "delete_expired_task_after": (
 | 
			
		||||
                self.remove_if_not_scheduled if self.expire_date else False
 | 
			
		||||
            ),
 | 
			
		||||
            "start_when_available": (
 | 
			
		||||
                self.run_asap_after_missed
 | 
			
		||||
                if self.task_type != TaskType.RUN_ONCE
 | 
			
		||||
                else True
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if self.task_type in [
 | 
			
		||||
            TaskType.RUN_ONCE,
 | 
			
		||||
        if self.task_type in (
 | 
			
		||||
            TaskType.DAILY,
 | 
			
		||||
            TaskType.WEEKLY,
 | 
			
		||||
            TaskType.MONTHLY,
 | 
			
		||||
            TaskType.MONTHLY_DOW,
 | 
			
		||||
        ]:
 | 
			
		||||
            # set runonce task in future if creating and run_asap_after_missed is set
 | 
			
		||||
            if (
 | 
			
		||||
                not editing
 | 
			
		||||
                and self.task_type == TaskType.RUN_ONCE
 | 
			
		||||
                and self.run_asap_after_missed
 | 
			
		||||
                and agent
 | 
			
		||||
                and self.run_time_date
 | 
			
		||||
                < djangotime.now().astimezone(pytz.timezone(agent.timezone))
 | 
			
		||||
            ):
 | 
			
		||||
                self.run_time_date = (
 | 
			
		||||
                    djangotime.now() + djangotime.timedelta(minutes=5)
 | 
			
		||||
                ).astimezone(pytz.timezone(agent.timezone))
 | 
			
		||||
            TaskType.RUN_ONCE,
 | 
			
		||||
        ):
 | 
			
		||||
            if not self.run_time_date:
 | 
			
		||||
                self.run_time_date = djangotime.now()
 | 
			
		||||
 | 
			
		||||
            task["start_year"] = int(self.run_time_date.strftime("%Y"))
 | 
			
		||||
            task["start_month"] = int(self.run_time_date.strftime("%-m"))
 | 
			
		||||
            task["start_day"] = int(self.run_time_date.strftime("%-d"))
 | 
			
		||||
            task["start_hour"] = int(self.run_time_date.strftime("%-H"))
 | 
			
		||||
            task["start_min"] = int(self.run_time_date.strftime("%-M"))
 | 
			
		||||
            task["start_year"] = self.run_time_date.year
 | 
			
		||||
            task["start_month"] = self.run_time_date.month
 | 
			
		||||
            task["start_day"] = self.run_time_date.day
 | 
			
		||||
            task["start_hour"] = self.run_time_date.hour
 | 
			
		||||
            task["start_min"] = self.run_time_date.minute
 | 
			
		||||
 | 
			
		||||
            if self.expire_date:
 | 
			
		||||
                task["expire_year"] = int(self.expire_date.strftime("%Y"))
 | 
			
		||||
                task["expire_month"] = int(self.expire_date.strftime("%-m"))
 | 
			
		||||
                task["expire_day"] = int(self.expire_date.strftime("%-d"))
 | 
			
		||||
                task["expire_hour"] = int(self.expire_date.strftime("%-H"))
 | 
			
		||||
                task["expire_min"] = int(self.expire_date.strftime("%-M"))
 | 
			
		||||
                task["expire_year"] = self.expire_date.year
 | 
			
		||||
                task["expire_month"] = self.expire_date.month
 | 
			
		||||
                task["expire_day"] = self.expire_date.day
 | 
			
		||||
                task["expire_hour"] = self.expire_date.hour
 | 
			
		||||
                task["expire_min"] = self.expire_date.minute
 | 
			
		||||
 | 
			
		||||
            if self.random_task_delay:
 | 
			
		||||
                task["random_delay"] = convert_to_iso_duration(self.random_task_delay)
 | 
			
		||||
 | 
			
		||||
            if self.task_repetition_interval:
 | 
			
		||||
            if self.task_repetition_interval and self.task_repetition_duration:
 | 
			
		||||
                task["repetition_interval"] = convert_to_iso_duration(
 | 
			
		||||
                    self.task_repetition_interval
 | 
			
		||||
                )
 | 
			
		||||
@@ -315,7 +305,6 @@ class AutomatedTask(BaseAuditModel):
 | 
			
		||||
                task["days_of_week"] = self.run_time_bit_weekdays
 | 
			
		||||
 | 
			
		||||
            elif self.task_type == TaskType.MONTHLY:
 | 
			
		||||
 | 
			
		||||
                # check if "last day is configured"
 | 
			
		||||
                if self.monthly_days_of_month >= 0x80000000:
 | 
			
		||||
                    task["days_of_month"] = self.monthly_days_of_month - 0x80000000
 | 
			
		||||
@@ -347,27 +336,24 @@ class AutomatedTask(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
        nats_data = {
 | 
			
		||||
            "func": "schedtask",
 | 
			
		||||
            "schedtaskpayload": self.generate_nats_task_payload(agent),
 | 
			
		||||
            "schedtaskpayload": self.generate_nats_task_payload(),
 | 
			
		||||
        }
 | 
			
		||||
        logger.debug(nats_data)
 | 
			
		||||
 | 
			
		||||
        r = asyncio.run(task_result.agent.nats_cmd(nats_data, timeout=5))
 | 
			
		||||
        r = asyncio.run(task_result.agent.nats_cmd(nats_data, timeout=10))
 | 
			
		||||
 | 
			
		||||
        if r != "ok":
 | 
			
		||||
            task_result.sync_status = TaskSyncStatus.INITIAL
 | 
			
		||||
            task_result.save(update_fields=["sync_status"])
 | 
			
		||||
            DebugLog.warning(
 | 
			
		||||
                agent=agent,
 | 
			
		||||
                log_type=DebugLogType.AGENT_ISSUES,
 | 
			
		||||
                message=f"Unable to create scheduled task {self.name} on {task_result.agent.hostname}. It will be created when the agent checks in.",
 | 
			
		||||
            logger.error(
 | 
			
		||||
                f"Unable to create scheduled task {self.name} on {task_result.agent.hostname}: {r}"
 | 
			
		||||
            )
 | 
			
		||||
            return "timeout"
 | 
			
		||||
        else:
 | 
			
		||||
            task_result.sync_status = TaskSyncStatus.SYNCED
 | 
			
		||||
            task_result.save(update_fields=["sync_status"])
 | 
			
		||||
            DebugLog.info(
 | 
			
		||||
                agent=agent,
 | 
			
		||||
                log_type=DebugLogType.AGENT_ISSUES,
 | 
			
		||||
                message=f"{task_result.agent.hostname} task {self.name} was successfully created",
 | 
			
		||||
            logger.info(
 | 
			
		||||
                f"{task_result.agent.hostname} task {self.name} was successfully created."
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        return "ok"
 | 
			
		||||
@@ -386,27 +372,24 @@ class AutomatedTask(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
        nats_data = {
 | 
			
		||||
            "func": "schedtask",
 | 
			
		||||
            "schedtaskpayload": self.generate_nats_task_payload(editing=True),
 | 
			
		||||
            "schedtaskpayload": self.generate_nats_task_payload(),
 | 
			
		||||
        }
 | 
			
		||||
        logger.debug(nats_data)
 | 
			
		||||
 | 
			
		||||
        r = asyncio.run(task_result.agent.nats_cmd(nats_data, timeout=5))
 | 
			
		||||
        r = asyncio.run(task_result.agent.nats_cmd(nats_data, timeout=10))
 | 
			
		||||
 | 
			
		||||
        if r != "ok":
 | 
			
		||||
            task_result.sync_status = TaskSyncStatus.NOT_SYNCED
 | 
			
		||||
            task_result.save(update_fields=["sync_status"])
 | 
			
		||||
            DebugLog.warning(
 | 
			
		||||
                agent=agent,
 | 
			
		||||
                log_type=DebugLogType.AGENT_ISSUES,
 | 
			
		||||
                message=f"Unable to modify scheduled task {self.name} on {task_result.agent.hostname}({task_result.agent.agent_id}). It will try again on next agent checkin",
 | 
			
		||||
            logger.error(
 | 
			
		||||
                f"Unable to modify scheduled task {self.name} on {task_result.agent.hostname}: {r}"
 | 
			
		||||
            )
 | 
			
		||||
            return "timeout"
 | 
			
		||||
        else:
 | 
			
		||||
            task_result.sync_status = TaskSyncStatus.SYNCED
 | 
			
		||||
            task_result.save(update_fields=["sync_status"])
 | 
			
		||||
            DebugLog.info(
 | 
			
		||||
                agent=agent,
 | 
			
		||||
                log_type=DebugLogType.AGENT_ISSUES,
 | 
			
		||||
                message=f"{task_result.agent.hostname} task {self.name} was successfully modified",
 | 
			
		||||
            logger.info(
 | 
			
		||||
                f"{task_result.agent.hostname} task {self.name} was successfully modified."
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        return "ok"
 | 
			
		||||
@@ -432,25 +415,16 @@ class AutomatedTask(BaseAuditModel):
 | 
			
		||||
        if r != "ok" and "The system cannot find the file specified" not in r:
 | 
			
		||||
            task_result.sync_status = TaskSyncStatus.PENDING_DELETION
 | 
			
		||||
 | 
			
		||||
            try:
 | 
			
		||||
            with suppress(DatabaseError):
 | 
			
		||||
                task_result.save(update_fields=["sync_status"])
 | 
			
		||||
            except DatabaseError:
 | 
			
		||||
                pass
 | 
			
		||||
 | 
			
		||||
            DebugLog.warning(
 | 
			
		||||
                agent=agent,
 | 
			
		||||
                log_type=DebugLogType.AGENT_ISSUES,
 | 
			
		||||
                message=f"{task_result.agent.hostname} task {self.name} will be deleted on next checkin",
 | 
			
		||||
            logger.error(
 | 
			
		||||
                f"Unable to delete task {self.name} on {task_result.agent.hostname}: {r}"
 | 
			
		||||
            )
 | 
			
		||||
            return "timeout"
 | 
			
		||||
        else:
 | 
			
		||||
            self.delete()
 | 
			
		||||
            DebugLog.info(
 | 
			
		||||
                agent=agent,
 | 
			
		||||
                log_type=DebugLogType.AGENT_ISSUES,
 | 
			
		||||
                message=f"{task_result.agent.hostname}({task_result.agent.agent_id}) task {self.name} was deleted",
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            logger.info(f"{task_result.agent.hostname} task {self.name} was deleted.")
 | 
			
		||||
        return "ok"
 | 
			
		||||
 | 
			
		||||
    def run_win_task(self, agent: "Optional[Agent]" = None) -> str:
 | 
			
		||||
@@ -473,18 +447,19 @@ class AutomatedTask(BaseAuditModel):
 | 
			
		||||
        return "ok"
 | 
			
		||||
 | 
			
		||||
    def should_create_alert(self, alert_template=None):
 | 
			
		||||
        has_autotask_notification = (
 | 
			
		||||
            self.dashboard_alert or self.email_alert or self.text_alert
 | 
			
		||||
        )
 | 
			
		||||
        has_alert_template_notification = alert_template and (
 | 
			
		||||
            alert_template.task_always_alert
 | 
			
		||||
            or alert_template.task_always_email
 | 
			
		||||
            or alert_template.task_always_text
 | 
			
		||||
        )
 | 
			
		||||
        return (
 | 
			
		||||
            self.dashboard_alert
 | 
			
		||||
            or self.email_alert
 | 
			
		||||
            or self.text_alert
 | 
			
		||||
            or (
 | 
			
		||||
                alert_template
 | 
			
		||||
                and (
 | 
			
		||||
                    alert_template.task_always_alert
 | 
			
		||||
                    or alert_template.task_always_email
 | 
			
		||||
                    or alert_template.task_always_text
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            has_autotask_notification
 | 
			
		||||
            or has_alert_template_notification
 | 
			
		||||
            or has_webhook(alert_template, "task")
 | 
			
		||||
            or has_script_actions(alert_template, "task")
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -494,6 +469,7 @@ class TaskResult(models.Model):
 | 
			
		||||
 | 
			
		||||
    objects = PermissionQuerySet.as_manager()
 | 
			
		||||
 | 
			
		||||
    id = models.BigAutoField(primary_key=True)
 | 
			
		||||
    agent = models.ForeignKey(
 | 
			
		||||
        "agents.Agent",
 | 
			
		||||
        related_name="taskresults",
 | 
			
		||||
@@ -532,7 +508,6 @@ class TaskResult(models.Model):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def save_collector_results(self) -> None:
 | 
			
		||||
 | 
			
		||||
        agent_field = self.task.custom_field.get_or_create_field_value(self.agent)
 | 
			
		||||
 | 
			
		||||
        value = (
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,8 @@
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from rest_framework import serializers
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
 | 
			
		||||
from scripts.models import Script
 | 
			
		||||
from tacticalrmm.constants import TaskType
 | 
			
		||||
@@ -14,7 +18,6 @@ class TaskResultSerializer(serializers.ModelSerializer):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TaskSerializer(serializers.ModelSerializer):
 | 
			
		||||
 | 
			
		||||
    check_name = serializers.ReadOnlyField(source="assigned_check.readable_desc")
 | 
			
		||||
    schedule = serializers.ReadOnlyField()
 | 
			
		||||
    alert_template = serializers.SerializerMethodField()
 | 
			
		||||
@@ -30,52 +33,49 @@ class TaskSerializer(serializers.ModelSerializer):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def validate_actions(self, value):
 | 
			
		||||
 | 
			
		||||
        if not value:
 | 
			
		||||
            raise serializers.ValidationError(
 | 
			
		||||
                f"There must be at least one action configured"
 | 
			
		||||
                "There must be at least one action configured"
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        for action in value:
 | 
			
		||||
            if "type" not in action:
 | 
			
		||||
                raise serializers.ValidationError(
 | 
			
		||||
                    f"Each action must have a type field of either 'script' or 'cmd'"
 | 
			
		||||
                    "Each action must have a type field of either 'script' or 'cmd'"
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            if action["type"] == "script":
 | 
			
		||||
                if "script" not in action:
 | 
			
		||||
                    raise serializers.ValidationError(
 | 
			
		||||
                        f"A script action type must have a 'script' field with primary key of script"
 | 
			
		||||
                        "A script action type must have a 'script' field with primary key of script"
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
                if "script_args" not in action:
 | 
			
		||||
                    raise serializers.ValidationError(
 | 
			
		||||
                        f"A script action type must have a 'script_args' field with an array of arguments"
 | 
			
		||||
                        "A script action type must have a 'script_args' field with an array of arguments"
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
                if "timeout" not in action:
 | 
			
		||||
                    raise serializers.ValidationError(
 | 
			
		||||
                        f"A script action type must have a 'timeout' field"
 | 
			
		||||
                        "A script action type must have a 'timeout' field"
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
            if action["type"] == "cmd":
 | 
			
		||||
                if "command" not in action:
 | 
			
		||||
                    raise serializers.ValidationError(
 | 
			
		||||
                        f"A command action type must have a 'command' field"
 | 
			
		||||
                        "A command action type must have a 'command' field"
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
                if "timeout" not in action:
 | 
			
		||||
                    raise serializers.ValidationError(
 | 
			
		||||
                        f"A command action type must have a 'timeout' field"
 | 
			
		||||
                        "A command action type must have a 'timeout' field"
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
        return value
 | 
			
		||||
 | 
			
		||||
    def validate(self, data):
 | 
			
		||||
 | 
			
		||||
        # allow editing with task_type not specified
 | 
			
		||||
        if self.instance and "task_type" not in data:
 | 
			
		||||
 | 
			
		||||
            # remove schedule related fields from data
 | 
			
		||||
            if "run_time_date" in data:
 | 
			
		||||
                del data["run_time_date"]
 | 
			
		||||
@@ -97,16 +97,23 @@ class TaskSerializer(serializers.ModelSerializer):
 | 
			
		||||
                del data["assigned_check"]
 | 
			
		||||
            return data
 | 
			
		||||
 | 
			
		||||
        if (
 | 
			
		||||
            "expire_date" in data
 | 
			
		||||
            and isinstance(data["expire_date"], datetime)
 | 
			
		||||
            and djangotime.now() > data["expire_date"]
 | 
			
		||||
        ):
 | 
			
		||||
            raise serializers.ValidationError("Expires date/time is in the past")
 | 
			
		||||
 | 
			
		||||
        # run_time_date required
 | 
			
		||||
        if (
 | 
			
		||||
            data["task_type"]
 | 
			
		||||
            in [
 | 
			
		||||
            in (
 | 
			
		||||
                TaskType.RUN_ONCE,
 | 
			
		||||
                TaskType.DAILY,
 | 
			
		||||
                TaskType.WEEKLY,
 | 
			
		||||
                TaskType.MONTHLY,
 | 
			
		||||
                TaskType.MONTHLY_DOW,
 | 
			
		||||
            ]
 | 
			
		||||
            )
 | 
			
		||||
            and not data["run_time_date"]
 | 
			
		||||
        ):
 | 
			
		||||
            raise serializers.ValidationError(
 | 
			
		||||
@@ -180,7 +187,6 @@ class TaskSerializer(serializers.ModelSerializer):
 | 
			
		||||
        return data
 | 
			
		||||
 | 
			
		||||
    def get_alert_template(self, obj):
 | 
			
		||||
 | 
			
		||||
        if obj.agent:
 | 
			
		||||
            alert_template = obj.agent.alert_template
 | 
			
		||||
        else:
 | 
			
		||||
@@ -188,13 +194,12 @@ class TaskSerializer(serializers.ModelSerializer):
 | 
			
		||||
 | 
			
		||||
        if not alert_template:
 | 
			
		||||
            return None
 | 
			
		||||
        else:
 | 
			
		||||
            return {
 | 
			
		||||
                "name": alert_template.name,
 | 
			
		||||
                "always_email": alert_template.task_always_email,
 | 
			
		||||
                "always_text": alert_template.task_always_text,
 | 
			
		||||
                "always_alert": alert_template.task_always_alert,
 | 
			
		||||
            }
 | 
			
		||||
        return {
 | 
			
		||||
            "name": alert_template.name,
 | 
			
		||||
            "always_email": alert_template.task_always_email,
 | 
			
		||||
            "always_text": alert_template.task_always_text,
 | 
			
		||||
            "always_alert": alert_template.task_always_alert,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = AutomatedTask
 | 
			
		||||
@@ -229,6 +234,12 @@ class TaskGOGetSerializer(serializers.ModelSerializer):
 | 
			
		||||
                    # script doesn't exist so remove it
 | 
			
		||||
                    actions_to_remove.append(action["script"])
 | 
			
		||||
                    continue
 | 
			
		||||
                # wrote a custom migration for env_vars but leaving this just in case.
 | 
			
		||||
                # can be removed later
 | 
			
		||||
                try:
 | 
			
		||||
                    env_vars = action["env_vars"]
 | 
			
		||||
                except KeyError:
 | 
			
		||||
                    env_vars = []
 | 
			
		||||
                tmp.append(
 | 
			
		||||
                    {
 | 
			
		||||
                        "type": "script",
 | 
			
		||||
@@ -242,6 +253,13 @@ class TaskGOGetSerializer(serializers.ModelSerializer):
 | 
			
		||||
                        "shell": script.shell,
 | 
			
		||||
                        "timeout": action["timeout"],
 | 
			
		||||
                        "run_as_user": script.run_as_user,
 | 
			
		||||
                        "env_vars": Script.parse_script_env_vars(
 | 
			
		||||
                            agent=agent,
 | 
			
		||||
                            shell=script.shell,
 | 
			
		||||
                            env_vars=env_vars,
 | 
			
		||||
                        ),
 | 
			
		||||
                        "nushell_enable_config": settings.NUSHELL_ENABLE_CONFIG,
 | 
			
		||||
                        "deno_default_permissions": settings.DENO_DEFAULT_PERMISSIONS,
 | 
			
		||||
                    }
 | 
			
		||||
                )
 | 
			
		||||
        if actions_to_remove:
 | 
			
		||||
 
 | 
			
		||||
@@ -1,134 +1,163 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
import datetime as dt
 | 
			
		||||
import random
 | 
			
		||||
from collections import namedtuple
 | 
			
		||||
from contextlib import suppress
 | 
			
		||||
from time import sleep
 | 
			
		||||
from typing import Optional, Union
 | 
			
		||||
from typing import TYPE_CHECKING, Optional, Union
 | 
			
		||||
 | 
			
		||||
import msgpack
 | 
			
		||||
import nats
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from nats.errors import TimeoutError
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from alerts.models import Alert
 | 
			
		||||
from autotasks.models import AutomatedTask, TaskResult
 | 
			
		||||
from logs.models import DebugLog
 | 
			
		||||
from tacticalrmm.celery import app
 | 
			
		||||
from tacticalrmm.constants import DebugLogType
 | 
			
		||||
from tacticalrmm.constants import AGENT_STATUS_ONLINE, ORPHANED_WIN_TASK_LOCK
 | 
			
		||||
from tacticalrmm.helpers import rand_range, setup_nats_options
 | 
			
		||||
from tacticalrmm.utils import redis_lock
 | 
			
		||||
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
    from nats.aio.client import Client as NATSClient
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def create_win_task_schedule(pk: int, agent_id: Optional[str] = None) -> str:
 | 
			
		||||
    try:
 | 
			
		||||
    with suppress(
 | 
			
		||||
        AutomatedTask.DoesNotExist,
 | 
			
		||||
        Agent.DoesNotExist,
 | 
			
		||||
    ):
 | 
			
		||||
        task = AutomatedTask.objects.get(pk=pk)
 | 
			
		||||
 | 
			
		||||
        if agent_id:
 | 
			
		||||
            task.create_task_on_agent(Agent.objects.get(agent_id=agent_id))
 | 
			
		||||
        else:
 | 
			
		||||
            task.create_task_on_agent()
 | 
			
		||||
    except (AutomatedTask.DoesNotExist, Agent.DoesNotExist):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    return "ok"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def modify_win_task(pk: int, agent_id: Optional[str] = None) -> str:
 | 
			
		||||
    try:
 | 
			
		||||
    with suppress(
 | 
			
		||||
        AutomatedTask.DoesNotExist,
 | 
			
		||||
        Agent.DoesNotExist,
 | 
			
		||||
    ):
 | 
			
		||||
        task = AutomatedTask.objects.get(pk=pk)
 | 
			
		||||
 | 
			
		||||
        if agent_id:
 | 
			
		||||
            task.modify_task_on_agent(Agent.objects.get(agent_id=agent_id))
 | 
			
		||||
        else:
 | 
			
		||||
            task.modify_task_on_agent()
 | 
			
		||||
    except (AutomatedTask.DoesNotExist, Agent.DoesNotExist):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    return "ok"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def delete_win_task_schedule(pk: int, agent_id: Optional[str] = None) -> str:
 | 
			
		||||
    try:
 | 
			
		||||
    with suppress(
 | 
			
		||||
        AutomatedTask.DoesNotExist,
 | 
			
		||||
        Agent.DoesNotExist,
 | 
			
		||||
    ):
 | 
			
		||||
        task = AutomatedTask.objects.get(pk=pk)
 | 
			
		||||
 | 
			
		||||
        if agent_id:
 | 
			
		||||
            task.delete_task_on_agent(Agent.objects.get(agent_id=agent_id))
 | 
			
		||||
        else:
 | 
			
		||||
            task.delete_task_on_agent()
 | 
			
		||||
    except (AutomatedTask.DoesNotExist, Agent.DoesNotExist):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    return "ok"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def run_win_task(pk: int, agent_id: Optional[str] = None) -> str:
 | 
			
		||||
    try:
 | 
			
		||||
    with suppress(
 | 
			
		||||
        AutomatedTask.DoesNotExist,
 | 
			
		||||
        Agent.DoesNotExist,
 | 
			
		||||
    ):
 | 
			
		||||
        task = AutomatedTask.objects.get(pk=pk)
 | 
			
		||||
 | 
			
		||||
        if agent_id:
 | 
			
		||||
            task.run_win_task(Agent.objects.get(agent_id=agent_id))
 | 
			
		||||
        else:
 | 
			
		||||
            task.run_win_task()
 | 
			
		||||
    except (AutomatedTask.DoesNotExist, Agent.DoesNotExist):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    return "ok"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def remove_orphaned_win_tasks() -> None:
 | 
			
		||||
    from agents.models import Agent
 | 
			
		||||
@app.task(bind=True)
 | 
			
		||||
def remove_orphaned_win_tasks(self) -> str:
 | 
			
		||||
    with redis_lock(ORPHANED_WIN_TASK_LOCK, self.app.oid) as acquired:
 | 
			
		||||
        if not acquired:
 | 
			
		||||
            return f"{self.app.oid} still running"
 | 
			
		||||
 | 
			
		||||
    for agent in Agent.online_agents():
 | 
			
		||||
        r = asyncio.run(agent.nats_cmd({"func": "listschedtasks"}, timeout=10))
 | 
			
		||||
        from core.tasks import _get_agent_qs
 | 
			
		||||
 | 
			
		||||
        if not isinstance(r, list):  # empty list
 | 
			
		||||
            DebugLog.error(
 | 
			
		||||
                agent=agent,
 | 
			
		||||
                log_type=DebugLogType.AGENT_ISSUES,
 | 
			
		||||
                message=f"Unable to pull list of scheduled tasks on {agent.hostname}: {r}",
 | 
			
		||||
            )
 | 
			
		||||
            continue
 | 
			
		||||
        AgentTup = namedtuple("AgentTup", ["agent_id", "task_names"])
 | 
			
		||||
        items: "list[AgentTup]" = []
 | 
			
		||||
        exclude_tasks = ("TacticalRMM_SchedReboot",)
 | 
			
		||||
 | 
			
		||||
        agent_task_names = [
 | 
			
		||||
            task.win_task_name for task in agent.get_tasks_with_policies()
 | 
			
		||||
        ]
 | 
			
		||||
        for agent in _get_agent_qs():
 | 
			
		||||
            if agent.status == AGENT_STATUS_ONLINE:
 | 
			
		||||
                names = [task.win_task_name for task in agent.get_tasks_with_policies()]
 | 
			
		||||
                items.append(AgentTup._make([agent.agent_id, names]))
 | 
			
		||||
 | 
			
		||||
        exclude_tasks = (
 | 
			
		||||
            "TacticalRMM_fixmesh",
 | 
			
		||||
            "TacticalRMM_SchedReboot",
 | 
			
		||||
            "TacticalRMM_sync",
 | 
			
		||||
            "TacticalRMM_agentupdate",
 | 
			
		||||
        )
 | 
			
		||||
        async def _handle_task(nc: "NATSClient", sub, data, names) -> str:
 | 
			
		||||
            try:
 | 
			
		||||
                msg = await nc.request(
 | 
			
		||||
                    subject=sub, payload=msgpack.dumps(data), timeout=5
 | 
			
		||||
                )
 | 
			
		||||
            except TimeoutError:
 | 
			
		||||
                return "timeout"
 | 
			
		||||
 | 
			
		||||
        for task in r:
 | 
			
		||||
            if task.startswith(exclude_tasks):
 | 
			
		||||
                # skip system tasks or any pending reboots
 | 
			
		||||
                continue
 | 
			
		||||
            try:
 | 
			
		||||
                r = msgpack.loads(msg.data)
 | 
			
		||||
            except Exception as e:
 | 
			
		||||
                return str(e)
 | 
			
		||||
 | 
			
		||||
            if task.startswith("TacticalRMM_") and task not in agent_task_names:
 | 
			
		||||
                # delete task since it doesn't exist in UI
 | 
			
		||||
                nats_data = {
 | 
			
		||||
                    "func": "delschedtask",
 | 
			
		||||
                    "schedtaskpayload": {"name": task},
 | 
			
		||||
                }
 | 
			
		||||
                ret = asyncio.run(agent.nats_cmd(nats_data, timeout=10))
 | 
			
		||||
                if ret != "ok":
 | 
			
		||||
                    DebugLog.error(
 | 
			
		||||
                        agent=agent,
 | 
			
		||||
                        log_type=DebugLogType.AGENT_ISSUES,
 | 
			
		||||
                        message=f"Unable to clean up orphaned task {task} on {agent.hostname}: {ret}",
 | 
			
		||||
                    )
 | 
			
		||||
                else:
 | 
			
		||||
                    DebugLog.info(
 | 
			
		||||
                        agent=agent,
 | 
			
		||||
                        log_type=DebugLogType.AGENT_ISSUES,
 | 
			
		||||
                        message=f"Removed orphaned task {task} from {agent.hostname}",
 | 
			
		||||
                    )
 | 
			
		||||
            if not isinstance(r, list):
 | 
			
		||||
                return "notlist"
 | 
			
		||||
 | 
			
		||||
            for name in r:
 | 
			
		||||
                if name.startswith(exclude_tasks):
 | 
			
		||||
                    # skip system tasks or any pending reboots
 | 
			
		||||
                    continue
 | 
			
		||||
 | 
			
		||||
                if name.startswith("TacticalRMM_") and name not in names:
 | 
			
		||||
                    nats_data = {
 | 
			
		||||
                        "func": "delschedtask",
 | 
			
		||||
                        "schedtaskpayload": {"name": name},
 | 
			
		||||
                    }
 | 
			
		||||
                    print(f"Deleting orphaned task: {name} on agent {sub}")
 | 
			
		||||
                    await nc.publish(subject=sub, payload=msgpack.dumps(nats_data))
 | 
			
		||||
 | 
			
		||||
            return "ok"
 | 
			
		||||
 | 
			
		||||
        async def _run() -> None:
 | 
			
		||||
            opts = setup_nats_options()
 | 
			
		||||
            try:
 | 
			
		||||
                nc = await nats.connect(**opts)
 | 
			
		||||
            except Exception as e:
 | 
			
		||||
                return str(e)
 | 
			
		||||
 | 
			
		||||
            payload = {"func": "listschedtasks"}
 | 
			
		||||
            tasks = [
 | 
			
		||||
                _handle_task(
 | 
			
		||||
                    nc=nc, sub=item.agent_id, data=payload, names=item.task_names
 | 
			
		||||
                )
 | 
			
		||||
                for item in items
 | 
			
		||||
            ]
 | 
			
		||||
            await asyncio.gather(*tasks)
 | 
			
		||||
            await nc.flush()
 | 
			
		||||
            await nc.close()
 | 
			
		||||
 | 
			
		||||
        asyncio.run(_run())
 | 
			
		||||
        return "completed"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def handle_task_email_alert(pk: int, alert_interval: Union[float, None] = None) -> str:
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        alert = Alert.objects.get(pk=pk)
 | 
			
		||||
    except Alert.DoesNotExist:
 | 
			
		||||
@@ -139,7 +168,7 @@ def handle_task_email_alert(pk: int, alert_interval: Union[float, None] = None)
 | 
			
		||||
        task_result = TaskResult.objects.get(
 | 
			
		||||
            task=alert.assigned_task, agent=alert.agent
 | 
			
		||||
        )
 | 
			
		||||
        sleep(random.randint(1, 5))
 | 
			
		||||
        sleep(rand_range(100, 1500))
 | 
			
		||||
        task_result.send_email()
 | 
			
		||||
        alert.email_sent = djangotime.now()
 | 
			
		||||
        alert.save(update_fields=["email_sent"])
 | 
			
		||||
@@ -151,7 +180,7 @@ def handle_task_email_alert(pk: int, alert_interval: Union[float, None] = None)
 | 
			
		||||
                task_result = TaskResult.objects.get(
 | 
			
		||||
                    task=alert.assigned_task, agent=alert.agent
 | 
			
		||||
                )
 | 
			
		||||
                sleep(random.randint(1, 5))
 | 
			
		||||
                sleep(rand_range(100, 1500))
 | 
			
		||||
                task_result.send_email()
 | 
			
		||||
                alert.email_sent = djangotime.now()
 | 
			
		||||
                alert.save(update_fields=["email_sent"])
 | 
			
		||||
@@ -161,7 +190,6 @@ def handle_task_email_alert(pk: int, alert_interval: Union[float, None] = None)
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def handle_task_sms_alert(pk: int, alert_interval: Union[float, None] = None) -> str:
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        alert = Alert.objects.get(pk=pk)
 | 
			
		||||
    except Alert.DoesNotExist:
 | 
			
		||||
@@ -172,7 +200,7 @@ def handle_task_sms_alert(pk: int, alert_interval: Union[float, None] = None) ->
 | 
			
		||||
        task_result = TaskResult.objects.get(
 | 
			
		||||
            task=alert.assigned_task, agent=alert.agent
 | 
			
		||||
        )
 | 
			
		||||
        sleep(random.randint(1, 3))
 | 
			
		||||
        sleep(rand_range(100, 1500))
 | 
			
		||||
        task_result.send_sms()
 | 
			
		||||
        alert.sms_sent = djangotime.now()
 | 
			
		||||
        alert.save(update_fields=["sms_sent"])
 | 
			
		||||
@@ -184,7 +212,7 @@ def handle_task_sms_alert(pk: int, alert_interval: Union[float, None] = None) ->
 | 
			
		||||
                task_result = TaskResult.objects.get(
 | 
			
		||||
                    task=alert.assigned_task, agent=alert.agent
 | 
			
		||||
                )
 | 
			
		||||
                sleep(random.randint(1, 3))
 | 
			
		||||
                sleep(rand_range(100, 1500))
 | 
			
		||||
                task_result.send_sms()
 | 
			
		||||
                alert.sms_sent = djangotime.now()
 | 
			
		||||
                alert.save(update_fields=["sms_sent"])
 | 
			
		||||
@@ -194,7 +222,6 @@ def handle_task_sms_alert(pk: int, alert_interval: Union[float, None] = None) ->
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def handle_resolved_task_sms_alert(pk: int) -> str:
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        alert = Alert.objects.get(pk=pk)
 | 
			
		||||
    except Alert.DoesNotExist:
 | 
			
		||||
@@ -205,7 +232,7 @@ def handle_resolved_task_sms_alert(pk: int) -> str:
 | 
			
		||||
        task_result = TaskResult.objects.get(
 | 
			
		||||
            task=alert.assigned_task, agent=alert.agent
 | 
			
		||||
        )
 | 
			
		||||
        sleep(random.randint(1, 3))
 | 
			
		||||
        sleep(rand_range(100, 1500))
 | 
			
		||||
        task_result.send_resolved_sms()
 | 
			
		||||
        alert.resolved_sms_sent = djangotime.now()
 | 
			
		||||
        alert.save(update_fields=["resolved_sms_sent"])
 | 
			
		||||
@@ -215,7 +242,6 @@ def handle_resolved_task_sms_alert(pk: int) -> str:
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def handle_resolved_task_email_alert(pk: int) -> str:
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        alert = Alert.objects.get(pk=pk)
 | 
			
		||||
    except Alert.DoesNotExist:
 | 
			
		||||
@@ -226,7 +252,7 @@ def handle_resolved_task_email_alert(pk: int) -> str:
 | 
			
		||||
        task_result = TaskResult.objects.get(
 | 
			
		||||
            task=alert.assigned_task, agent=alert.agent
 | 
			
		||||
        )
 | 
			
		||||
        sleep(random.randint(1, 5))
 | 
			
		||||
        sleep(rand_range(100, 1500))
 | 
			
		||||
        task_result.send_resolved_email()
 | 
			
		||||
        alert.resolved_email_sent = djangotime.now()
 | 
			
		||||
        alert.save(update_fields=["resolved_email_sent"])
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
from unittest.mock import call, patch
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from model_bakery import baker
 | 
			
		||||
@@ -8,7 +8,7 @@ from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
 | 
			
		||||
from .models import AutomatedTask, TaskResult, TaskSyncStatus
 | 
			
		||||
from .serializers import TaskSerializer
 | 
			
		||||
from .tasks import create_win_task_schedule, remove_orphaned_win_tasks, run_win_task
 | 
			
		||||
from .tasks import create_win_task_schedule, run_win_task
 | 
			
		||||
 | 
			
		||||
base_url = "/tasks"
 | 
			
		||||
 | 
			
		||||
@@ -51,7 +51,7 @@ class TestAutotaskViews(TacticalTestCase):
 | 
			
		||||
        # setup data
 | 
			
		||||
        script = baker.make_recipe("scripts.script")
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        policy = baker.make("automation.Policy")
 | 
			
		||||
        policy = baker.make("automation.Policy")  # noqa
 | 
			
		||||
        check = baker.make_recipe("checks.diskspace_check", agent=agent)
 | 
			
		||||
        custom_field = baker.make("core.CustomField")
 | 
			
		||||
 | 
			
		||||
@@ -137,7 +137,7 @@ class TestAutotaskViews(TacticalTestCase):
 | 
			
		||||
            "weekly_interval": 2,
 | 
			
		||||
            "run_time_bit_weekdays": 26,
 | 
			
		||||
            "run_time_date": djangotime.now(),
 | 
			
		||||
            "expire_date": djangotime.now(),
 | 
			
		||||
            "expire_date": djangotime.now() + djangotime.timedelta(weeks=5),
 | 
			
		||||
            "repetition_interval": "30S",
 | 
			
		||||
            "repetition_duration": "1H",
 | 
			
		||||
            "random_task_delay": "5M",
 | 
			
		||||
@@ -160,7 +160,7 @@ class TestAutotaskViews(TacticalTestCase):
 | 
			
		||||
            "monthly_months_of_year": 56,
 | 
			
		||||
            "monthly_days_of_month": 350,
 | 
			
		||||
            "run_time_date": djangotime.now(),
 | 
			
		||||
            "expire_date": djangotime.now(),
 | 
			
		||||
            "expire_date": djangotime.now() + djangotime.timedelta(weeks=5),
 | 
			
		||||
            "repetition_interval": "30S",
 | 
			
		||||
            "repetition_duration": "1H",
 | 
			
		||||
            "random_task_delay": "5M",
 | 
			
		||||
@@ -183,7 +183,7 @@ class TestAutotaskViews(TacticalTestCase):
 | 
			
		||||
            "monthly_weeks_of_month": 4,
 | 
			
		||||
            "run_time_bit_weekdays": 15,
 | 
			
		||||
            "run_time_date": djangotime.now(),
 | 
			
		||||
            "expire_date": djangotime.now(),
 | 
			
		||||
            "expire_date": djangotime.now() + djangotime.timedelta(weeks=5),
 | 
			
		||||
            "repetition_interval": "30S",
 | 
			
		||||
            "repetition_duration": "1H",
 | 
			
		||||
            "random_task_delay": "5M",
 | 
			
		||||
@@ -206,7 +206,7 @@ class TestAutotaskViews(TacticalTestCase):
 | 
			
		||||
            "monthly_weeks_of_month": 4,
 | 
			
		||||
            "run_time_bit_weekdays": 15,
 | 
			
		||||
            "run_time_date": djangotime.now(),
 | 
			
		||||
            "expire_date": djangotime.now(),
 | 
			
		||||
            "expire_date": djangotime.now() + djangotime.timedelta(weeks=5),
 | 
			
		||||
            "repetition_interval": "30S",
 | 
			
		||||
            "repetition_duration": "1H",
 | 
			
		||||
            "random_task_delay": "5M",
 | 
			
		||||
@@ -238,7 +238,6 @@ class TestAutotaskViews(TacticalTestCase):
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
    def test_get_autotask(self):
 | 
			
		||||
 | 
			
		||||
        # setup data
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        task = baker.make("autotasks.AutomatedTask", agent=agent)
 | 
			
		||||
@@ -258,7 +257,9 @@ class TestAutotaskViews(TacticalTestCase):
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        agent_task = baker.make("autotasks.AutomatedTask", agent=agent)
 | 
			
		||||
        policy = baker.make("automation.Policy")
 | 
			
		||||
        policy_task = baker.make("autotasks.AutomatedTask", enabled=True, policy=policy)
 | 
			
		||||
        policy_task = baker.make(  # noqa
 | 
			
		||||
            "autotasks.AutomatedTask", enabled=True, policy=policy
 | 
			
		||||
        )
 | 
			
		||||
        custom_field = baker.make("core.CustomField")
 | 
			
		||||
        script = baker.make("scripts.Script")
 | 
			
		||||
 | 
			
		||||
@@ -294,7 +295,7 @@ class TestAutotaskViews(TacticalTestCase):
 | 
			
		||||
            "monthly_weeks_of_month": 4,
 | 
			
		||||
            "run_time_bit_weekdays": 15,
 | 
			
		||||
            "run_time_date": djangotime.now(),
 | 
			
		||||
            "expire_date": djangotime.now(),
 | 
			
		||||
            "expire_date": djangotime.now() + djangotime.timedelta(weeks=5),
 | 
			
		||||
            "repetition_interval": "30S",
 | 
			
		||||
            "repetition_duration": "1H",
 | 
			
		||||
            "random_task_delay": "5M",
 | 
			
		||||
@@ -314,7 +315,7 @@ class TestAutotaskViews(TacticalTestCase):
 | 
			
		||||
            "monthly_weeks_of_month": 4,
 | 
			
		||||
            "run_time_bit_weekdays": 15,
 | 
			
		||||
            "run_time_date": djangotime.now(),
 | 
			
		||||
            "expire_date": djangotime.now(),
 | 
			
		||||
            "expire_date": djangotime.now() + djangotime.timedelta(weeks=5),
 | 
			
		||||
            "repetition_interval": "30S",
 | 
			
		||||
            "repetition_duration": "1H",
 | 
			
		||||
            "random_task_delay": "5M",
 | 
			
		||||
@@ -381,60 +382,6 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        self.setup_coresettings()
 | 
			
		||||
 | 
			
		||||
    @patch("agents.models.Agent.nats_cmd")
 | 
			
		||||
    def test_remove_orphaned_win_task(self, nats_cmd):
 | 
			
		||||
        agent = baker.make_recipe("agents.online_agent")
 | 
			
		||||
        baker.make_recipe("agents.offline_agent")
 | 
			
		||||
        task1 = AutomatedTask.objects.create(
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            name="test task 1",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # test removing an orphaned task
 | 
			
		||||
        win_tasks = [
 | 
			
		||||
            "Adobe Acrobat Update Task",
 | 
			
		||||
            "AdobeGCInvoker-1.0",
 | 
			
		||||
            "GoogleUpdateTaskMachineCore",
 | 
			
		||||
            "GoogleUpdateTaskMachineUA",
 | 
			
		||||
            "OneDrive Standalone Update Task-S-1-5-21-717461175-241712648-1206041384-1001",
 | 
			
		||||
            task1.win_task_name,
 | 
			
		||||
            "TacticalRMM_fixmesh",
 | 
			
		||||
            "TacticalRMM_SchedReboot_jk324kajd",
 | 
			
		||||
            "TacticalRMM_iggrLcOaldIZnUzLuJWPLNwikiOoJJHHznb",  # orphaned task
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        calls = [
 | 
			
		||||
            call({"func": "listschedtasks"}, timeout=10),
 | 
			
		||||
            call(
 | 
			
		||||
                {
 | 
			
		||||
                    "func": "delschedtask",
 | 
			
		||||
                    "schedtaskpayload": {
 | 
			
		||||
                        "name": "TacticalRMM_iggrLcOaldIZnUzLuJWPLNwikiOoJJHHznb"
 | 
			
		||||
                    },
 | 
			
		||||
                },
 | 
			
		||||
                timeout=10,
 | 
			
		||||
            ),
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        nats_cmd.side_effect = [win_tasks, "ok"]
 | 
			
		||||
        remove_orphaned_win_tasks()
 | 
			
		||||
        self.assertEqual(nats_cmd.call_count, 2)
 | 
			
		||||
        nats_cmd.assert_has_calls(calls)
 | 
			
		||||
 | 
			
		||||
        # test nats delete task fail
 | 
			
		||||
        nats_cmd.reset_mock()
 | 
			
		||||
        nats_cmd.side_effect = [win_tasks, "error deleting task"]
 | 
			
		||||
        remove_orphaned_win_tasks()
 | 
			
		||||
        nats_cmd.assert_has_calls(calls)
 | 
			
		||||
        self.assertEqual(nats_cmd.call_count, 2)
 | 
			
		||||
 | 
			
		||||
        # no orphaned tasks
 | 
			
		||||
        nats_cmd.reset_mock()
 | 
			
		||||
        win_tasks.remove("TacticalRMM_iggrLcOaldIZnUzLuJWPLNwikiOoJJHHznb")
 | 
			
		||||
        nats_cmd.side_effect = [win_tasks, "ok"]
 | 
			
		||||
        remove_orphaned_win_tasks()
 | 
			
		||||
        self.assertEqual(nats_cmd.call_count, 1)
 | 
			
		||||
 | 
			
		||||
    @patch("agents.models.Agent.nats_cmd")
 | 
			
		||||
    def test_run_win_task(self, nats_cmd):
 | 
			
		||||
        self.agent = baker.make_recipe("agents.agent")
 | 
			
		||||
@@ -470,7 +417,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
 | 
			
		||||
                    "pk": task1.pk,
 | 
			
		||||
                    "type": "rmm",
 | 
			
		||||
                    "name": task1.win_task_name,
 | 
			
		||||
                    "overwrite_task": False,
 | 
			
		||||
                    "overwrite_task": True,
 | 
			
		||||
                    "enabled": True,
 | 
			
		||||
                    "trigger": "daily",
 | 
			
		||||
                    "multiple_instances": 1,
 | 
			
		||||
@@ -484,7 +431,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
 | 
			
		||||
                    "day_interval": 1,
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            timeout=5,
 | 
			
		||||
            timeout=10,
 | 
			
		||||
        )
 | 
			
		||||
        nats_cmd.reset_mock()
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
@@ -523,7 +470,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
 | 
			
		||||
                    "pk": task1.pk,
 | 
			
		||||
                    "type": "rmm",
 | 
			
		||||
                    "name": task1.win_task_name,
 | 
			
		||||
                    "overwrite_task": False,
 | 
			
		||||
                    "overwrite_task": True,
 | 
			
		||||
                    "enabled": True,
 | 
			
		||||
                    "trigger": "weekly",
 | 
			
		||||
                    "multiple_instances": 2,
 | 
			
		||||
@@ -543,7 +490,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
 | 
			
		||||
                    "days_of_week": 127,
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            timeout=5,
 | 
			
		||||
            timeout=10,
 | 
			
		||||
        )
 | 
			
		||||
        nats_cmd.reset_mock()
 | 
			
		||||
 | 
			
		||||
@@ -571,7 +518,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
 | 
			
		||||
                    "pk": task1.pk,
 | 
			
		||||
                    "type": "rmm",
 | 
			
		||||
                    "name": task1.win_task_name,
 | 
			
		||||
                    "overwrite_task": False,
 | 
			
		||||
                    "overwrite_task": True,
 | 
			
		||||
                    "enabled": True,
 | 
			
		||||
                    "trigger": "monthly",
 | 
			
		||||
                    "multiple_instances": 1,
 | 
			
		||||
@@ -591,7 +538,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
 | 
			
		||||
                    "months_of_year": 1024,
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            timeout=5,
 | 
			
		||||
            timeout=10,
 | 
			
		||||
        )
 | 
			
		||||
        nats_cmd.reset_mock()
 | 
			
		||||
 | 
			
		||||
@@ -615,7 +562,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
 | 
			
		||||
                    "pk": task1.pk,
 | 
			
		||||
                    "type": "rmm",
 | 
			
		||||
                    "name": task1.win_task_name,
 | 
			
		||||
                    "overwrite_task": False,
 | 
			
		||||
                    "overwrite_task": True,
 | 
			
		||||
                    "enabled": True,
 | 
			
		||||
                    "trigger": "monthlydow",
 | 
			
		||||
                    "multiple_instances": 1,
 | 
			
		||||
@@ -631,7 +578,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
 | 
			
		||||
                    "weeks_of_month": 3,
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            timeout=5,
 | 
			
		||||
            timeout=10,
 | 
			
		||||
        )
 | 
			
		||||
        nats_cmd.reset_mock()
 | 
			
		||||
 | 
			
		||||
@@ -653,7 +600,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
 | 
			
		||||
                    "pk": task1.pk,
 | 
			
		||||
                    "type": "rmm",
 | 
			
		||||
                    "name": task1.win_task_name,
 | 
			
		||||
                    "overwrite_task": False,
 | 
			
		||||
                    "overwrite_task": True,
 | 
			
		||||
                    "enabled": True,
 | 
			
		||||
                    "trigger": "runonce",
 | 
			
		||||
                    "multiple_instances": 1,
 | 
			
		||||
@@ -666,39 +613,10 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
 | 
			
		||||
                    "start_min": int(task1.run_time_date.strftime("%-M")),
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            timeout=5,
 | 
			
		||||
            timeout=10,
 | 
			
		||||
        )
 | 
			
		||||
        nats_cmd.reset_mock()
 | 
			
		||||
 | 
			
		||||
        # test runonce with date in the past
 | 
			
		||||
        task1 = baker.make(
 | 
			
		||||
            "autotasks.AutomatedTask",
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            name="test task 3",
 | 
			
		||||
            task_type=TaskType.RUN_ONCE,
 | 
			
		||||
            run_asap_after_missed=True,
 | 
			
		||||
            run_time_date=djangotime.datetime(2018, 6, 1, 23, 23, 23),
 | 
			
		||||
        )
 | 
			
		||||
        nats_cmd.return_value = "ok"
 | 
			
		||||
        create_win_task_schedule(pk=task1.pk)
 | 
			
		||||
        nats_cmd.assert_called()
 | 
			
		||||
 | 
			
		||||
        # check if task is scheduled for at most 5min in the future
 | 
			
		||||
        _, args, _ = nats_cmd.mock_calls[0]
 | 
			
		||||
 | 
			
		||||
        current_minute = int(djangotime.now().strftime("%-M"))
 | 
			
		||||
 | 
			
		||||
        if current_minute >= 55 and current_minute < 60:
 | 
			
		||||
            self.assertLess(
 | 
			
		||||
                args[0]["schedtaskpayload"]["start_min"],
 | 
			
		||||
                int(djangotime.now().strftime("%-M")),
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            self.assertGreater(
 | 
			
		||||
                args[0]["schedtaskpayload"]["start_min"],
 | 
			
		||||
                int(djangotime.now().strftime("%-M")),
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        # test checkfailure task
 | 
			
		||||
        nats_cmd.reset_mock()
 | 
			
		||||
        check = baker.make_recipe("checks.diskspace_check", agent=agent)
 | 
			
		||||
@@ -718,7 +636,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
 | 
			
		||||
                    "pk": task1.pk,
 | 
			
		||||
                    "type": "rmm",
 | 
			
		||||
                    "name": task1.win_task_name,
 | 
			
		||||
                    "overwrite_task": False,
 | 
			
		||||
                    "overwrite_task": True,
 | 
			
		||||
                    "enabled": True,
 | 
			
		||||
                    "trigger": "manual",
 | 
			
		||||
                    "multiple_instances": 1,
 | 
			
		||||
@@ -726,7 +644,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
 | 
			
		||||
                    "start_when_available": False,
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            timeout=5,
 | 
			
		||||
            timeout=10,
 | 
			
		||||
        )
 | 
			
		||||
        nats_cmd.reset_mock()
 | 
			
		||||
 | 
			
		||||
@@ -745,7 +663,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
 | 
			
		||||
                    "pk": task1.pk,
 | 
			
		||||
                    "type": "rmm",
 | 
			
		||||
                    "name": task1.win_task_name,
 | 
			
		||||
                    "overwrite_task": False,
 | 
			
		||||
                    "overwrite_task": True,
 | 
			
		||||
                    "enabled": True,
 | 
			
		||||
                    "trigger": "manual",
 | 
			
		||||
                    "multiple_instances": 1,
 | 
			
		||||
@@ -753,7 +671,7 @@ class TestAutoTaskCeleryTasks(TacticalTestCase):
 | 
			
		||||
                    "start_when_available": False,
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            timeout=5,
 | 
			
		||||
            timeout=10,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -766,12 +684,14 @@ class TestTaskPermissions(TacticalTestCase):
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        policy = baker.make("automation.Policy")
 | 
			
		||||
        unauthorized_agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        task = baker.make("autotasks.AutomatedTask", agent=agent, _quantity=5)
 | 
			
		||||
        unauthorized_task = baker.make(
 | 
			
		||||
        task = baker.make("autotasks.AutomatedTask", agent=agent, _quantity=5)  # noqa
 | 
			
		||||
        unauthorized_task = baker.make(  # noqa
 | 
			
		||||
            "autotasks.AutomatedTask", agent=unauthorized_agent, _quantity=7
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        policy_tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=2)
 | 
			
		||||
        policy_tasks = baker.make(  # noqa
 | 
			
		||||
            "autotasks.AutomatedTask", policy=policy, _quantity=2
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # test super user access
 | 
			
		||||
        self.check_authorized_superuser("get", f"{base_url}/")
 | 
			
		||||
@@ -864,7 +784,7 @@ class TestTaskPermissions(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        url = f"{base_url}/"
 | 
			
		||||
 | 
			
		||||
        for data in [policy_data, agent_data]:
 | 
			
		||||
        for data in (policy_data, agent_data):
 | 
			
		||||
            # test superuser access
 | 
			
		||||
            self.check_authorized_superuser("post", url, data)
 | 
			
		||||
 | 
			
		||||
@@ -900,8 +820,7 @@ class TestTaskPermissions(TacticalTestCase):
 | 
			
		||||
        )
 | 
			
		||||
        policy_task = baker.make("autotasks.AutomatedTask", policy=policy)
 | 
			
		||||
 | 
			
		||||
        for method in ["get", "put", "delete"]:
 | 
			
		||||
 | 
			
		||||
        for method in ("get", "put", "delete"):
 | 
			
		||||
            url = f"{base_url}/{task.id}/"
 | 
			
		||||
            unauthorized_url = f"{base_url}/{unauthorized_task.id}/"
 | 
			
		||||
            policy_url = f"{base_url}/{policy_task.id}/"
 | 
			
		||||
@@ -939,7 +858,6 @@ class TestTaskPermissions(TacticalTestCase):
 | 
			
		||||
            self.check_authorized(method, policy_url)
 | 
			
		||||
 | 
			
		||||
    def test_task_action_permissions(self):
 | 
			
		||||
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        unauthorized_agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        task = baker.make("autotasks.AutomatedTask", agent=agent)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from packaging import version as pyver
 | 
			
		||||
from rest_framework.exceptions import PermissionDenied
 | 
			
		||||
from rest_framework.permissions import IsAuthenticated
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
@@ -6,6 +7,8 @@ from rest_framework.views import APIView
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from automation.models import Policy
 | 
			
		||||
from tacticalrmm.constants import TaskType
 | 
			
		||||
from tacticalrmm.helpers import notify_error
 | 
			
		||||
from tacticalrmm.permissions import _has_perm_on_agent
 | 
			
		||||
 | 
			
		||||
from .models import AutomatedTask
 | 
			
		||||
@@ -18,7 +21,6 @@ class GetAddAutoTasks(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, AutoTaskPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, agent_id=None, policy=None):
 | 
			
		||||
 | 
			
		||||
        if agent_id:
 | 
			
		||||
            agent = get_object_or_404(Agent, agent_id=agent_id)
 | 
			
		||||
            tasks = agent.get_tasks_with_policies()
 | 
			
		||||
@@ -41,6 +43,11 @@ class GetAddAutoTasks(APIView):
 | 
			
		||||
            if not _has_perm_on_agent(request.user, agent.agent_id):
 | 
			
		||||
                raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
            if data["task_type"] == TaskType.ONBOARDING and pyver.parse(
 | 
			
		||||
                agent.version
 | 
			
		||||
            ) < pyver.parse("2.6.0"):
 | 
			
		||||
                return notify_error("Onboarding tasks require agent >= 2.6.0")
 | 
			
		||||
 | 
			
		||||
            data["agent"] = agent.pk
 | 
			
		||||
 | 
			
		||||
        serializer = TaskSerializer(data=data)
 | 
			
		||||
@@ -59,7 +66,6 @@ class GetEditDeleteAutoTask(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, AutoTaskPerms]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
 | 
			
		||||
        task = get_object_or_404(AutomatedTask, pk=pk)
 | 
			
		||||
 | 
			
		||||
        if task.agent and not _has_perm_on_agent(request.user, task.agent.agent_id):
 | 
			
		||||
@@ -68,7 +74,6 @@ class GetEditDeleteAutoTask(APIView):
 | 
			
		||||
        return Response(TaskSerializer(task).data)
 | 
			
		||||
 | 
			
		||||
    def put(self, request, pk):
 | 
			
		||||
 | 
			
		||||
        task = get_object_or_404(AutomatedTask, pk=pk)
 | 
			
		||||
 | 
			
		||||
        if task.agent and not _has_perm_on_agent(request.user, task.agent.agent_id):
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										0
									
								
								api/tacticalrmm/beta/v1/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								api/tacticalrmm/beta/v1/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								api/tacticalrmm/beta/v1/agent/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								api/tacticalrmm/beta/v1/agent/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										37
									
								
								api/tacticalrmm/beta/v1/agent/filter.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								api/tacticalrmm/beta/v1/agent/filter.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
			
		||||
import django_filters
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AgentFilter(django_filters.FilterSet):
 | 
			
		||||
    last_seen_range = django_filters.DateTimeFromToRangeFilter(field_name="last_seen")
 | 
			
		||||
    total_ram_range = django_filters.NumericRangeFilter(field_name="total_ram")
 | 
			
		||||
    patches_last_installed_range = django_filters.DateTimeFromToRangeFilter(
 | 
			
		||||
        field_name="patches_last_installed"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    client_id = django_filters.NumberFilter(method="client_id_filter")
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Agent
 | 
			
		||||
        fields = [
 | 
			
		||||
            "id",
 | 
			
		||||
            "hostname",
 | 
			
		||||
            "agent_id",
 | 
			
		||||
            "operating_system",
 | 
			
		||||
            "plat",
 | 
			
		||||
            "monitoring_type",
 | 
			
		||||
            "needs_reboot",
 | 
			
		||||
            "logged_in_username",
 | 
			
		||||
            "last_logged_in_user",
 | 
			
		||||
            "alert_template",
 | 
			
		||||
            "site",
 | 
			
		||||
            "policy",
 | 
			
		||||
            "last_seen_range",
 | 
			
		||||
            "total_ram_range",
 | 
			
		||||
            "patches_last_installed_range",
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
    def client_id_filter(self, queryset, name, value):
 | 
			
		||||
        if value:
 | 
			
		||||
            return queryset.filter(site__client__id=value)
 | 
			
		||||
        return queryset
 | 
			
		||||
							
								
								
									
										40
									
								
								api/tacticalrmm/beta/v1/agent/views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								api/tacticalrmm/beta/v1/agent/views.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
			
		||||
from rest_framework import viewsets
 | 
			
		||||
from rest_framework.permissions import IsAuthenticated
 | 
			
		||||
from django_filters.rest_framework import DjangoFilterBackend
 | 
			
		||||
from rest_framework.filters import SearchFilter, OrderingFilter
 | 
			
		||||
from rest_framework.request import Request
 | 
			
		||||
from rest_framework.serializers import BaseSerializer
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from agents.permissions import AgentPerms
 | 
			
		||||
from beta.v1.agent.filter import AgentFilter
 | 
			
		||||
from beta.v1.pagination import StandardResultsSetPagination
 | 
			
		||||
from ..serializers import DetailAgentSerializer, ListAgentSerializer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AgentViewSet(viewsets.ModelViewSet):
 | 
			
		||||
    permission_classes = [IsAuthenticated, AgentPerms]
 | 
			
		||||
    queryset = Agent.objects.all()
 | 
			
		||||
    pagination_class = StandardResultsSetPagination
 | 
			
		||||
    http_method_names = ["get", "put"]
 | 
			
		||||
    filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
 | 
			
		||||
    filterset_class = AgentFilter
 | 
			
		||||
    search_fields = ["hostname", "services"]
 | 
			
		||||
    ordering_fields = ["id"]
 | 
			
		||||
    ordering = ["id"]
 | 
			
		||||
 | 
			
		||||
    def check_permissions(self, request: Request) -> None:
 | 
			
		||||
        if "agent_id" in request.query_params:
 | 
			
		||||
            self.kwargs["agent_id"] = request.query_params["agent_id"]
 | 
			
		||||
        super().check_permissions(request)
 | 
			
		||||
 | 
			
		||||
    def get_permissions(self):
 | 
			
		||||
        if self.request.method == "POST":
 | 
			
		||||
            self.permission_classes = [IsAuthenticated]
 | 
			
		||||
        return super().get_permissions()
 | 
			
		||||
 | 
			
		||||
    def get_serializer_class(self) -> type[BaseSerializer]:
 | 
			
		||||
        if self.kwargs:
 | 
			
		||||
            if self.kwargs["pk"]:
 | 
			
		||||
                return DetailAgentSerializer
 | 
			
		||||
        return ListAgentSerializer
 | 
			
		||||
							
								
								
									
										0
									
								
								api/tacticalrmm/beta/v1/client/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								api/tacticalrmm/beta/v1/client/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										13
									
								
								api/tacticalrmm/beta/v1/client/views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								api/tacticalrmm/beta/v1/client/views.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
from rest_framework import viewsets
 | 
			
		||||
from rest_framework.permissions import IsAuthenticated
 | 
			
		||||
 | 
			
		||||
from clients.models import Client
 | 
			
		||||
from clients.permissions import ClientsPerms
 | 
			
		||||
from ..serializers import ClientSerializer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ClientViewSet(viewsets.ModelViewSet):
 | 
			
		||||
    permission_classes = [IsAuthenticated, ClientsPerms]
 | 
			
		||||
    queryset = Client.objects.all()
 | 
			
		||||
    serializer_class = ClientSerializer
 | 
			
		||||
    http_method_names = ["get", "put"]
 | 
			
		||||
							
								
								
									
										7
									
								
								api/tacticalrmm/beta/v1/pagination.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								api/tacticalrmm/beta/v1/pagination.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
from rest_framework.pagination import PageNumberPagination
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StandardResultsSetPagination(PageNumberPagination):
 | 
			
		||||
    page_size = 100
 | 
			
		||||
    page_size_query_param = "page_size"
 | 
			
		||||
    max_page_size = 1000
 | 
			
		||||
							
								
								
									
										73
									
								
								api/tacticalrmm/beta/v1/serializers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								api/tacticalrmm/beta/v1/serializers.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,73 @@
 | 
			
		||||
from rest_framework import serializers
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from clients.models import Client, Site
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ListAgentSerializer(serializers.ModelSerializer[Agent]):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Agent
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DetailAgentSerializer(serializers.ModelSerializer[Agent]):
 | 
			
		||||
    status = serializers.ReadOnlyField()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Agent
 | 
			
		||||
        fields = (
 | 
			
		||||
            "version",
 | 
			
		||||
            "operating_system",
 | 
			
		||||
            "plat",
 | 
			
		||||
            "goarch",
 | 
			
		||||
            "hostname",
 | 
			
		||||
            "agent_id",
 | 
			
		||||
            "last_seen",
 | 
			
		||||
            "services",
 | 
			
		||||
            "public_ip",
 | 
			
		||||
            "total_ram",
 | 
			
		||||
            "disks",
 | 
			
		||||
            "boot_time",
 | 
			
		||||
            "logged_in_username",
 | 
			
		||||
            "last_logged_in_user",
 | 
			
		||||
            "monitoring_type",
 | 
			
		||||
            "description",
 | 
			
		||||
            "mesh_node_id",
 | 
			
		||||
            "overdue_email_alert",
 | 
			
		||||
            "overdue_text_alert",
 | 
			
		||||
            "overdue_dashboard_alert",
 | 
			
		||||
            "offline_time",
 | 
			
		||||
            "overdue_time",
 | 
			
		||||
            "check_interval",
 | 
			
		||||
            "needs_reboot",
 | 
			
		||||
            "choco_installed",
 | 
			
		||||
            "wmi_detail",
 | 
			
		||||
            "patches_last_installed",
 | 
			
		||||
            "time_zone",
 | 
			
		||||
            "maintenance_mode",
 | 
			
		||||
            "block_policy_inheritance",
 | 
			
		||||
            "alert_template",
 | 
			
		||||
            "site",
 | 
			
		||||
            "policy",
 | 
			
		||||
            "status",
 | 
			
		||||
            "checks",
 | 
			
		||||
            "pending_actions_count",
 | 
			
		||||
            "cpu_model",
 | 
			
		||||
            "graphics",
 | 
			
		||||
            "local_ips",
 | 
			
		||||
            "make_model",
 | 
			
		||||
            "physical_disks",
 | 
			
		||||
            "serial_number",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ClientSerializer(serializers.ModelSerializer[Client]):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Client
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SiteSerializer(serializers.ModelSerializer[Site]):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Site
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
							
								
								
									
										21
									
								
								api/tacticalrmm/beta/v1/site/views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								api/tacticalrmm/beta/v1/site/views.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
from rest_framework import viewsets
 | 
			
		||||
from rest_framework.permissions import IsAuthenticated
 | 
			
		||||
from django_filters.rest_framework import DjangoFilterBackend
 | 
			
		||||
from rest_framework.filters import SearchFilter, OrderingFilter
 | 
			
		||||
 | 
			
		||||
from clients.models import Site
 | 
			
		||||
from clients.permissions import SitesPerms
 | 
			
		||||
from beta.v1.pagination import StandardResultsSetPagination
 | 
			
		||||
from ..serializers import SiteSerializer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SiteViewSet(viewsets.ModelViewSet):
 | 
			
		||||
    permission_classes = [IsAuthenticated, SitesPerms]
 | 
			
		||||
    queryset = Site.objects.all()
 | 
			
		||||
    serializer_class = SiteSerializer
 | 
			
		||||
    pagination_class = StandardResultsSetPagination
 | 
			
		||||
    http_method_names = ["get", "put"]
 | 
			
		||||
    filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
 | 
			
		||||
    search_fields = ["name"]
 | 
			
		||||
    ordering_fields = ["id"]
 | 
			
		||||
    ordering = ["id"]
 | 
			
		||||
							
								
								
									
										12
									
								
								api/tacticalrmm/beta/v1/urls.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								api/tacticalrmm/beta/v1/urls.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
from rest_framework import routers
 | 
			
		||||
from .agent import views as agent
 | 
			
		||||
from .client import views as client
 | 
			
		||||
from .site import views as site
 | 
			
		||||
 | 
			
		||||
router = routers.DefaultRouter()
 | 
			
		||||
 | 
			
		||||
router.register("agent", agent.AgentViewSet, basename="agent")
 | 
			
		||||
router.register("client", client.ClientViewSet, basename="client")
 | 
			
		||||
router.register("site", site.SiteViewSet, basename="site")
 | 
			
		||||
 | 
			
		||||
urlpatterns = router.urls
 | 
			
		||||
							
								
								
									
										25
									
								
								api/tacticalrmm/checks/migrations/0031_check_env_vars.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								api/tacticalrmm/checks/migrations/0031_check_env_vars.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
			
		||||
# Generated by Django 4.1.3 on 2022-12-03 09:38
 | 
			
		||||
 | 
			
		||||
import django.contrib.postgres.fields
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("checks", "0030_alter_checkresult_retcode"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name="check",
 | 
			
		||||
            name="env_vars",
 | 
			
		||||
            field=django.contrib.postgres.fields.ArrayField(
 | 
			
		||||
                base_field=models.TextField(blank=True, null=True),
 | 
			
		||||
                blank=True,
 | 
			
		||||
                default=list,
 | 
			
		||||
                null=True,
 | 
			
		||||
                size=None,
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,23 @@
 | 
			
		||||
# Generated by Django 4.2.10 on 2024-02-19 05:57
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("checks", "0031_check_env_vars"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="checkhistory",
 | 
			
		||||
            name="id",
 | 
			
		||||
            field=models.BigAutoField(primary_key=True, serialize=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="checkresult",
 | 
			
		||||
            name="id",
 | 
			
		||||
            field=models.BigAutoField(primary_key=True, serialize=False),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -19,6 +19,7 @@ from tacticalrmm.constants import (
 | 
			
		||||
    EvtLogNames,
 | 
			
		||||
    EvtLogTypes,
 | 
			
		||||
)
 | 
			
		||||
from tacticalrmm.helpers import has_script_actions, has_webhook
 | 
			
		||||
from tacticalrmm.models import PermissionQuerySet
 | 
			
		||||
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
@@ -98,6 +99,12 @@ class Check(BaseAuditModel):
 | 
			
		||||
        blank=True,
 | 
			
		||||
        default=list,
 | 
			
		||||
    )
 | 
			
		||||
    env_vars = ArrayField(
 | 
			
		||||
        models.TextField(null=True, blank=True),
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        default=list,
 | 
			
		||||
    )
 | 
			
		||||
    info_return_codes = ArrayField(
 | 
			
		||||
        models.PositiveIntegerField(),
 | 
			
		||||
        null=True,
 | 
			
		||||
@@ -149,11 +156,10 @@ class Check(BaseAuditModel):
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        if self.agent:
 | 
			
		||||
            return f"{self.agent.hostname} - {self.readable_desc}"
 | 
			
		||||
        else:
 | 
			
		||||
            return f"{self.policy.name} - {self.readable_desc}"
 | 
			
		||||
 | 
			
		||||
        return f"{self.policy.name} - {self.readable_desc}"
 | 
			
		||||
 | 
			
		||||
    def save(self, *args, **kwargs):
 | 
			
		||||
 | 
			
		||||
        # if check is a policy check clear cache on everything
 | 
			
		||||
        if self.policy:
 | 
			
		||||
            cache.delete_many_pattern("site_*_checks")
 | 
			
		||||
@@ -163,13 +169,9 @@ class Check(BaseAuditModel):
 | 
			
		||||
        elif self.agent:
 | 
			
		||||
            cache.delete(f"agent_{self.agent.agent_id}_checks")
 | 
			
		||||
 | 
			
		||||
        super(Check, self).save(
 | 
			
		||||
            *args,
 | 
			
		||||
            **kwargs,
 | 
			
		||||
        )
 | 
			
		||||
        super().save(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def delete(self, *args, **kwargs):
 | 
			
		||||
 | 
			
		||||
        # if check is a policy check clear cache on everything
 | 
			
		||||
        if self.policy:
 | 
			
		||||
            cache.delete_many_pattern("site_*_checks")
 | 
			
		||||
@@ -179,16 +181,12 @@ class Check(BaseAuditModel):
 | 
			
		||||
        elif self.agent:
 | 
			
		||||
            cache.delete(f"agent_{self.agent.agent_id}_checks")
 | 
			
		||||
 | 
			
		||||
        super(Check, self).delete(
 | 
			
		||||
            *args,
 | 
			
		||||
            **kwargs,
 | 
			
		||||
        )
 | 
			
		||||
        super().delete(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def readable_desc(self):
 | 
			
		||||
        display = self.get_check_type_display()  # type: ignore
 | 
			
		||||
        if self.check_type == CheckType.DISK_SPACE:
 | 
			
		||||
 | 
			
		||||
            text = ""
 | 
			
		||||
            if self.warning_threshold:
 | 
			
		||||
                text += f" Warning Threshold: {self.warning_threshold}%"
 | 
			
		||||
@@ -198,10 +196,7 @@ class Check(BaseAuditModel):
 | 
			
		||||
            return f"{display}: Drive {self.disk} - {text}"
 | 
			
		||||
        elif self.check_type == CheckType.PING:
 | 
			
		||||
            return f"{display}: {self.name}"
 | 
			
		||||
        elif (
 | 
			
		||||
            self.check_type == CheckType.CPU_LOAD or self.check_type == CheckType.MEMORY
 | 
			
		||||
        ):
 | 
			
		||||
 | 
			
		||||
        elif self.check_type in (CheckType.CPU_LOAD, CheckType.MEMORY):
 | 
			
		||||
            text = ""
 | 
			
		||||
            if self.warning_threshold:
 | 
			
		||||
                text += f" Warning Threshold: {self.warning_threshold}%"
 | 
			
		||||
@@ -215,17 +210,14 @@ class Check(BaseAuditModel):
 | 
			
		||||
            return f"{display}: {self.name}"
 | 
			
		||||
        elif self.check_type == CheckType.SCRIPT:
 | 
			
		||||
            return f"{display}: {self.script.name}"
 | 
			
		||||
        else:
 | 
			
		||||
            return "n/a"
 | 
			
		||||
 | 
			
		||||
        return "n/a"
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def non_editable_fields() -> list[str]:
 | 
			
		||||
        return CHECKS_NON_EDITABLE_FIELDS
 | 
			
		||||
 | 
			
		||||
    def create_policy_check(self, policy: "Policy") -> None:
 | 
			
		||||
 | 
			
		||||
        fields_to_copy = POLICY_CHECK_FIELDS_TO_COPY
 | 
			
		||||
 | 
			
		||||
        check = Check.objects.create(
 | 
			
		||||
            policy=policy,
 | 
			
		||||
        )
 | 
			
		||||
@@ -233,25 +225,25 @@ class Check(BaseAuditModel):
 | 
			
		||||
        for task in self.assignedtasks.all():  # type: ignore
 | 
			
		||||
            task.create_policy_task(policy=policy, assigned_check=check)
 | 
			
		||||
 | 
			
		||||
        for field in fields_to_copy:
 | 
			
		||||
        for field in POLICY_CHECK_FIELDS_TO_COPY:
 | 
			
		||||
            setattr(check, field, getattr(self, field))
 | 
			
		||||
 | 
			
		||||
        check.save()
 | 
			
		||||
 | 
			
		||||
    def should_create_alert(self, alert_template=None):
 | 
			
		||||
 | 
			
		||||
        has_check_notifications = (
 | 
			
		||||
            self.dashboard_alert or self.email_alert or self.text_alert
 | 
			
		||||
        )
 | 
			
		||||
        has_alert_template_notification = alert_template and (
 | 
			
		||||
            alert_template.check_always_alert
 | 
			
		||||
            or alert_template.check_always_email
 | 
			
		||||
            or alert_template.check_always_text
 | 
			
		||||
        )
 | 
			
		||||
        return (
 | 
			
		||||
            self.dashboard_alert
 | 
			
		||||
            or self.email_alert
 | 
			
		||||
            or self.text_alert
 | 
			
		||||
            or (
 | 
			
		||||
                alert_template
 | 
			
		||||
                and (
 | 
			
		||||
                    alert_template.check_always_alert
 | 
			
		||||
                    or alert_template.check_always_email
 | 
			
		||||
                    or alert_template.check_always_text
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            has_check_notifications
 | 
			
		||||
            or has_alert_template_notification
 | 
			
		||||
            or has_webhook(alert_template, "check")
 | 
			
		||||
            or has_script_actions(alert_template, "check")
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def add_check_history(
 | 
			
		||||
@@ -294,6 +286,7 @@ class CheckResult(models.Model):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        unique_together = (("agent", "assigned_check"),)
 | 
			
		||||
 | 
			
		||||
    id = models.BigAutoField(primary_key=True)
 | 
			
		||||
    agent = models.ForeignKey(
 | 
			
		||||
        "agents.Agent",
 | 
			
		||||
        related_name="checkresults",
 | 
			
		||||
@@ -333,20 +326,16 @@ class CheckResult(models.Model):
 | 
			
		||||
        return f"{self.agent.hostname} - {self.assigned_check}"
 | 
			
		||||
 | 
			
		||||
    def save(self, *args, **kwargs):
 | 
			
		||||
 | 
			
		||||
        # if check is a policy check clear cache on everything
 | 
			
		||||
        if not self.alert_severity and self.assigned_check.check_type in [
 | 
			
		||||
        if not self.alert_severity and self.assigned_check.check_type in (
 | 
			
		||||
            CheckType.MEMORY,
 | 
			
		||||
            CheckType.CPU_LOAD,
 | 
			
		||||
            CheckType.DISK_SPACE,
 | 
			
		||||
            CheckType.SCRIPT,
 | 
			
		||||
        ]:
 | 
			
		||||
        ):
 | 
			
		||||
            self.alert_severity = AlertSeverity.WARNING
 | 
			
		||||
 | 
			
		||||
        super(CheckResult, self).save(
 | 
			
		||||
            *args,
 | 
			
		||||
            **kwargs,
 | 
			
		||||
        )
 | 
			
		||||
        super().save(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def history_info(self):
 | 
			
		||||
@@ -371,15 +360,16 @@ class CheckResult(models.Model):
 | 
			
		||||
        update_fields = []
 | 
			
		||||
        # cpuload or mem checks
 | 
			
		||||
        if check.check_type in (CheckType.CPU_LOAD, CheckType.MEMORY):
 | 
			
		||||
 | 
			
		||||
            self.history.append(data["percent"])
 | 
			
		||||
 | 
			
		||||
            if len(self.history) > 15:
 | 
			
		||||
                self.history = self.history[-15:]
 | 
			
		||||
 | 
			
		||||
            update_fields.extend(["history"])
 | 
			
		||||
            update_fields.extend(["history", "more_info"])
 | 
			
		||||
 | 
			
		||||
            avg = int(mean(self.history))
 | 
			
		||||
            txt = "Memory Usage" if check.check_type == CheckType.MEMORY else "CPU Load"
 | 
			
		||||
            self.more_info = f"Average {txt}: {avg}%"
 | 
			
		||||
 | 
			
		||||
            if check.error_threshold and avg > check.error_threshold:
 | 
			
		||||
                self.status = CheckStatus.FAILING
 | 
			
		||||
@@ -536,7 +526,6 @@ class CheckResult(models.Model):
 | 
			
		||||
        return self.status
 | 
			
		||||
 | 
			
		||||
    def send_email(self):
 | 
			
		||||
 | 
			
		||||
        CORE = get_core_settings()
 | 
			
		||||
 | 
			
		||||
        body: str = ""
 | 
			
		||||
@@ -565,14 +554,12 @@ class CheckResult(models.Model):
 | 
			
		||||
                body = subject + f" - Disk {self.assigned_check.disk} does not exist"
 | 
			
		||||
 | 
			
		||||
        elif self.assigned_check.check_type == CheckType.SCRIPT:
 | 
			
		||||
 | 
			
		||||
            body = (
 | 
			
		||||
                subject
 | 
			
		||||
                + f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        elif self.assigned_check.check_type == CheckType.PING:
 | 
			
		||||
 | 
			
		||||
            body = self.more_info
 | 
			
		||||
 | 
			
		||||
        elif self.assigned_check.check_type in (CheckType.CPU_LOAD, CheckType.MEMORY):
 | 
			
		||||
@@ -594,7 +581,6 @@ class CheckResult(models.Model):
 | 
			
		||||
            body = subject + f" - Status: {self.more_info}"
 | 
			
		||||
 | 
			
		||||
        elif self.assigned_check.check_type == CheckType.EVENT_LOG:
 | 
			
		||||
 | 
			
		||||
            if self.assigned_check.event_source and self.assigned_check.event_message:
 | 
			
		||||
                start = f"Event ID {self.assigned_check.event_id}, source {self.assigned_check.event_source}, containing string {self.assigned_check.event_message} "
 | 
			
		||||
            elif self.assigned_check.event_source:
 | 
			
		||||
@@ -616,7 +602,6 @@ class CheckResult(models.Model):
 | 
			
		||||
        CORE.send_mail(subject, body, alert_template=self.agent.alert_template)
 | 
			
		||||
 | 
			
		||||
    def send_sms(self):
 | 
			
		||||
 | 
			
		||||
        CORE = get_core_settings()
 | 
			
		||||
        body: str = ""
 | 
			
		||||
 | 
			
		||||
@@ -684,6 +669,7 @@ class CheckResult(models.Model):
 | 
			
		||||
class CheckHistory(models.Model):
 | 
			
		||||
    objects = PermissionQuerySet.as_manager()
 | 
			
		||||
 | 
			
		||||
    id = models.BigAutoField(primary_key=True)
 | 
			
		||||
    check_id = models.PositiveIntegerField(default=0)
 | 
			
		||||
    agent_id = models.CharField(max_length=200, null=True, blank=True)
 | 
			
		||||
    x = models.DateTimeField(auto_now_add=True)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,16 @@
 | 
			
		||||
from rest_framework import permissions
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.permissions import _has_perm, _has_perm_on_agent
 | 
			
		||||
from tacticalrmm.permissions import (
 | 
			
		||||
    _has_perm,
 | 
			
		||||
    _has_perm_on_agent,
 | 
			
		||||
    _has_perm_on_client,
 | 
			
		||||
    _has_perm_on_site,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ChecksPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view) -> bool:
 | 
			
		||||
        if r.method == "GET" or r.method == "PATCH":
 | 
			
		||||
        if r.method in ("GET", "PATCH"):
 | 
			
		||||
            if "agent_id" in view.kwargs.keys():
 | 
			
		||||
                return _has_perm(r, "can_list_checks") and _has_perm_on_agent(
 | 
			
		||||
                    r.user, view.kwargs["agent_id"]
 | 
			
		||||
@@ -21,3 +26,17 @@ class RunChecksPerms(permissions.BasePermission):
 | 
			
		||||
        return _has_perm(r, "can_run_checks") and _has_perm_on_agent(
 | 
			
		||||
            r.user, view.kwargs["agent_id"]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BulkRunChecksPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view) -> bool:
 | 
			
		||||
        if not _has_perm(r, "can_run_checks"):
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
        if view.kwargs["target"] == "client":
 | 
			
		||||
            return _has_perm_on_client(user=r.user, client_id=view.kwargs["pk"])
 | 
			
		||||
 | 
			
		||||
        elif view.kwargs["target"] == "site":
 | 
			
		||||
            return _has_perm_on_site(user=r.user, site_id=view.kwargs["pk"])
 | 
			
		||||
 | 
			
		||||
        return False
 | 
			
		||||
 
 | 
			
		||||
@@ -22,7 +22,6 @@ class CheckResultSerializer(serializers.ModelSerializer):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CheckSerializer(serializers.ModelSerializer):
 | 
			
		||||
 | 
			
		||||
    readable_desc = serializers.ReadOnlyField()
 | 
			
		||||
    assignedtasks = AssignedTaskField(many=True, read_only=True)
 | 
			
		||||
    alert_template = serializers.SerializerMethodField()
 | 
			
		||||
@@ -43,13 +42,13 @@ class CheckSerializer(serializers.ModelSerializer):
 | 
			
		||||
 | 
			
		||||
        if not alert_template:
 | 
			
		||||
            return None
 | 
			
		||||
        else:
 | 
			
		||||
            return {
 | 
			
		||||
                "name": alert_template.name,
 | 
			
		||||
                "always_email": alert_template.check_always_email,
 | 
			
		||||
                "always_text": alert_template.check_always_text,
 | 
			
		||||
                "always_alert": alert_template.check_always_alert,
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            "name": alert_template.name,
 | 
			
		||||
            "always_email": alert_template.check_always_email,
 | 
			
		||||
            "always_text": alert_template.check_always_text,
 | 
			
		||||
            "always_alert": alert_template.check_always_alert,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Check
 | 
			
		||||
@@ -82,7 +81,7 @@ class CheckSerializer(serializers.ModelSerializer):
 | 
			
		||||
 | 
			
		||||
            if not val["warning_threshold"] and not val["error_threshold"]:
 | 
			
		||||
                raise serializers.ValidationError(
 | 
			
		||||
                    f"Warning threshold or Error Threshold must be set"
 | 
			
		||||
                    "Warning threshold or Error Threshold must be set"
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            if (
 | 
			
		||||
@@ -91,7 +90,7 @@ class CheckSerializer(serializers.ModelSerializer):
 | 
			
		||||
                and val["error_threshold"] > 0
 | 
			
		||||
            ):
 | 
			
		||||
                raise serializers.ValidationError(
 | 
			
		||||
                    f"Warning threshold must be greater than Error Threshold"
 | 
			
		||||
                    "Warning threshold must be greater than Error Threshold"
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
        # ping checks
 | 
			
		||||
@@ -113,7 +112,7 @@ class CheckSerializer(serializers.ModelSerializer):
 | 
			
		||||
 | 
			
		||||
            if not val["warning_threshold"] and not val["error_threshold"]:
 | 
			
		||||
                raise serializers.ValidationError(
 | 
			
		||||
                    f"Warning threshold or Error Threshold must be set"
 | 
			
		||||
                    "Warning threshold or Error Threshold must be set"
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            if (
 | 
			
		||||
@@ -122,7 +121,7 @@ class CheckSerializer(serializers.ModelSerializer):
 | 
			
		||||
                and val["error_threshold"] > 0
 | 
			
		||||
            ):
 | 
			
		||||
                raise serializers.ValidationError(
 | 
			
		||||
                    f"Warning threshold must be less than Error Threshold"
 | 
			
		||||
                    "Warning threshold must be less than Error Threshold"
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
        if check_type == CheckType.MEMORY and not self.instance:
 | 
			
		||||
@@ -133,7 +132,7 @@ class CheckSerializer(serializers.ModelSerializer):
 | 
			
		||||
 | 
			
		||||
            if not val["warning_threshold"] and not val["error_threshold"]:
 | 
			
		||||
                raise serializers.ValidationError(
 | 
			
		||||
                    f"Warning threshold or Error Threshold must be set"
 | 
			
		||||
                    "Warning threshold or Error Threshold must be set"
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            if (
 | 
			
		||||
@@ -142,7 +141,7 @@ class CheckSerializer(serializers.ModelSerializer):
 | 
			
		||||
                and val["error_threshold"] > 0
 | 
			
		||||
            ):
 | 
			
		||||
                raise serializers.ValidationError(
 | 
			
		||||
                    f"Warning threshold must be less than Error Threshold"
 | 
			
		||||
                    "Warning threshold must be less than Error Threshold"
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
        return val
 | 
			
		||||
@@ -158,6 +157,7 @@ class CheckRunnerGetSerializer(serializers.ModelSerializer):
 | 
			
		||||
    # only send data needed for agent to run a check
 | 
			
		||||
    script = ScriptCheckSerializer(read_only=True)
 | 
			
		||||
    script_args = serializers.SerializerMethodField()
 | 
			
		||||
    env_vars = serializers.SerializerMethodField()
 | 
			
		||||
 | 
			
		||||
    def get_script_args(self, obj):
 | 
			
		||||
        if obj.check_type != CheckType.SCRIPT:
 | 
			
		||||
@@ -168,6 +168,18 @@ class CheckRunnerGetSerializer(serializers.ModelSerializer):
 | 
			
		||||
            agent=agent, shell=obj.script.shell, args=obj.script_args
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def get_env_vars(self, obj):
 | 
			
		||||
        if obj.check_type != CheckType.SCRIPT:
 | 
			
		||||
            return []
 | 
			
		||||
 | 
			
		||||
        agent = self.context["agent"] if "agent" in self.context.keys() else obj.agent
 | 
			
		||||
 | 
			
		||||
        return Script.parse_script_env_vars(
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            shell=obj.script.shell,
 | 
			
		||||
            env_vars=obj.env_vars,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Check
 | 
			
		||||
        exclude = [
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
import datetime as dt
 | 
			
		||||
import random
 | 
			
		||||
from time import sleep
 | 
			
		||||
from typing import Optional
 | 
			
		||||
 | 
			
		||||
@@ -8,6 +7,8 @@ from django.utils import timezone as djangotime
 | 
			
		||||
from alerts.models import Alert
 | 
			
		||||
from checks.models import CheckResult
 | 
			
		||||
from tacticalrmm.celery import app
 | 
			
		||||
from tacticalrmm.helpers import rand_range
 | 
			
		||||
from tacticalrmm.logger import logger
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
@@ -24,7 +25,7 @@ def handle_check_email_alert_task(
 | 
			
		||||
        check_result = CheckResult.objects.get(
 | 
			
		||||
            assigned_check=alert.assigned_check, agent=alert.agent
 | 
			
		||||
        )
 | 
			
		||||
        sleep(random.randint(1, 5))
 | 
			
		||||
        sleep(rand_range(100, 1500))
 | 
			
		||||
        check_result.send_email()
 | 
			
		||||
        alert.email_sent = djangotime.now()
 | 
			
		||||
        alert.save(update_fields=["email_sent"])
 | 
			
		||||
@@ -36,7 +37,7 @@ def handle_check_email_alert_task(
 | 
			
		||||
                check_result = CheckResult.objects.get(
 | 
			
		||||
                    assigned_check=alert.assigned_check, agent=alert.agent
 | 
			
		||||
                )
 | 
			
		||||
                sleep(random.randint(1, 5))
 | 
			
		||||
                sleep(rand_range(100, 1500))
 | 
			
		||||
                check_result.send_email()
 | 
			
		||||
                alert.email_sent = djangotime.now()
 | 
			
		||||
                alert.save(update_fields=["email_sent"])
 | 
			
		||||
@@ -46,7 +47,6 @@ def handle_check_email_alert_task(
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def handle_check_sms_alert_task(pk: int, alert_interval: Optional[float] = None) -> str:
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        alert = Alert.objects.get(pk=pk)
 | 
			
		||||
    except Alert.DoesNotExist:
 | 
			
		||||
@@ -57,7 +57,7 @@ def handle_check_sms_alert_task(pk: int, alert_interval: Optional[float] = None)
 | 
			
		||||
        check_result = CheckResult.objects.get(
 | 
			
		||||
            assigned_check=alert.assigned_check, agent=alert.agent
 | 
			
		||||
        )
 | 
			
		||||
        sleep(random.randint(1, 3))
 | 
			
		||||
        sleep(rand_range(100, 1500))
 | 
			
		||||
        check_result.send_sms()
 | 
			
		||||
        alert.sms_sent = djangotime.now()
 | 
			
		||||
        alert.save(update_fields=["sms_sent"])
 | 
			
		||||
@@ -69,7 +69,7 @@ def handle_check_sms_alert_task(pk: int, alert_interval: Optional[float] = None)
 | 
			
		||||
                check_result = CheckResult.objects.get(
 | 
			
		||||
                    assigned_check=alert.assigned_check, agent=alert.agent
 | 
			
		||||
                )
 | 
			
		||||
                sleep(random.randint(1, 3))
 | 
			
		||||
                sleep(rand_range(100, 1500))
 | 
			
		||||
                check_result.send_sms()
 | 
			
		||||
                alert.sms_sent = djangotime.now()
 | 
			
		||||
                alert.save(update_fields=["sms_sent"])
 | 
			
		||||
@@ -79,7 +79,6 @@ def handle_check_sms_alert_task(pk: int, alert_interval: Optional[float] = None)
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def handle_resolved_check_sms_alert_task(pk: int) -> str:
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        alert = Alert.objects.get(pk=pk)
 | 
			
		||||
    except Alert.DoesNotExist:
 | 
			
		||||
@@ -90,7 +89,7 @@ def handle_resolved_check_sms_alert_task(pk: int) -> str:
 | 
			
		||||
        check_result = CheckResult.objects.get(
 | 
			
		||||
            assigned_check=alert.assigned_check, agent=alert.agent
 | 
			
		||||
        )
 | 
			
		||||
        sleep(random.randint(1, 3))
 | 
			
		||||
        sleep(rand_range(100, 1500))
 | 
			
		||||
        check_result.send_resolved_sms()
 | 
			
		||||
        alert.resolved_sms_sent = djangotime.now()
 | 
			
		||||
        alert.save(update_fields=["resolved_sms_sent"])
 | 
			
		||||
@@ -100,7 +99,6 @@ def handle_resolved_check_sms_alert_task(pk: int) -> str:
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def handle_resolved_check_email_alert_task(pk: int) -> str:
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        alert = Alert.objects.get(pk=pk)
 | 
			
		||||
    except Alert.DoesNotExist:
 | 
			
		||||
@@ -111,7 +109,7 @@ def handle_resolved_check_email_alert_task(pk: int) -> str:
 | 
			
		||||
        check_result = CheckResult.objects.get(
 | 
			
		||||
            assigned_check=alert.assigned_check, agent=alert.agent
 | 
			
		||||
        )
 | 
			
		||||
        sleep(random.randint(1, 5))
 | 
			
		||||
        sleep(rand_range(100, 1500))
 | 
			
		||||
        check_result.send_resolved_email()
 | 
			
		||||
        alert.resolved_email_sent = djangotime.now()
 | 
			
		||||
        alert.save(update_fields=["resolved_email_sent"])
 | 
			
		||||
@@ -123,9 +121,9 @@ def handle_resolved_check_email_alert_task(pk: int) -> str:
 | 
			
		||||
def prune_check_history(older_than_days: int) -> str:
 | 
			
		||||
    from .models import CheckHistory
 | 
			
		||||
 | 
			
		||||
    CheckHistory.objects.filter(
 | 
			
		||||
        x__lt=djangotime.make_aware(dt.datetime.today())
 | 
			
		||||
        - djangotime.timedelta(days=older_than_days)
 | 
			
		||||
    c, _ = CheckHistory.objects.filter(
 | 
			
		||||
        x__lt=djangotime.now() - djangotime.timedelta(days=older_than_days)
 | 
			
		||||
    ).delete()
 | 
			
		||||
    logger.info(f"Pruned {c} check history objects")
 | 
			
		||||
 | 
			
		||||
    return "ok"
 | 
			
		||||
 
 | 
			
		||||
@@ -41,7 +41,7 @@ class TestCheckViews(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(len(resp.data), 4)
 | 
			
		||||
 | 
			
		||||
        # test agent doesn't exist
 | 
			
		||||
        url = f"/agents/jh3498uf8fkh4ro8hfd8df98/checks/"
 | 
			
		||||
        url = "/agents/jh3498uf8fkh4ro8hfd8df98/checks/"
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 404)
 | 
			
		||||
 | 
			
		||||
@@ -101,8 +101,7 @@ class TestCheckViews(TacticalTestCase):
 | 
			
		||||
            "fails_b4_alert": 3,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for payload in [agent_payload, policy_payload]:
 | 
			
		||||
 | 
			
		||||
        for payload in (agent_payload, policy_payload):
 | 
			
		||||
            # add valid check
 | 
			
		||||
            resp = self.client.post(url, payload, format="json")
 | 
			
		||||
            self.assertEqual(resp.status_code, 200)
 | 
			
		||||
@@ -148,8 +147,7 @@ class TestCheckViews(TacticalTestCase):
 | 
			
		||||
            "fails_b4_alert": 9,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for payload in [agent_payload, policy_payload]:
 | 
			
		||||
 | 
			
		||||
        for payload in (agent_payload, policy_payload):
 | 
			
		||||
            # add cpu check
 | 
			
		||||
            resp = self.client.post(url, payload, format="json")
 | 
			
		||||
            self.assertEqual(resp.status_code, 200)
 | 
			
		||||
@@ -174,6 +172,31 @@ class TestCheckViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
    def test_reset_all_checks_status(self):
 | 
			
		||||
        # setup data
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        check = baker.make_recipe("checks.diskspace_check", agent=agent)
 | 
			
		||||
        baker.make("checks.CheckResult", assigned_check=check, agent=agent)
 | 
			
		||||
        baker.make(
 | 
			
		||||
            "checks.CheckHistory",
 | 
			
		||||
            check_id=check.id,
 | 
			
		||||
            agent_id=agent.agent_id,
 | 
			
		||||
            _quantity=30,
 | 
			
		||||
        )
 | 
			
		||||
        baker.make(
 | 
			
		||||
            "checks.CheckHistory",
 | 
			
		||||
            check_id=check.id,
 | 
			
		||||
            agent_id=agent.agent_id,
 | 
			
		||||
            _quantity=30,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        url = f"{base_url}/{agent.agent_id}/resetall/"
 | 
			
		||||
 | 
			
		||||
        resp = self.client.post(url)
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
    def test_add_memory_check(self):
 | 
			
		||||
        url = f"{base_url}/"
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
@@ -195,8 +218,7 @@ class TestCheckViews(TacticalTestCase):
 | 
			
		||||
            "fails_b4_alert": 1,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for payload in [agent_payload, policy_payload]:
 | 
			
		||||
 | 
			
		||||
        for payload in (agent_payload, policy_payload):
 | 
			
		||||
            # add memory check
 | 
			
		||||
            resp = self.client.post(url, payload, format="json")
 | 
			
		||||
            self.assertEqual(resp.status_code, 200)
 | 
			
		||||
@@ -239,7 +261,6 @@ class TestCheckViews(TacticalTestCase):
 | 
			
		||||
        r = self.client.post(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        nats_cmd.assert_called_with({"func": "runchecks"}, timeout=15)
 | 
			
		||||
        self.assertEqual(r.json(), f"Checks will now be re-run on {agent.hostname}")
 | 
			
		||||
 | 
			
		||||
        nats_cmd.reset_mock()
 | 
			
		||||
        nats_cmd.return_value = "timeout"
 | 
			
		||||
@@ -885,12 +906,12 @@ class TestCheckPermissions(TacticalTestCase):
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        policy = baker.make("automation.Policy")
 | 
			
		||||
        unauthorized_agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        check = baker.make("checks.Check", agent=agent, _quantity=5)
 | 
			
		||||
        unauthorized_check = baker.make(
 | 
			
		||||
        check = baker.make("checks.Check", agent=agent, _quantity=5)  # noqa
 | 
			
		||||
        unauthorized_check = baker.make(  # noqa
 | 
			
		||||
            "checks.Check", agent=unauthorized_agent, _quantity=7
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        policy_checks = baker.make("checks.Check", policy=policy, _quantity=2)
 | 
			
		||||
        policy_checks = baker.make("checks.Check", policy=policy, _quantity=2)  # noqa
 | 
			
		||||
 | 
			
		||||
        # test super user access
 | 
			
		||||
        self.check_authorized_superuser("get", f"{base_url}/")
 | 
			
		||||
@@ -973,7 +994,7 @@ class TestCheckPermissions(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        url = f"{base_url}/"
 | 
			
		||||
 | 
			
		||||
        for data in [policy_data, agent_data]:
 | 
			
		||||
        for data in (policy_data, agent_data):
 | 
			
		||||
            # test superuser access
 | 
			
		||||
            self.check_authorized_superuser("post", url, data)
 | 
			
		||||
 | 
			
		||||
@@ -1007,8 +1028,7 @@ class TestCheckPermissions(TacticalTestCase):
 | 
			
		||||
        unauthorized_check = baker.make("checks.Check", agent=unauthorized_agent)
 | 
			
		||||
        policy_check = baker.make("checks.Check", policy=policy)
 | 
			
		||||
 | 
			
		||||
        for method in ["get", "put", "delete"]:
 | 
			
		||||
 | 
			
		||||
        for method in ("get", "put", "delete"):
 | 
			
		||||
            url = f"{base_url}/{check.id}/"
 | 
			
		||||
            unauthorized_url = f"{base_url}/{unauthorized_check.id}/"
 | 
			
		||||
            policy_url = f"{base_url}/{policy_check.id}/"
 | 
			
		||||
@@ -1047,7 +1067,6 @@ class TestCheckPermissions(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
    @patch("agents.models.Agent.nats_cmd")
 | 
			
		||||
    def test_check_action_permissions(self, nats_cmd):
 | 
			
		||||
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        unauthorized_agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        check = baker.make("checks.Check", agent=agent)
 | 
			
		||||
@@ -1061,7 +1080,7 @@ class TestCheckPermissions(TacticalTestCase):
 | 
			
		||||
            assigned_check=unauthorized_check,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        for action in ["reset", "run"]:
 | 
			
		||||
        for action in ("reset", "run"):
 | 
			
		||||
            if action == "reset":
 | 
			
		||||
                url = f"{base_url}/{check_result.id}/{action}/"
 | 
			
		||||
                unauthorized_url = (
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user