Compare commits
	
		
			1082 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					715982e40a | ||
| 
						 | 
					d00cd4453a | ||
| 
						 | 
					429c08c24a | ||
| 
						 | 
					6a71490e20 | ||
| 
						 | 
					9bceda0646 | ||
| 
						 | 
					a1027a6773 | ||
| 
						 | 
					302d4b75f9 | ||
| 
						 | 
					5f6ee0e883 | ||
| 
						 | 
					27f9720de1 | ||
| 
						 | 
					22aa3fdbbc | ||
| 
						 | 
					069ecdd33f | ||
| 
						 | 
					dd545ae933 | ||
| 
						 | 
					6650b705c4 | ||
| 
						 | 
					59b0350289 | ||
| 
						 | 
					1ad159f820 | ||
| 
						 | 
					0bf42190e9 | ||
| 
						 | 
					d2fa836232 | ||
| 
						 | 
					c387774093 | ||
| 
						 | 
					e99736ba3c | ||
| 
						 | 
					16cb54fcc9 | ||
| 
						 | 
					5aa15c51ec | ||
| 
						 | 
					a8aedd9cf3 | ||
| 
						 | 
					b851b632bc | ||
| 
						 | 
					541e07fb65 | ||
| 
						 | 
					6ad16a897d | ||
| 
						 | 
					72f1053a93 | ||
| 
						 | 
					fb15a2762c | ||
| 
						 | 
					9165248b91 | ||
| 
						 | 
					add18b29db | ||
| 
						 | 
					1971653548 | ||
| 
						 | 
					392cd64d7b | ||
| 
						 | 
					b5affbb7c8 | ||
| 
						 | 
					71d1206277 | ||
| 
						 | 
					26e6a8c409 | ||
| 
						 | 
					eb54fae11a | ||
| 
						 | 
					ee773e5966 | ||
| 
						 | 
					7218ccdba8 | ||
| 
						 | 
					332400e48a | ||
| 
						 | 
					ad1a5d3702 | ||
| 
						 | 
					3006b4184d | ||
| 
						 | 
					84eb84a080 | ||
| 
						 | 
					60beea548b | ||
| 
						 | 
					5f9c149e59 | ||
| 
						 | 
					53367c6f04 | ||
| 
						 | 
					d7f817ee44 | ||
| 
						 | 
					d33a87da54 | ||
| 
						 | 
					3aebfb12b7 | ||
| 
						 | 
					1d6c55ffa6 | ||
| 
						 | 
					5e7080aac3 | ||
| 
						 | 
					fad739bc01 | ||
| 
						 | 
					c6b7f23884 | ||
| 
						 | 
					a6f7e446de | ||
| 
						 | 
					89d95d3ae1 | ||
| 
						 | 
					764208698f | ||
| 
						 | 
					57129cf934 | ||
| 
						 | 
					aae1a842d5 | ||
| 
						 | 
					623f35aec7 | ||
| 
						 | 
					870bf842cf | ||
| 
						 | 
					07f2d7dd5c | ||
| 
						 | 
					f223f2edc5 | ||
| 
						 | 
					e848a9a577 | ||
| 
						 | 
					7569d98e07 | ||
| 
						 | 
					596dee2f24 | ||
| 
						 | 
					9970403964 | ||
| 
						 | 
					07a88ae00d | ||
| 
						 | 
					5475b4d287 | ||
| 
						 | 
					6631dcfd3e | ||
| 
						 | 
					0dd3f337f3 | ||
| 
						 | 
					8eb27b5875 | ||
| 
						 | 
					2d1863031c | ||
| 
						 | 
					9feb76ca81 | ||
| 
						 | 
					993e8f4ab3 | ||
| 
						 | 
					e08ae95d4f | ||
| 
						 | 
					15359e8846 | ||
| 
						 | 
					d1457b312b | ||
| 
						 | 
					c9dd2af196 | ||
| 
						 | 
					564ef4e688 | ||
| 
						 | 
					a33e6e8bb5 | ||
| 
						 | 
					cf34f33f04 | ||
| 
						 | 
					827cfe4e8f | ||
| 
						 | 
					2ce1c2383c | ||
| 
						 | 
					6fc0a665ae | ||
| 
						 | 
					4f16d01263 | ||
| 
						 | 
					67cc37354a | ||
| 
						 | 
					e388243ef4 | ||
| 
						 | 
					3dc92763c7 | ||
| 
						 | 
					dfe97dd466 | ||
| 
						 | 
					2803cee29b | ||
| 
						 | 
					3a03020e54 | ||
| 
						 | 
					64443cc703 | ||
| 
						 | 
					4d1aa6ed18 | ||
| 
						 | 
					84837e88d2 | ||
| 
						 | 
					ff49c936ea | ||
| 
						 | 
					e6e0901329 | ||
| 
						 | 
					23b6284b51 | ||
| 
						 | 
					33dfbcbe32 | ||
| 
						 | 
					700c23d537 | ||
| 
						 | 
					369fac9e38 | ||
| 
						 | 
					2229eb1167 | ||
| 
						 | 
					a3dec841b6 | ||
| 
						 | 
					b17620bdb6 | ||
| 
						 | 
					f39cd5ae2f | ||
| 
						 | 
					83a19e005b | ||
| 
						 | 
					a9dd01b0c8 | ||
| 
						 | 
					eb59afa1d1 | ||
| 
						 | 
					2adcfce9d0 | ||
| 
						 | 
					314ab9b304 | ||
| 
						 | 
					8576fb82c7 | ||
| 
						 | 
					0f95a6bb2f | ||
| 
						 | 
					ad5104567d | ||
| 
						 | 
					ece68ba1d5 | ||
| 
						 | 
					acccd3a586 | ||
| 
						 | 
					8ebef1c1ca | ||
| 
						 | 
					28abc0d5ed | ||
| 
						 | 
					1efe25d3ec | ||
| 
						 | 
					c40e4f8e4b | ||
| 
						 | 
					baca84092d | ||
| 
						 | 
					346d4da059 | ||
| 
						 | 
					ade64d6c0a | ||
| 
						 | 
					8204bdfc5f | ||
| 
						 | 
					1a9bb3e986 | ||
| 
						 | 
					49356479e5 | ||
| 
						 | 
					c44e9a7292 | ||
| 
						 | 
					21771a593f | ||
| 
						 | 
					84458dfc4c | ||
| 
						 | 
					5835632dab | ||
| 
						 | 
					67aa7229ef | ||
| 
						 | 
					b72dc3ed3a | ||
| 
						 | 
					0f93d4a5bd | ||
| 
						 | 
					106320b035 | ||
| 
						 | 
					63951705cd | ||
| 
						 | 
					a8d56921d5 | ||
| 
						 | 
					10bc133cf1 | ||
| 
						 | 
					adeb5b35c9 | ||
| 
						 | 
					589ff46ea5 | ||
| 
						 | 
					656fcb9fe7 | ||
| 
						 | 
					1cb9353006 | ||
| 
						 | 
					57bf16ba07 | ||
| 
						 | 
					659846ed88 | ||
| 
						 | 
					25894044e0 | ||
| 
						 | 
					e7a0826beb | ||
| 
						 | 
					1f7ddee23b | ||
| 
						 | 
					7e186730db | ||
| 
						 | 
					6713a50208 | ||
| 
						 | 
					7c9d8fcfec | ||
| 
						 | 
					33bfc8cfe8 | ||
| 
						 | 
					ca735bc14a | ||
| 
						 | 
					4ba748a18b | ||
| 
						 | 
					f1845106f8 | ||
| 
						 | 
					67e7156c4b | ||
| 
						 | 
					4a476adebf | ||
| 
						 | 
					918798f8cc | ||
| 
						 | 
					5a3f868866 | ||
| 
						 | 
					feea2c6396 | ||
| 
						 | 
					707b4c46d9 | ||
| 
						 | 
					89ca39fc2b | ||
| 
						 | 
					204281b12d | ||
| 
						 | 
					a8538a7e95 | ||
| 
						 | 
					dee1b471e9 | ||
| 
						 | 
					aa04e9b01f | ||
| 
						 | 
					350f0dc604 | ||
| 
						 | 
					6021f2efd6 | ||
| 
						 | 
					51838ec25a | ||
| 
						 | 
					54768a121e | ||
| 
						 | 
					8ff72cdca3 | ||
| 
						 | 
					2cb53ad06b | ||
| 
						 | 
					b8349de31d | ||
| 
						 | 
					d7e11af7f8 | ||
| 
						 | 
					dd8d39e698 | ||
| 
						 | 
					afb1316daa | ||
| 
						 | 
					04d7017536 | ||
| 
						 | 
					6a1c75b060 | ||
| 
						 | 
					5c94611f3b | ||
| 
						 | 
					4e5676e80f | ||
| 
						 | 
					c96d688a9c | ||
| 
						 | 
					804242e9a5 | ||
| 
						 | 
					0ec9760b17 | ||
| 
						 | 
					d481ae3da4 | ||
| 
						 | 
					4742c14fc1 | ||
| 
						 | 
					509b0d501b | ||
| 
						 | 
					d4c9b04d4e | ||
| 
						 | 
					16fb4d331b | ||
| 
						 | 
					e9e5bf31a7 | ||
| 
						 | 
					221418120e | ||
| 
						 | 
					46f852e26e | ||
| 
						 | 
					4234cf0a31 | ||
| 
						 | 
					7f3daea648 | ||
| 
						 | 
					2eb16c82f4 | ||
| 
						 | 
					e00b2ce591 | ||
| 
						 | 
					d71e1311ca | ||
| 
						 | 
					2cf16963e3 | ||
| 
						 | 
					10bf7b7fb4 | ||
| 
						 | 
					182c85a228 | ||
| 
						 | 
					94b1988b90 | ||
| 
						 | 
					6f7e62e9a0 | ||
| 
						 | 
					aa7076af04 | ||
| 
						 | 
					c928e8f0d4 | ||
| 
						 | 
					5c6b106f68 | ||
| 
						 | 
					d45bcea1ff | ||
| 
						 | 
					6ff2dc79f8 | ||
| 
						 | 
					b752329987 | ||
| 
						 | 
					f21465335a | ||
| 
						 | 
					0801adfc4b | ||
| 
						 | 
					5bee8052d5 | ||
| 
						 | 
					68dca5dfef | ||
| 
						 | 
					3f51dd1d2f | ||
| 
						 | 
					7f80889d77 | ||
| 
						 | 
					efc61c0222 | ||
| 
						 | 
					6fc0a05d34 | ||
| 
						 | 
					a9be872d7a | ||
| 
						 | 
					6ca85f099e | ||
| 
						 | 
					86ff677b8a | ||
| 
						 | 
					35e295df86 | ||
| 
						 | 
					cd4d301790 | ||
| 
						 | 
					93bb329c3d | ||
| 
						 | 
					7c1e0f2c30 | ||
| 
						 | 
					b57f471f44 | ||
| 
						 | 
					252a9a2ed6 | ||
| 
						 | 
					7258d4d787 | ||
| 
						 | 
					75522fa295 | ||
| 
						 | 
					4ba8f41d95 | ||
| 
						 | 
					f326f8e4de | ||
| 
						 | 
					f863dc058e | ||
| 
						 | 
					20891db251 | ||
| 
						 | 
					f1d05f1342 | ||
| 
						 | 
					8dd636b0eb | ||
| 
						 | 
					6b5bda8ee1 | ||
| 
						 | 
					ddc5597157 | ||
| 
						 | 
					ae112c7257 | ||
| 
						 | 
					c22f10f96a | ||
| 
						 | 
					18d10c9bec | ||
| 
						 | 
					890e430cb7 | ||
| 
						 | 
					dadc3d4cd7 | ||
| 
						 | 
					d98b4d7320 | ||
| 
						 | 
					340f532238 | ||
| 
						 | 
					7669f68e7c | ||
| 
						 | 
					3557e5514f | ||
| 
						 | 
					a9f09b7614 | ||
| 
						 | 
					845b9e4568 | ||
| 
						 | 
					24a6092dcf | ||
| 
						 | 
					195ae7d8b1 | ||
| 
						 | 
					a5c6ea7ffc | ||
| 
						 | 
					eb7a4ac29f | ||
| 
						 | 
					508ef73fde | ||
| 
						 | 
					838d6d8076 | ||
| 
						 | 
					762c3159b8 | ||
| 
						 | 
					7a88a06bcf | ||
| 
						 | 
					0b1e3d7de5 | ||
| 
						 | 
					9a83c73f21 | ||
| 
						 | 
					aa50c7b268 | ||
| 
						 | 
					179a5a80f4 | ||
| 
						 | 
					0ddae527ef | ||
| 
						 | 
					ee7a46de26 | ||
| 
						 | 
					95522fda74 | ||
| 
						 | 
					e58881c2bd | ||
| 
						 | 
					36a902a44e | ||
| 
						 | 
					16b74549a2 | ||
| 
						 | 
					da7ededfb1 | ||
| 
						 | 
					790bb08718 | ||
| 
						 | 
					e6765f421f | ||
| 
						 | 
					7e8f1fe904 | ||
| 
						 | 
					eacce4578a | ||
| 
						 | 
					07b2543972 | ||
| 
						 | 
					d1c3fc8493 | ||
| 
						 | 
					f453b16010 | ||
| 
						 | 
					05151d8978 | ||
| 
						 | 
					8218e1acc3 | ||
| 
						 | 
					30212fc89a | ||
| 
						 | 
					b31c13fcae | ||
| 
						 | 
					6b95fc6f1d | ||
| 
						 | 
					369cf17eb2 | ||
| 
						 | 
					4dd8f512cc | ||
| 
						 | 
					26cfec7d80 | ||
| 
						 | 
					67a87ccf00 | ||
| 
						 | 
					667cebcf94 | ||
| 
						 | 
					bc1747ca1c | ||
| 
						 | 
					945d8647bf | ||
| 
						 | 
					dfe2e94627 | ||
| 
						 | 
					09a5591eec | ||
| 
						 | 
					f2bf06a0ba | ||
| 
						 | 
					eedad4ab1c | ||
| 
						 | 
					336a62ab29 | ||
| 
						 | 
					b5603a5233 | ||
| 
						 | 
					73890f553c | ||
| 
						 | 
					f6243b8968 | ||
| 
						 | 
					3770dc74d4 | ||
| 
						 | 
					45f4e947c5 | ||
| 
						 | 
					9928d7c6e1 | ||
| 
						 | 
					bf776eeb2b | ||
| 
						 | 
					ae7c0e9195 | ||
| 
						 | 
					e90b640602 | ||
| 
						 | 
					ba7529d3f5 | ||
| 
						 | 
					34667f252e | ||
| 
						 | 
					d18bddcb7b | ||
| 
						 | 
					96dff49d33 | ||
| 
						 | 
					b389728338 | ||
| 
						 | 
					cdc7da86f3 | ||
| 
						 | 
					4745cc0378 | ||
| 
						 | 
					434f132479 | ||
| 
						 | 
					fb0f31ffc7 | ||
| 
						 | 
					bb1d73c0ae | ||
| 
						 | 
					0e823d1191 | ||
| 
						 | 
					48f4199ff3 | ||
| 
						 | 
					eaf379587b | ||
| 
						 | 
					672446b7d1 | ||
| 
						 | 
					dfe52c1b07 | ||
| 
						 | 
					d63df03ad8 | ||
| 
						 | 
					aba4f9f2ce | ||
| 
						 | 
					ac5c1e7803 | ||
| 
						 | 
					d521dbf50e | ||
| 
						 | 
					f210ed3e6a | ||
| 
						 | 
					df3cac4ea6 | ||
| 
						 | 
					f778c5175b | ||
| 
						 | 
					6c66ff28dd | ||
| 
						 | 
					d5b6ec702b | ||
| 
						 | 
					c62a5fcef2 | ||
| 
						 | 
					59c47e9200 | ||
| 
						 | 
					4ba44d8932 | ||
| 
						 | 
					27dae05e1b | ||
| 
						 | 
					a251ae9b90 | ||
| 
						 | 
					7e960b2bde | ||
| 
						 | 
					5df4825158 | ||
| 
						 | 
					8984d06d93 | ||
| 
						 | 
					eed7aac047 | ||
| 
						 | 
					54b068de4a | ||
| 
						 | 
					f0f33b00b6 | ||
| 
						 | 
					1043405088 | ||
| 
						 | 
					0131b10805 | ||
| 
						 | 
					a19b441f62 | ||
| 
						 | 
					28edc31d43 | ||
| 
						 | 
					0f9872a818 | ||
| 
						 | 
					76ce4296f3 | ||
| 
						 | 
					3dd2671380 | ||
| 
						 | 
					298ca31332 | ||
| 
						 | 
					8f911aa6b9 | ||
| 
						 | 
					82a5c7d9b1 | ||
| 
						 | 
					7f013dcdba | ||
| 
						 | 
					68e2e16076 | ||
| 
						 | 
					ea23c763c9 | ||
| 
						 | 
					5dcecb3206 | ||
| 
						 | 
					5bd48e2d0e | ||
| 
						 | 
					afd0a02589 | ||
| 
						 | 
					2379192d53 | ||
| 
						 | 
					a6489290c8 | ||
| 
						 | 
					5f74c43415 | ||
| 
						 | 
					aa8b84a302 | ||
| 
						 | 
					b987d041b0 | ||
| 
						 | 
					b62e37307e | ||
| 
						 | 
					61a59aa6ac | ||
| 
						 | 
					f79ec27f1d | ||
| 
						 | 
					b993fe380f | ||
| 
						 | 
					d974b5f55f | ||
| 
						 | 
					f21ae93197 | ||
| 
						 | 
					342ff18be8 | ||
| 
						 | 
					a8236f69bf | ||
| 
						 | 
					ab15a2448d | ||
| 
						 | 
					6ff4d8f558 | ||
| 
						 | 
					bb04ba528c | ||
| 
						 | 
					b94a795189 | ||
| 
						 | 
					9968184733 | ||
| 
						 | 
					1be6f8f87a | ||
| 
						 | 
					426821cceb | ||
| 
						 | 
					4fec0deaf7 | ||
| 
						 | 
					144ac5b6ce | ||
| 
						 | 
					97c73786fa | ||
| 
						 | 
					82e59d7da0 | ||
| 
						 | 
					b2c10de6af | ||
| 
						 | 
					d72029c2c6 | ||
| 
						 | 
					17b9987063 | ||
| 
						 | 
					fde07da2b7 | ||
| 
						 | 
					c23bc29511 | ||
| 
						 | 
					714cad2a52 | ||
| 
						 | 
					357d5d2fde | ||
| 
						 | 
					d477cce901 | ||
| 
						 | 
					eb6af52ad1 | ||
| 
						 | 
					aae75023a7 | ||
| 
						 | 
					41dcd4f458 | ||
| 
						 | 
					4651ae4495 | ||
| 
						 | 
					ed61e0b0fc | ||
| 
						 | 
					1eefc6fbf4 | ||
| 
						 | 
					09ebf2cea2 | ||
| 
						 | 
					b3b0c4cd65 | ||
| 
						 | 
					f4b7924e8f | ||
| 
						 | 
					ea68d38b82 | ||
| 
						 | 
					dfbaa71132 | ||
| 
						 | 
					6c328deb08 | ||
| 
						 | 
					add564d5bf | ||
| 
						 | 
					fa94acb426 | ||
| 
						 | 
					6827468f13 | ||
| 
						 | 
					53fd43868f | ||
| 
						 | 
					9ced7561c5 | ||
| 
						 | 
					31d55d3425 | ||
| 
						 | 
					171d2a5bb9 | ||
| 
						 | 
					c5d05c1205 | ||
| 
						 | 
					2973e0559a | ||
| 
						 | 
					ec27288dcf | ||
| 
						 | 
					f92e5c7093 | ||
| 
						 | 
					7c67155c49 | ||
| 
						 | 
					b102cd4652 | ||
| 
						 | 
					67f9a48c37 | ||
| 
						 | 
					a0c8a1ee65 | ||
| 
						 | 
					7e7d272b06 | ||
| 
						 | 
					3c642240ae | ||
| 
						 | 
					b5157fcaf1 | ||
| 
						 | 
					d1cb42f1bc | ||
| 
						 | 
					84cde1a16a | ||
| 
						 | 
					877f5db1ce | ||
| 
						 | 
					787164e245 | ||
| 
						 | 
					d77fc5e7c5 | ||
| 
						 | 
					cca39a67d6 | ||
| 
						 | 
					a6c9a0431a | ||
| 
						 | 
					729a80a639 | ||
| 
						 | 
					31cb3001f6 | ||
| 
						 | 
					5d0f54a329 | ||
| 
						 | 
					c8c3f5b5b7 | ||
| 
						 | 
					ba473ed75a | ||
| 
						 | 
					7236fd59f8 | ||
| 
						 | 
					9471e8f1fd | ||
| 
						 | 
					a2d39b51bb | ||
| 
						 | 
					2920934b55 | ||
| 
						 | 
					3f709d448e | ||
| 
						 | 
					b79f66183f | ||
| 
						 | 
					8672f57e55 | ||
| 
						 | 
					1e99c82351 | ||
| 
						 | 
					1a2ff851f3 | ||
| 
						 | 
					f1c27c3959 | ||
| 
						 | 
					b30dac0f15 | ||
| 
						 | 
					cc79e5cdaf | ||
| 
						 | 
					d9a3b2f2cb | ||
| 
						 | 
					479b528d09 | ||
| 
						 | 
					461fb84fb9 | ||
| 
						 | 
					bd7685e3fa | ||
| 
						 | 
					cd98cb64b3 | ||
| 
						 | 
					0f32a3ec24 | ||
| 
						 | 
					ca446cac87 | ||
| 
						 | 
					6ea907ffda | ||
| 
						 | 
					5287baa70d | ||
| 
						 | 
					25935fec84 | ||
| 
						 | 
					e855a063ff | ||
| 
						 | 
					c726b8c9f0 | ||
| 
						 | 
					13cb99290e | ||
| 
						 | 
					cea9413fd1 | ||
| 
						 | 
					1432853b39 | ||
| 
						 | 
					6d6c2b86e8 | ||
| 
						 | 
					77b1d964b5 | ||
| 
						 | 
					549936fc09 | ||
| 
						 | 
					c9c32f09c5 | ||
| 
						 | 
					77f7778d4a | ||
| 
						 | 
					84b6be9364 | ||
| 
						 | 
					1e43b55804 | ||
| 
						 | 
					ba9bdaae0a | ||
| 
						 | 
					7dfd7bde8e | ||
| 
						 | 
					5e6c4161d0 | ||
| 
						 | 
					d75d56dfc9 | ||
| 
						 | 
					1d9d350091 | ||
| 
						 | 
					5744053c6f | ||
| 
						 | 
					65589b6ca2 | ||
| 
						 | 
					e03a9d1137 | ||
| 
						 | 
					29f80f2276 | ||
| 
						 | 
					a9b74aa69b | ||
| 
						 | 
					63ebfd3210 | ||
| 
						 | 
					87fa5ff7a6 | ||
| 
						 | 
					b686b53a9c | ||
| 
						 | 
					258261dc64 | ||
| 
						 | 
					9af5c9ead9 | ||
| 
						 | 
					382654188c | ||
| 
						 | 
					fa1df082b7 | ||
| 
						 | 
					5c227d8f80 | ||
| 
						 | 
					81dabdbfb7 | ||
| 
						 | 
					91f89f5a33 | ||
| 
						 | 
					9f92746aa0 | ||
| 
						 | 
					5d6e6f9441 | ||
| 
						 | 
					01395a2726 | ||
| 
						 | 
					465d75c65d | ||
| 
						 | 
					4634f8927e | ||
| 
						 | 
					74a287f9fe | ||
| 
						 | 
					7ff6c79835 | ||
| 
						 | 
					3629982237 | ||
| 
						 | 
					ddb610f1bc | ||
| 
						 | 
					f899905d27 | ||
| 
						 | 
					3e4531b5c5 | ||
| 
						 | 
					a9e189e51d | ||
| 
						 | 
					58ba08a8f3 | ||
| 
						 | 
					9078ff27d8 | ||
| 
						 | 
					6f43e61c24 | ||
| 
						 | 
					4be0d3f212 | ||
| 
						 | 
					00e47e5a27 | ||
| 
						 | 
					152e145b32 | ||
| 
						 | 
					54e55e8f57 | ||
| 
						 | 
					05b8707f9e | ||
| 
						 | 
					543e952023 | ||
| 
						 | 
					6e5f40ea06 | ||
| 
						 | 
					bbafb0be87 | ||
| 
						 | 
					1c9c5232fe | ||
| 
						 | 
					598d79a502 | ||
| 
						 | 
					37d8360b77 | ||
| 
						 | 
					82d9ca3317 | ||
| 
						 | 
					4e4238d486 | ||
| 
						 | 
					c77dbe44dc | ||
| 
						 | 
					e03737f15f | ||
| 
						 | 
					a02629bcd7 | ||
| 
						 | 
					6c3fc23d78 | ||
| 
						 | 
					0fe40f9ccb | ||
| 
						 | 
					9bd7c8edd1 | ||
| 
						 | 
					83ba480863 | ||
| 
						 | 
					f158ea25e9 | ||
| 
						 | 
					0227519eab | ||
| 
						 | 
					616a9685fa | ||
| 
						 | 
					fe61b01320 | ||
| 
						 | 
					7b25144311 | ||
| 
						 | 
					9d42fbbdd7 | ||
| 
						 | 
					39ac5b088b | ||
| 
						 | 
					c14ffd08a0 | ||
| 
						 | 
					6e1239340b | ||
| 
						 | 
					a297dc8b3b | ||
| 
						 | 
					8d4ecc0898 | ||
| 
						 | 
					eae9c04429 | ||
| 
						 | 
					a41c48a9c5 | ||
| 
						 | 
					ff2a94bd9b | ||
| 
						 | 
					4a1f5558b8 | ||
| 
						 | 
					608db9889f | ||
| 
						 | 
					012b697337 | ||
| 
						 | 
					0580506cf3 | ||
| 
						 | 
					ff4ab9b661 | ||
| 
						 | 
					b7ce5fdd3e | ||
| 
						 | 
					a11e617322 | ||
| 
						 | 
					d0beac7e2b | ||
| 
						 | 
					9db497092f | ||
| 
						 | 
					8eb91c08aa | ||
| 
						 | 
					ded5437522 | ||
| 
						 | 
					9348657951 | ||
| 
						 | 
					bca85933f7 | ||
| 
						 | 
					c32bb35f1c | ||
| 
						 | 
					4b84062d62 | ||
| 
						 | 
					d6d0f8fa17 | ||
| 
						 | 
					dd72c875d3 | ||
| 
						 | 
					1a1df50300 | ||
| 
						 | 
					53cbb527b4 | ||
| 
						 | 
					8b87b2717e | ||
| 
						 | 
					1007d6dac7 | ||
| 
						 | 
					6799fac120 | ||
| 
						 | 
					558e6288ca | ||
| 
						 | 
					d9cb73291b | ||
| 
						 | 
					d0f7be3ac3 | ||
| 
						 | 
					331e16d3ca | ||
| 
						 | 
					0db246c311 | ||
| 
						 | 
					94dc62ff58 | ||
| 
						 | 
					e68ecf6844 | ||
| 
						 | 
					5167b0a8c6 | ||
| 
						 | 
					77e3d3786d | ||
| 
						 | 
					708d4d39bc | ||
| 
						 | 
					2a8cda2a1e | ||
| 
						 | 
					8d783840ad | ||
| 
						 | 
					abe39d5790 | ||
| 
						 | 
					d7868e9e5a | ||
| 
						 | 
					7b84e36e15 | ||
| 
						 | 
					6cab6d69d8 | ||
| 
						 | 
					87846d7aef | ||
| 
						 | 
					2557769c6a | ||
| 
						 | 
					48375f3878 | ||
| 
						 | 
					176c85d8c1 | ||
| 
						 | 
					17cad71ede | ||
| 
						 | 
					e8bf9d4e6f | ||
| 
						 | 
					7bdd2038ef | ||
| 
						 | 
					e9f6e7943a | ||
| 
						 | 
					e74ba387ab | ||
| 
						 | 
					27c79e5b99 | ||
| 
						 | 
					8170d5ea73 | ||
| 
						 | 
					196f73705d | ||
| 
						 | 
					ad0bbf5248 | ||
| 
						 | 
					4cae9cd90d | ||
| 
						 | 
					be7bc55a76 | ||
| 
						 | 
					684b545e8f | ||
| 
						 | 
					7835cc3b10 | ||
| 
						 | 
					f8706b51e8 | ||
| 
						 | 
					d97f8fd5da | ||
| 
						 | 
					f8fa87441e | ||
| 
						 | 
					d42537814a | ||
| 
						 | 
					792421b0e2 | ||
| 
						 | 
					72d55a010b | ||
| 
						 | 
					880d8258ce | ||
| 
						 | 
					b79bf82efb | ||
| 
						 | 
					b3118b6253 | ||
| 
						 | 
					ba172e2e25 | ||
| 
						 | 
					892d53abeb | ||
| 
						 | 
					5cbaa1ce98 | ||
| 
						 | 
					7b35d9ad2e | ||
| 
						 | 
					8462de7911 | ||
| 
						 | 
					8721f44298 | ||
| 
						 | 
					c7a2d69afa | ||
| 
						 | 
					0453d81e7a | ||
| 
						 | 
					501c04ac2b | ||
| 
						 | 
					0ef4e9a5c3 | ||
| 
						 | 
					129c50e598 | ||
| 
						 | 
					3e276fc2ac | ||
| 
						 | 
					658d5e05ae | ||
| 
						 | 
					4e7d5d476e | ||
| 
						 | 
					6a55ca20f3 | ||
| 
						 | 
					c56c537f7f | ||
| 
						 | 
					fd7d776121 | ||
| 
						 | 
					1af28190d8 | ||
| 
						 | 
					6b305be567 | ||
| 
						 | 
					3bf70513b7 | ||
| 
						 | 
					7e64404654 | ||
| 
						 | 
					e1b5226f34 | ||
| 
						 | 
					0d7128ad31 | ||
| 
						 | 
					5778626087 | ||
| 
						 | 
					3ff48756ed | ||
| 
						 | 
					0ce9a6eeba | ||
| 
						 | 
					ad527b4aed | ||
| 
						 | 
					6633bb452e | ||
| 
						 | 
					efeb0b4feb | ||
| 
						 | 
					8cc11fc102 | ||
| 
						 | 
					ee6a167220 | ||
| 
						 | 
					8d4ad3c405 | ||
| 
						 | 
					072fbf4d60 | ||
| 
						 | 
					727c41c283 | ||
| 
						 | 
					e2266838b6 | ||
| 
						 | 
					775762d615 | ||
| 
						 | 
					900c3008cb | ||
| 
						 | 
					09379213a6 | ||
| 
						 | 
					ceb97048e3 | ||
| 
						 | 
					4561515517 | ||
| 
						 | 
					a7b285759f | ||
| 
						 | 
					b4531b2a12 | ||
| 
						 | 
					9e1d261c76 | ||
| 
						 | 
					e35fa15cd2 | ||
| 
						 | 
					dbd1f0d4f9 | ||
| 
						 | 
					9ade78b703 | ||
| 
						 | 
					f20e244b5f | ||
| 
						 | 
					0989308b7e | ||
| 
						 | 
					12c7140536 | ||
| 
						 | 
					2a0b605e92 | ||
| 
						 | 
					6978890e6a | ||
| 
						 | 
					561abd6cb9 | ||
| 
						 | 
					4dd6227f0b | ||
| 
						 | 
					1ec314c31c | ||
| 
						 | 
					a2be5a00be | ||
| 
						 | 
					4e2241c115 | ||
| 
						 | 
					8459bca64a | ||
| 
						 | 
					24cb0565b9 | ||
| 
						 | 
					9442acb028 | ||
| 
						 | 
					4f7f181a42 | ||
| 
						 | 
					b7dd8737a7 | ||
| 
						 | 
					2207eeb727 | ||
| 
						 | 
					89dad7dfe7 | ||
| 
						 | 
					e5803d0cf3 | ||
| 
						 | 
					c1fffe9ae6 | ||
| 
						 | 
					9e6cbd3d32 | ||
| 
						 | 
					2ea8742510 | ||
| 
						 | 
					5cfa0254f9 | ||
| 
						 | 
					8cd2544f78 | ||
| 
						 | 
					c03b768364 | ||
| 
						 | 
					d60481ead4 | ||
| 
						 | 
					126be3827d | ||
| 
						 | 
					121274dca2 | ||
| 
						 | 
					0ecf8da27e | ||
| 
						 | 
					4a6bcb525d | ||
| 
						 | 
					83f9ee50dd | ||
| 
						 | 
					2bff297f79 | ||
| 
						 | 
					dee68f6933 | ||
| 
						 | 
					afa1e19c83 | ||
| 
						 | 
					6052088eb4 | ||
| 
						 | 
					c7fa5167c4 | ||
| 
						 | 
					1034b0b146 | ||
| 
						 | 
					8bcc4e5945 | ||
| 
						 | 
					c3c24aa1db | ||
| 
						 | 
					281c75d2d2 | ||
| 
						 | 
					52307420f3 | ||
| 
						 | 
					6185347cd8 | ||
| 
						 | 
					b6cd29f77e | ||
| 
						 | 
					b8ea8b1567 | ||
| 
						 | 
					2f7dc98830 | ||
| 
						 | 
					e248a99f79 | ||
| 
						 | 
					4fb6d9aa5d | ||
| 
						 | 
					f092ea8d67 | ||
| 
						 | 
					c32cbbdda6 | ||
| 
						 | 
					2497675259 | ||
| 
						 | 
					8d084ab90a | ||
| 
						 | 
					2398773ef0 | ||
| 
						 | 
					a05998a30e | ||
| 
						 | 
					f863c29194 | ||
| 
						 | 
					d16a98c788 | ||
| 
						 | 
					9421b02e96 | ||
| 
						 | 
					10256864e4 | ||
| 
						 | 
					85d010615d | ||
| 
						 | 
					cd1cb186be | ||
| 
						 | 
					4458354d70 | ||
| 
						 | 
					0f27da8808 | ||
| 
						 | 
					dd76bfa3c2 | ||
| 
						 | 
					5780a66f7d | ||
| 
						 | 
					d4342c034c | ||
| 
						 | 
					1ec43f2530 | ||
| 
						 | 
					3c300d8fdf | ||
| 
						 | 
					23119b55d1 | ||
| 
						 | 
					c8fb0e8f8a | ||
| 
						 | 
					0ec32a77ef | ||
| 
						 | 
					52921bfce8 | ||
| 
						 | 
					960b929097 | ||
| 
						 | 
					d4ce23eced | ||
| 
						 | 
					6925510f44 | ||
| 
						 | 
					9827ad4c22 | ||
| 
						 | 
					ef8aaee028 | ||
| 
						 | 
					3d7d39f248 | ||
| 
						 | 
					3eac620560 | ||
| 
						 | 
					ab17006956 | ||
| 
						 | 
					bfc6889ee9 | ||
| 
						 | 
					0ec0b4a044 | ||
| 
						 | 
					f1a523f327 | ||
| 
						 | 
					4181449aea | ||
| 
						 | 
					e192f8db52 | ||
| 
						 | 
					8097c681ac | ||
| 
						 | 
					f45938bdd5 | ||
| 
						 | 
					6ea4e97eca | ||
| 
						 | 
					f274c8e837 | ||
| 
						 | 
					335e571485 | ||
| 
						 | 
					a11616aace | ||
| 
						 | 
					883acadbc4 | ||
| 
						 | 
					f51e6a3fcf | ||
| 
						 | 
					371e081c0d | ||
| 
						 | 
					6f41b3bf1c | ||
| 
						 | 
					c1d74a6c9e | ||
| 
						 | 
					24eaa6796e | ||
| 
						 | 
					1521e3b620 | ||
| 
						 | 
					b6ff38dd62 | ||
| 
						 | 
					44ea9ac03c | ||
| 
						 | 
					4c2701505b | ||
| 
						 | 
					9022fe18da | ||
| 
						 | 
					63be349f8b | ||
| 
						 | 
					c40256a290 | ||
| 
						 | 
					33ecb8ec52 | ||
| 
						 | 
					82d62a0015 | ||
| 
						 | 
					6278240526 | ||
| 
						 | 
					8c2dc5f57d | ||
| 
						 | 
					2e5868778a | ||
| 
						 | 
					a10b8dab9b | ||
| 
						 | 
					92f4f7ef59 | ||
| 
						 | 
					31257bd5cb | ||
| 
						 | 
					bb6510862f | ||
| 
						 | 
					797ecf0780 | ||
| 
						 | 
					f9536dc67f | ||
| 
						 | 
					e8b95362af | ||
| 
						 | 
					bdc39ad4ec | ||
| 
						 | 
					4a202c5585 | ||
| 
						 | 
					3c6b321f73 | ||
| 
						 | 
					cb29b52799 | ||
| 
						 | 
					7e48015a54 | ||
| 
						 | 
					9ed3abf932 | ||
| 
						 | 
					61762828a3 | ||
| 
						 | 
					59beabe5ac | ||
| 
						 | 
					0b30faa28c | ||
| 
						 | 
					d12d49b93f | ||
| 
						 | 
					f1d64d275a | ||
| 
						 | 
					d094eeeb03 | ||
| 
						 | 
					be25af658e | ||
| 
						 | 
					794f52c229 | ||
| 
						 | 
					5d4dc4ed4c | ||
| 
						 | 
					e49d97b898 | ||
| 
						 | 
					b6b4f1ba62 | ||
| 
						 | 
					653d476716 | ||
| 
						 | 
					48b855258c | ||
| 
						 | 
					c7efdaf5f9 | ||
| 
						 | 
					22523ed3d3 | ||
| 
						 | 
					33c602dd61 | ||
| 
						 | 
					e2a5509b76 | ||
| 
						 | 
					61a0fa1a89 | ||
| 
						 | 
					a35bd8292b | ||
| 
						 | 
					06c8ae60e3 | ||
| 
						 | 
					deeab1f845 | ||
| 
						 | 
					da81c4c987 | ||
| 
						 | 
					d180f1b2d5 | ||
| 
						 | 
					526135629c | ||
| 
						 | 
					6b9493e057 | ||
| 
						 | 
					9bb33d2afc | ||
| 
						 | 
					7421138533 | ||
| 
						 | 
					d0800c52bb | ||
| 
						 | 
					913fcd4df2 | ||
| 
						 | 
					83322cc725 | ||
| 
						 | 
					5944501feb | ||
| 
						 | 
					17e3603d3d | ||
| 
						 | 
					95be43ae47 | ||
| 
						 | 
					feb91cbbaa | ||
| 
						 | 
					79409af168 | ||
| 
						 | 
					5dbfb64822 | ||
| 
						 | 
					5e7ebf5e69 | ||
| 
						 | 
					e73215ca74 | ||
| 
						 | 
					a5f123b9ce | ||
| 
						 | 
					ac058e9675 | ||
| 
						 | 
					371b764d1d | ||
| 
						 | 
					66d7172e09 | ||
| 
						 | 
					99d3a8a749 | ||
| 
						 | 
					db5ff372a4 | ||
| 
						 | 
					3fe83f81be | ||
| 
						 | 
					669e638fd6 | ||
| 
						 | 
					f1f999f3b6 | ||
| 
						 | 
					6f3b6fa9ce | ||
| 
						 | 
					938f945301 | ||
| 
						 | 
					e3efb2aad6 | ||
| 
						 | 
					1e678c0d78 | ||
| 
						 | 
					a59c111140 | ||
| 
						 | 
					a8b2a31bed | ||
| 
						 | 
					37402f9ee8 | ||
| 
						 | 
					e7b5ecb40f | ||
| 
						 | 
					c817ef04b9 | ||
| 
						 | 
					f52b18439c | ||
| 
						 | 
					1e03c628d5 | ||
| 
						 | 
					71fb39db1f | ||
| 
						 | 
					bcfb3726b0 | ||
| 
						 | 
					c6e9e29671 | ||
| 
						 | 
					1bfefcce39 | ||
| 
						 | 
					22488e93e1 | ||
| 
						 | 
					244b89f035 | ||
| 
						 | 
					1f9a241b94 | ||
| 
						 | 
					03641aae42 | ||
| 
						 | 
					a2bdd113cc | ||
| 
						 | 
					a92e2f3c7b | ||
| 
						 | 
					97766b3a57 | ||
| 
						 | 
					9ef4c3bb06 | ||
| 
						 | 
					d82f0cd757 | ||
| 
						 | 
					5f529e2af4 | ||
| 
						 | 
					beadd9e02b | ||
| 
						 | 
					72543789cb | ||
| 
						 | 
					5789439fa9 | ||
| 
						 | 
					f549126bcf | ||
| 
						 | 
					7197548bad | ||
| 
						 | 
					241fde783c | ||
| 
						 | 
					2b872cd1f4 | ||
| 
						 | 
					a606fb4d1d | ||
| 
						 | 
					9f9c6be38e | ||
| 
						 | 
					01ee524049 | ||
| 
						 | 
					af9cb65338 | ||
| 
						 | 
					8aa11c580b | ||
| 
						 | 
					ada627f444 | ||
| 
						 | 
					a7b6d338c3 | ||
| 
						 | 
					9f00538b97 | ||
| 
						 | 
					a085015282 | ||
| 
						 | 
					0b9c220fbb | ||
| 
						 | 
					0e3d04873d | ||
| 
						 | 
					b7578d939f | ||
| 
						 | 
					b5c28de03f | ||
| 
						 | 
					e17d25c156 | ||
| 
						 | 
					c25dc1b99c | ||
| 
						 | 
					a493a574bd | ||
| 
						 | 
					4284493dce | ||
| 
						 | 
					25059de8e1 | ||
| 
						 | 
					1731b05ad0 | ||
| 
						 | 
					e80dc663ac | ||
| 
						 | 
					39988a4c2f | ||
| 
						 | 
					415bff303a | ||
| 
						 | 
					a65eb62a54 | ||
| 
						 | 
					03b2982128 | ||
| 
						 | 
					bff0527857 | ||
| 
						 | 
					f3b7634254 | ||
| 
						 | 
					6a9593c0b9 | ||
| 
						 | 
					edb785b8e5 | ||
| 
						 | 
					26d757b50a | ||
| 
						 | 
					535079ee87 | ||
| 
						 | 
					ac380c29c1 | ||
| 
						 | 
					3fd212f26c | ||
| 
						 | 
					04a3abc651 | ||
| 
						 | 
					6caf85ddd1 | ||
| 
						 | 
					16e4071508 | ||
| 
						 | 
					69e7c4324b | ||
| 
						 | 
					a1c4a8cbe5 | ||
| 
						 | 
					e37f6cfda7 | ||
| 
						 | 
					989c804409 | ||
| 
						 | 
					7345bc3c82 | ||
| 
						 | 
					69bee35700 | ||
| 
						 | 
					598e24df7c | ||
| 
						 | 
					0ae669201e | ||
| 
						 | 
					f52a8a4642 | ||
| 
						 | 
					9c40b61ef2 | ||
| 
						 | 
					72dabcda83 | ||
| 
						 | 
					161a06dbcc | ||
| 
						 | 
					8ed3d4e70c | ||
| 
						 | 
					a4223ccc8a | ||
| 
						 | 
					ca85923855 | ||
| 
						 | 
					52bfe7c493 | ||
| 
						 | 
					4786bd0cbe | ||
| 
						 | 
					cadab160ff | ||
| 
						 | 
					6a7f17b2b0 | ||
| 
						 | 
					4986a4d775 | ||
| 
						 | 
					903af0c2cf | ||
| 
						 | 
					3282fa803c | ||
| 
						 | 
					67cc47608d | ||
| 
						 | 
					0411704b8b | ||
| 
						 | 
					1de85b2c69 | ||
| 
						 | 
					33b012f29d | ||
| 
						 | 
					1357584df3 | ||
| 
						 | 
					e15809e271 | ||
| 
						 | 
					0da1950427 | ||
| 
						 | 
					e590b921be | ||
| 
						 | 
					09462692f5 | ||
| 
						 | 
					c1d1b5f762 | ||
| 
						 | 
					6b9c87b858 | ||
| 
						 | 
					485b6eb904 | ||
| 
						 | 
					057630bdb5 | ||
| 
						 | 
					6b02873b30 | ||
| 
						 | 
					0fa0fc6d6b | ||
| 
						 | 
					339ec07465 | ||
| 
						 | 
					cd2e798fea | ||
| 
						 | 
					d5cadbeae2 | ||
| 
						 | 
					8046a3ccae | ||
| 
						 | 
					bf91d60b31 | ||
| 
						 | 
					539c047ec8 | ||
| 
						 | 
					290c18fa87 | ||
| 
						 | 
					98c46f5e57 | ||
| 
						 | 
					f8bd5b5b4e | ||
| 
						 | 
					816d32edad | ||
| 
						 | 
					8453835c05 | ||
| 
						 | 
					9328c356c8 | ||
| 
						 | 
					89e3c1fc94 | ||
| 
						 | 
					67e54cd15d | ||
| 
						 | 
					278ea24786 | ||
| 
						 | 
					aba1662631 | ||
| 
						 | 
					61eeb60c19 | ||
| 
						 | 
					5e9a8f4806 | ||
| 
						 | 
					4cb274e9bc | ||
| 
						 | 
					8b9b1a6a35 | ||
| 
						 | 
					2655964113 | ||
| 
						 | 
					188bad061b | ||
| 
						 | 
					3af4c329aa | ||
| 
						 | 
					6c13395f7d | ||
| 
						 | 
					77b32ba360 | ||
| 
						 | 
					91dba291ac | ||
| 
						 | 
					a6bc293640 | ||
| 
						 | 
					53882d6e5f | ||
| 
						 | 
					d68adfbf10 | ||
| 
						 | 
					498a392d7f | ||
| 
						 | 
					740f6c05db | ||
| 
						 | 
					d810ce301f | ||
| 
						 | 
					5ef6a14d24 | ||
| 
						 | 
					a13f6f1e68 | ||
| 
						 | 
					d2d0f1aaee | ||
| 
						 | 
					e64c72cc89 | ||
| 
						 | 
					9ab915a08b | ||
| 
						 | 
					e26fbf0328 | ||
| 
						 | 
					d9a52c4a2a | ||
| 
						 | 
					7b2ec90de9 | ||
| 
						 | 
					d310bf8bbf | ||
| 
						 | 
					2abc6cc939 | ||
| 
						 | 
					56d4e694a2 | ||
| 
						 | 
					5f002c9cdc | ||
| 
						 | 
					759daf4b4a | ||
| 
						 | 
					3a8d9568e3 | ||
| 
						 | 
					ff22a9d94a | ||
| 
						 | 
					a6e42d5374 | ||
| 
						 | 
					a2f74e0488 | ||
| 
						 | 
					ee44240569 | ||
| 
						 | 
					d0828744a2 | ||
| 
						 | 
					6e2e576b29 | ||
| 
						 | 
					bf61e27f8a | ||
| 
						 | 
					c441c30b46 | ||
| 
						 | 
					0e741230ea | ||
| 
						 | 
					1bfe9ac2db | ||
| 
						 | 
					6812e72348 | ||
| 
						 | 
					b6449d2f5b | ||
| 
						 | 
					7e3ea20dce | ||
| 
						 | 
					c9d6fe9dcd | ||
| 
						 | 
					4a649a6b8b | ||
| 
						 | 
					8fef184963 | ||
| 
						 | 
					69583ca3c0 | ||
| 
						 | 
					6038a68e91 | ||
| 
						 | 
					fa8bd8db87 | ||
| 
						 | 
					18b4f0ed0f | ||
| 
						 | 
					461f9d66c9 | ||
| 
						 | 
					2155103c7a | ||
| 
						 | 
					c9a6839c45 | ||
| 
						 | 
					9fbe331a80 | ||
| 
						 | 
					a56389c4ce | ||
| 
						 | 
					64656784cb | ||
| 
						 | 
					6eff2c181e | ||
| 
						 | 
					1aa48c6d62 | ||
| 
						 | 
					c7ca1a346d | ||
| 
						 | 
					fa0ec7b502 | ||
| 
						 | 
					768438c136 | ||
| 
						 | 
					9badea0b3c | ||
| 
						 | 
					43263a1650 | ||
| 
						 | 
					821e02dc75 | ||
| 
						 | 
					ed011ecf28 | ||
| 
						 | 
					d861de4c2f | ||
| 
						 | 
					3a3b2449dc | ||
| 
						 | 
					d2614406ca | ||
| 
						 | 
					0798d098ae | ||
| 
						 | 
					dab7ddc2bb | ||
| 
						 | 
					081a96e281 | ||
| 
						 | 
					a7dd881d79 | ||
| 
						 | 
					8134d5e24d | ||
| 
						 | 
					ba6756cd45 | ||
| 
						 | 
					5d8fce21ac | ||
| 
						 | 
					e7e4a5bcd4 | ||
| 
						 | 
					55f33357ea | ||
| 
						 | 
					90568bba31 | ||
| 
						 | 
					5d6e2dc2e4 | ||
| 
						 | 
					6bb33f2559 | ||
| 
						 | 
					ced92554ed | ||
| 
						 | 
					dff3383158 | ||
| 
						 | 
					bf03c89cb2 | ||
| 
						 | 
					9f1484bbef | ||
| 
						 | 
					3899680e26 | ||
| 
						 | 
					6bb2eb25a1 | ||
| 
						 | 
					f8dfd8edb3 | ||
| 
						 | 
					042be624a3 | ||
| 
						 | 
					6bafa4c79a | ||
| 
						 | 
					58b42fac5c | ||
| 
						 | 
					3b47b9558a | ||
| 
						 | 
					ccf9636296 | ||
| 
						 | 
					96942719f2 | ||
| 
						 | 
					69cf1c1adc | ||
| 
						 | 
					d77cba40b8 | ||
| 
						 | 
					968735b555 | ||
| 
						 | 
					ceed9d29eb | ||
| 
						 | 
					41329039ee | ||
| 
						 | 
					f68b102ca8 | ||
| 
						 | 
					fa36e54298 | ||
| 
						 | 
					b689f57435 | ||
| 
						 | 
					885fa0ff56 | ||
| 
						 | 
					303acb72a3 | ||
| 
						 | 
					b2a46cd0cd | ||
| 
						 | 
					5a5ecb3ee3 | ||
| 
						 | 
					60b4ab6a63 | ||
| 
						 | 
					e4b096a08f | ||
| 
						 | 
					343f55049b | ||
| 
						 | 
					6b46025261 | ||
| 
						 | 
					5ea503f23e | ||
| 
						 | 
					ce95f9ac23 | ||
| 
						 | 
					c3fb87501b | ||
| 
						 | 
					dc6a343612 | ||
| 
						 | 
					3a61053957 | ||
| 
						 | 
					570129e4d4 | ||
| 
						 | 
					3315c7045f | ||
| 
						 | 
					5ae50e242c | ||
| 
						 | 
					bbcf449719 | ||
| 
						 | 
					aab10f7184 | ||
| 
						 | 
					8d43488cb8 | ||
| 
						 | 
					0a9c647e19 | ||
| 
						 | 
					40db5d4aa8 | ||
| 
						 | 
					9254532baa | ||
| 
						 | 
					7abed47cf0 | ||
| 
						 | 
					5c6ac758f7 | ||
| 
						 | 
					007677962c | ||
| 
						 | 
					9c4aeab64a | ||
| 
						 | 
					48e6fc0efe | ||
| 
						 | 
					c8be713d11 | ||
| 
						 | 
					ae887c8648 | ||
| 
						 | 
					5daac2531b | ||
| 
						 | 
					68def00327 | ||
| 
						 | 
					67e7976710 | ||
| 
						 | 
					35747e937e | ||
| 
						 | 
					fb439787a4 | ||
| 
						 | 
					8fa368f473 | ||
| 
						 | 
					c84a9d07b1 | ||
| 
						 | 
					7fb46cdfc4 | ||
| 
						 | 
					52985e5ddc | ||
| 
						 | 
					e880935dc3 | ||
| 
						 | 
					cc22b1bca5 | ||
| 
						 | 
					49a5128918 | ||
| 
						 | 
					fedc7dcb44 | ||
| 
						 | 
					cd32b20215 | ||
| 
						 | 
					15cd9832c4 | ||
| 
						 | 
					f25d4e4553 | ||
| 
						 | 
					12d1c82b63 | ||
| 
						 | 
					aebe855078 | ||
| 
						 | 
					3416a71ebd | ||
| 
						 | 
					94b3fea528 | ||
| 
						 | 
					ad1a9ecca1 | ||
| 
						 | 
					715accfb8a | ||
| 
						 | 
					a8e03c6138 | ||
| 
						 | 
					f69446b648 | ||
| 
						 | 
					eedfbe5846 | ||
| 
						 | 
					153351cc9f | ||
| 
						 | 
					1b1eec40a7 | ||
| 
						 | 
					763877541a | ||
| 
						 | 
					1fad7d72a2 | ||
| 
						 | 
					51ea2ea879 | ||
| 
						 | 
					d77a478bf0 | ||
| 
						 | 
					e413c0264a | ||
| 
						 | 
					f88e7f898c | ||
| 
						 | 
					d07bd4a6db | ||
| 
						 | 
					fb34c099d5 | ||
| 
						 | 
					1d2ee56a15 | ||
| 
						 | 
					86665f7f09 | 
							
								
								
									
										28
									
								
								.devcontainer/.env.example
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								.devcontainer/.env.example
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
				
			|||||||
 | 
					COMPOSE_PROJECT_NAME=trmm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					IMAGE_REPO=tacticalrmm/
 | 
				
			||||||
 | 
					VERSION=latest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# tactical credentials (Used to login to dashboard)
 | 
				
			||||||
 | 
					TRMM_USER=tactical
 | 
				
			||||||
 | 
					TRMM_PASS=tactical
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# dns settings
 | 
				
			||||||
 | 
					APP_HOST=rmm.example.com
 | 
				
			||||||
 | 
					API_HOST=api.example.com
 | 
				
			||||||
 | 
					MESH_HOST=mesh.example.com
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# mesh settings
 | 
				
			||||||
 | 
					MESH_USER=tactical
 | 
				
			||||||
 | 
					MESH_PASS=tactical
 | 
				
			||||||
 | 
					MONGODB_USER=mongouser
 | 
				
			||||||
 | 
					MONGODB_PASSWORD=mongopass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# database settings
 | 
				
			||||||
 | 
					POSTGRES_USER=postgres
 | 
				
			||||||
 | 
					POSTGRES_PASS=postgrespass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# DEV SETTINGS
 | 
				
			||||||
 | 
					APP_PORT=80
 | 
				
			||||||
 | 
					API_PORT=80
 | 
				
			||||||
 | 
					HTTP_PROTOCOL=https
 | 
				
			||||||
							
								
								
									
										24
									
								
								.devcontainer/api.dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								.devcontainer/api.dockerfile
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					FROM python:3.9.2-slim
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ENV TACTICAL_DIR /opt/tactical
 | 
				
			||||||
 | 
					ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready
 | 
				
			||||||
 | 
					ENV WORKSPACE_DIR /workspace
 | 
				
			||||||
 | 
					ENV TACTICAL_USER tactical
 | 
				
			||||||
 | 
					ENV VIRTUAL_ENV ${WORKSPACE_DIR}/api/tacticalrmm/env
 | 
				
			||||||
 | 
					ENV PYTHONDONTWRITEBYTECODE=1
 | 
				
			||||||
 | 
					ENV PYTHONUNBUFFERED=1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					EXPOSE 8000 8383 8005
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					RUN groupadd -g 1000 tactical && \
 | 
				
			||||||
 | 
					    useradd -u 1000 -g 1000 tactical
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Copy Dev python reqs
 | 
				
			||||||
 | 
					COPY ./requirements.txt /
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Copy Docker Entrypoint
 | 
				
			||||||
 | 
					COPY ./entrypoint.sh /
 | 
				
			||||||
 | 
					RUN chmod +x /entrypoint.sh
 | 
				
			||||||
 | 
					ENTRYPOINT ["/entrypoint.sh"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					WORKDIR ${WORKSPACE_DIR}/api/tacticalrmm
 | 
				
			||||||
							
								
								
									
										19
									
								
								.devcontainer/docker-compose.debug.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								.devcontainer/docker-compose.debug.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					version: '3.4'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					services:
 | 
				
			||||||
 | 
					  api-dev:
 | 
				
			||||||
 | 
					    image: api-dev
 | 
				
			||||||
 | 
					    build:
 | 
				
			||||||
 | 
					      context: .
 | 
				
			||||||
 | 
					      dockerfile: ./api.dockerfile
 | 
				
			||||||
 | 
					    command: ["sh", "-c", "pip install debugpy -t /tmp && python /tmp/debugpy --wait-for-client --listen 0.0.0.0:5678 manage.py runserver 0.0.0.0:8000 --nothreading --noreload"]
 | 
				
			||||||
 | 
					    ports:
 | 
				
			||||||
 | 
					      - 8000:8000
 | 
				
			||||||
 | 
					      - 5678:5678
 | 
				
			||||||
 | 
					    volumes:
 | 
				
			||||||
 | 
					      - tactical-data-dev:/opt/tactical
 | 
				
			||||||
 | 
					      - ..:/workspace:cached
 | 
				
			||||||
 | 
					    networks:
 | 
				
			||||||
 | 
					      dev:
 | 
				
			||||||
 | 
					        aliases: 
 | 
				
			||||||
 | 
					          - tactical-backend
 | 
				
			||||||
							
								
								
									
										261
									
								
								.devcontainer/docker-compose.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										261
									
								
								.devcontainer/docker-compose.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,261 @@
 | 
				
			|||||||
 | 
					version: '3.4'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					services:
 | 
				
			||||||
 | 
					  api-dev:
 | 
				
			||||||
 | 
					    container_name: trmm-api-dev
 | 
				
			||||||
 | 
					    image: api-dev
 | 
				
			||||||
 | 
					    restart: always
 | 
				
			||||||
 | 
					    build:
 | 
				
			||||||
 | 
					      context: .
 | 
				
			||||||
 | 
					      dockerfile: ./api.dockerfile
 | 
				
			||||||
 | 
					    command: ["tactical-api"]
 | 
				
			||||||
 | 
					    environment:
 | 
				
			||||||
 | 
					      API_PORT: ${API_PORT}
 | 
				
			||||||
 | 
					    ports:
 | 
				
			||||||
 | 
					      - "8000:${API_PORT}"
 | 
				
			||||||
 | 
					    volumes:
 | 
				
			||||||
 | 
					      - tactical-data-dev:/opt/tactical
 | 
				
			||||||
 | 
					      - ..:/workspace:cached
 | 
				
			||||||
 | 
					    networks:
 | 
				
			||||||
 | 
					      dev:
 | 
				
			||||||
 | 
					        aliases: 
 | 
				
			||||||
 | 
					          - tactical-backend
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  app-dev:
 | 
				
			||||||
 | 
					    container_name: trmm-app-dev
 | 
				
			||||||
 | 
					    image: node:14-alpine
 | 
				
			||||||
 | 
					    restart: always
 | 
				
			||||||
 | 
					    command: /bin/sh -c "npm install npm@latest -g && npm install && npm run serve -- --host 0.0.0.0 --port ${APP_PORT}"
 | 
				
			||||||
 | 
					    working_dir: /workspace/web
 | 
				
			||||||
 | 
					    volumes:
 | 
				
			||||||
 | 
					      - ..:/workspace:cached
 | 
				
			||||||
 | 
					    ports:
 | 
				
			||||||
 | 
					      - "8080:${APP_PORT}"
 | 
				
			||||||
 | 
					    networks:
 | 
				
			||||||
 | 
					      dev:
 | 
				
			||||||
 | 
					        aliases: 
 | 
				
			||||||
 | 
					          - tactical-frontend
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # nats
 | 
				
			||||||
 | 
					  nats-dev:
 | 
				
			||||||
 | 
					    container_name: trmm-nats-dev
 | 
				
			||||||
 | 
					    image: ${IMAGE_REPO}tactical-nats:${VERSION}
 | 
				
			||||||
 | 
					    restart: always
 | 
				
			||||||
 | 
					    environment:
 | 
				
			||||||
 | 
					      API_HOST: ${API_HOST}
 | 
				
			||||||
 | 
					      API_PORT: ${API_PORT}
 | 
				
			||||||
 | 
					      DEV: 1
 | 
				
			||||||
 | 
					    ports:
 | 
				
			||||||
 | 
					      - "4222:4222"
 | 
				
			||||||
 | 
					    volumes:
 | 
				
			||||||
 | 
					      - tactical-data-dev:/opt/tactical
 | 
				
			||||||
 | 
					      - ..:/workspace:cached
 | 
				
			||||||
 | 
					    networks:
 | 
				
			||||||
 | 
					      dev:
 | 
				
			||||||
 | 
					        aliases:
 | 
				
			||||||
 | 
					          - ${API_HOST}
 | 
				
			||||||
 | 
					          - tactical-nats
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # meshcentral container
 | 
				
			||||||
 | 
					  meshcentral-dev:
 | 
				
			||||||
 | 
					    container_name: trmm-meshcentral-dev
 | 
				
			||||||
 | 
					    image: ${IMAGE_REPO}tactical-meshcentral:${VERSION}
 | 
				
			||||||
 | 
					    restart: always
 | 
				
			||||||
 | 
					    environment: 
 | 
				
			||||||
 | 
					      MESH_HOST: ${MESH_HOST}
 | 
				
			||||||
 | 
					      MESH_USER: ${MESH_USER}
 | 
				
			||||||
 | 
					      MESH_PASS: ${MESH_PASS}
 | 
				
			||||||
 | 
					      MONGODB_USER: ${MONGODB_USER}
 | 
				
			||||||
 | 
					      MONGODB_PASSWORD: ${MONGODB_PASSWORD}
 | 
				
			||||||
 | 
					      NGINX_HOST_IP: 172.21.0.20
 | 
				
			||||||
 | 
					    networks:
 | 
				
			||||||
 | 
					      dev:
 | 
				
			||||||
 | 
					        aliases:
 | 
				
			||||||
 | 
					          - tactical-meshcentral
 | 
				
			||||||
 | 
					          - ${MESH_HOST}
 | 
				
			||||||
 | 
					    volumes:
 | 
				
			||||||
 | 
					      - tactical-data-dev:/opt/tactical
 | 
				
			||||||
 | 
					      - mesh-data-dev:/home/node/app/meshcentral-data
 | 
				
			||||||
 | 
					    depends_on:
 | 
				
			||||||
 | 
					      - mongodb-dev
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # mongodb container for meshcentral
 | 
				
			||||||
 | 
					  mongodb-dev:
 | 
				
			||||||
 | 
					    container_name: trmm-mongodb-dev
 | 
				
			||||||
 | 
					    image: mongo:4.4
 | 
				
			||||||
 | 
					    restart: always
 | 
				
			||||||
 | 
					    environment:
 | 
				
			||||||
 | 
					      MONGO_INITDB_ROOT_USERNAME: ${MONGODB_USER}
 | 
				
			||||||
 | 
					      MONGO_INITDB_ROOT_PASSWORD: ${MONGODB_PASSWORD}
 | 
				
			||||||
 | 
					      MONGO_INITDB_DATABASE: meshcentral
 | 
				
			||||||
 | 
					    networks:
 | 
				
			||||||
 | 
					      dev:
 | 
				
			||||||
 | 
					        aliases:
 | 
				
			||||||
 | 
					          - tactical-mongodb
 | 
				
			||||||
 | 
					    volumes:
 | 
				
			||||||
 | 
					      - mongo-dev-data:/data/db
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # postgres database for api service
 | 
				
			||||||
 | 
					  postgres-dev:
 | 
				
			||||||
 | 
					    container_name: trmm-postgres-dev
 | 
				
			||||||
 | 
					    image: postgres:13-alpine
 | 
				
			||||||
 | 
					    restart: always
 | 
				
			||||||
 | 
					    environment:
 | 
				
			||||||
 | 
					      POSTGRES_DB: tacticalrmm
 | 
				
			||||||
 | 
					      POSTGRES_USER: ${POSTGRES_USER}
 | 
				
			||||||
 | 
					      POSTGRES_PASSWORD: ${POSTGRES_PASS}
 | 
				
			||||||
 | 
					    volumes:
 | 
				
			||||||
 | 
					      - postgres-data-dev:/var/lib/postgresql/data
 | 
				
			||||||
 | 
					    networks:
 | 
				
			||||||
 | 
					      dev:
 | 
				
			||||||
 | 
					        aliases:
 | 
				
			||||||
 | 
					          - tactical-postgres
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # redis container for celery tasks
 | 
				
			||||||
 | 
					  redis-dev:
 | 
				
			||||||
 | 
					    container_name: trmm-redis-dev
 | 
				
			||||||
 | 
					    restart: always
 | 
				
			||||||
 | 
					    command: redis-server --appendonly yes
 | 
				
			||||||
 | 
					    image: redis:6.0-alpine
 | 
				
			||||||
 | 
					    volumes: 
 | 
				
			||||||
 | 
					      - redis-data-dev:/data
 | 
				
			||||||
 | 
					    networks:
 | 
				
			||||||
 | 
					      dev:
 | 
				
			||||||
 | 
					        aliases:
 | 
				
			||||||
 | 
					          - tactical-redis
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  init-dev:
 | 
				
			||||||
 | 
					    container_name: trmm-init-dev
 | 
				
			||||||
 | 
					    image: api-dev
 | 
				
			||||||
 | 
					    build:
 | 
				
			||||||
 | 
					      context: .
 | 
				
			||||||
 | 
					      dockerfile: ./api.dockerfile
 | 
				
			||||||
 | 
					    restart: on-failure
 | 
				
			||||||
 | 
					    command: ["tactical-init-dev"]
 | 
				
			||||||
 | 
					    environment:
 | 
				
			||||||
 | 
					      POSTGRES_USER: ${POSTGRES_USER}
 | 
				
			||||||
 | 
					      POSTGRES_PASS: ${POSTGRES_PASS}
 | 
				
			||||||
 | 
					      APP_HOST: ${APP_HOST}
 | 
				
			||||||
 | 
					      API_HOST: ${API_HOST}
 | 
				
			||||||
 | 
					      MESH_HOST: ${MESH_HOST}
 | 
				
			||||||
 | 
					      MESH_USER: ${MESH_USER}
 | 
				
			||||||
 | 
					      TRMM_USER: ${TRMM_USER}
 | 
				
			||||||
 | 
					      TRMM_PASS: ${TRMM_PASS}
 | 
				
			||||||
 | 
					      HTTP_PROTOCOL: ${HTTP_PROTOCOL}
 | 
				
			||||||
 | 
					      APP_PORT: ${APP_PORT}
 | 
				
			||||||
 | 
					    depends_on:
 | 
				
			||||||
 | 
					      - postgres-dev
 | 
				
			||||||
 | 
					      - meshcentral-dev
 | 
				
			||||||
 | 
					    networks:
 | 
				
			||||||
 | 
					      - dev
 | 
				
			||||||
 | 
					    volumes:
 | 
				
			||||||
 | 
					      - tactical-data-dev:/opt/tactical
 | 
				
			||||||
 | 
					      - ..:/workspace:cached
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # container for celery worker service
 | 
				
			||||||
 | 
					  celery-dev:
 | 
				
			||||||
 | 
					    container_name: trmm-celery-dev
 | 
				
			||||||
 | 
					    image: api-dev
 | 
				
			||||||
 | 
					    build:
 | 
				
			||||||
 | 
					      context: .
 | 
				
			||||||
 | 
					      dockerfile: ./api.dockerfile
 | 
				
			||||||
 | 
					    command: ["tactical-celery-dev"]
 | 
				
			||||||
 | 
					    restart: always
 | 
				
			||||||
 | 
					    networks:
 | 
				
			||||||
 | 
					      - dev
 | 
				
			||||||
 | 
					    volumes:
 | 
				
			||||||
 | 
					      - tactical-data-dev:/opt/tactical
 | 
				
			||||||
 | 
					      - ..:/workspace:cached
 | 
				
			||||||
 | 
					    depends_on:
 | 
				
			||||||
 | 
					      - postgres-dev
 | 
				
			||||||
 | 
					      - redis-dev
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # container for celery beat service
 | 
				
			||||||
 | 
					  celerybeat-dev:
 | 
				
			||||||
 | 
					    container_name: trmm-celerybeat-dev
 | 
				
			||||||
 | 
					    image: api-dev
 | 
				
			||||||
 | 
					    build:
 | 
				
			||||||
 | 
					      context: .
 | 
				
			||||||
 | 
					      dockerfile: ./api.dockerfile
 | 
				
			||||||
 | 
					    command: ["tactical-celerybeat-dev"]
 | 
				
			||||||
 | 
					    restart: always
 | 
				
			||||||
 | 
					    networks:
 | 
				
			||||||
 | 
					      - dev
 | 
				
			||||||
 | 
					    volumes:
 | 
				
			||||||
 | 
					      - tactical-data-dev:/opt/tactical
 | 
				
			||||||
 | 
					      - ..:/workspace:cached
 | 
				
			||||||
 | 
					    depends_on:
 | 
				
			||||||
 | 
					      - postgres-dev
 | 
				
			||||||
 | 
					      - redis-dev
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # container for websockets communication
 | 
				
			||||||
 | 
					  websockets-dev:
 | 
				
			||||||
 | 
					    container_name: trmm-websockets-dev
 | 
				
			||||||
 | 
					    image: api-dev
 | 
				
			||||||
 | 
					    build:
 | 
				
			||||||
 | 
					      context: .
 | 
				
			||||||
 | 
					      dockerfile: ./api.dockerfile
 | 
				
			||||||
 | 
					    command: ["tactical-websockets-dev"]
 | 
				
			||||||
 | 
					    restart: always
 | 
				
			||||||
 | 
					    networks:
 | 
				
			||||||
 | 
					      dev:
 | 
				
			||||||
 | 
					        aliases:
 | 
				
			||||||
 | 
					          - tactical-websockets
 | 
				
			||||||
 | 
					    volumes:
 | 
				
			||||||
 | 
					      - tactical-data-dev:/opt/tactical
 | 
				
			||||||
 | 
					      - ..:/workspace:cached
 | 
				
			||||||
 | 
					    depends_on:
 | 
				
			||||||
 | 
					      - postgres-dev
 | 
				
			||||||
 | 
					      - redis-dev
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # container for tactical reverse proxy
 | 
				
			||||||
 | 
					  nginx-dev:
 | 
				
			||||||
 | 
					    container_name: trmm-nginx-dev
 | 
				
			||||||
 | 
					    image: ${IMAGE_REPO}tactical-nginx:${VERSION}
 | 
				
			||||||
 | 
					    restart: always
 | 
				
			||||||
 | 
					    environment:
 | 
				
			||||||
 | 
					      APP_HOST: ${APP_HOST}
 | 
				
			||||||
 | 
					      API_HOST: ${API_HOST}
 | 
				
			||||||
 | 
					      MESH_HOST: ${MESH_HOST}
 | 
				
			||||||
 | 
					      CERT_PUB_KEY: ${CERT_PUB_KEY}
 | 
				
			||||||
 | 
					      CERT_PRIV_KEY: ${CERT_PRIV_KEY}
 | 
				
			||||||
 | 
					      APP_PORT: ${APP_PORT}
 | 
				
			||||||
 | 
					      API_PORT: ${API_PORT}
 | 
				
			||||||
 | 
					    networks:
 | 
				
			||||||
 | 
					      dev:
 | 
				
			||||||
 | 
					        ipv4_address: 172.21.0.20
 | 
				
			||||||
 | 
					    ports:
 | 
				
			||||||
 | 
					      - "80:80"
 | 
				
			||||||
 | 
					      - "443:443"
 | 
				
			||||||
 | 
					    volumes:
 | 
				
			||||||
 | 
					      - tactical-data-dev:/opt/tactical
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  mkdocs-dev:
 | 
				
			||||||
 | 
					    container_name: trmm-mkdocs-dev
 | 
				
			||||||
 | 
					    image: api-dev
 | 
				
			||||||
 | 
					    restart: always
 | 
				
			||||||
 | 
					    build:
 | 
				
			||||||
 | 
					      context: .
 | 
				
			||||||
 | 
					      dockerfile: ./api.dockerfile
 | 
				
			||||||
 | 
					    command: ["tactical-mkdocs-dev"]
 | 
				
			||||||
 | 
					    ports:
 | 
				
			||||||
 | 
					      - "8005:8005"
 | 
				
			||||||
 | 
					    volumes:
 | 
				
			||||||
 | 
					      - ..:/workspace:cached
 | 
				
			||||||
 | 
					    networks:
 | 
				
			||||||
 | 
					      - dev
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					volumes:
 | 
				
			||||||
 | 
					  tactical-data-dev:
 | 
				
			||||||
 | 
					  postgres-data-dev:
 | 
				
			||||||
 | 
					  mongo-dev-data:
 | 
				
			||||||
 | 
					  mesh-data-dev:
 | 
				
			||||||
 | 
					  redis-data-dev:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					networks:
 | 
				
			||||||
 | 
					  dev:
 | 
				
			||||||
 | 
					    driver: bridge
 | 
				
			||||||
 | 
					    ipam:
 | 
				
			||||||
 | 
					      driver: default
 | 
				
			||||||
 | 
					      config:
 | 
				
			||||||
 | 
					        - subnet: 172.21.0.0/24  
 | 
				
			||||||
							
								
								
									
										177
									
								
								.devcontainer/entrypoint.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										177
									
								
								.devcontainer/entrypoint.sh
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,177 @@
 | 
				
			|||||||
 | 
					#!/usr/bin/env bash
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					set -e
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					: "${TRMM_USER:=tactical}"
 | 
				
			||||||
 | 
					: "${TRMM_PASS:=tactical}"
 | 
				
			||||||
 | 
					: "${POSTGRES_HOST:=tactical-postgres}"
 | 
				
			||||||
 | 
					: "${POSTGRES_PORT:=5432}"
 | 
				
			||||||
 | 
					: "${POSTGRES_USER:=tactical}"
 | 
				
			||||||
 | 
					: "${POSTGRES_PASS:=tactical}"
 | 
				
			||||||
 | 
					: "${POSTGRES_DB:=tacticalrmm}"
 | 
				
			||||||
 | 
					: "${MESH_CONTAINER:=tactical-meshcentral}"
 | 
				
			||||||
 | 
					: "${MESH_USER:=meshcentral}"
 | 
				
			||||||
 | 
					: "${MESH_PASS:=meshcentralpass}"
 | 
				
			||||||
 | 
					: "${MESH_HOST:=tactical-meshcentral}"
 | 
				
			||||||
 | 
					: "${API_HOST:=tactical-backend}"
 | 
				
			||||||
 | 
					: "${APP_HOST:=tactical-frontend}"
 | 
				
			||||||
 | 
					: "${REDIS_HOST:=tactical-redis}"
 | 
				
			||||||
 | 
					: "${HTTP_PROTOCOL:=http}"
 | 
				
			||||||
 | 
					: "${APP_PORT:=8080}"
 | 
				
			||||||
 | 
					: "${API_PORT:=8000}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Add python venv to path
 | 
				
			||||||
 | 
					export PATH="${VIRTUAL_ENV}/bin:$PATH"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function check_tactical_ready {
 | 
				
			||||||
 | 
					  sleep 15
 | 
				
			||||||
 | 
					  until [ -f "${TACTICAL_READY_FILE}" ]; do
 | 
				
			||||||
 | 
					    echo "waiting for init container to finish install or update..."
 | 
				
			||||||
 | 
					    sleep 10
 | 
				
			||||||
 | 
					  done
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function django_setup {
 | 
				
			||||||
 | 
					  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_CONTAINER}"/443) &> /dev/null; do
 | 
				
			||||||
 | 
					    echo "waiting for meshcentral container to be ready..."
 | 
				
			||||||
 | 
					    sleep 5
 | 
				
			||||||
 | 
					  done
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  echo "setting up django environment"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # configure django settings
 | 
				
			||||||
 | 
					  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
 | 
				
			||||||
 | 
					SECRET_KEY = '${DJANGO_SEKRET}'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					DEBUG = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					DOCKER_BUILD = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CERT_FILE = '/opt/tactical/certs/fullchain.pem'
 | 
				
			||||||
 | 
					KEY_FILE = '/opt/tactical/certs/privkey.pem'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SCRIPTS_DIR = '${WORKSPACE_DIR}/scripts'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ALLOWED_HOSTS = ['${API_HOST}', '*']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ADMIN_URL = 'admin/'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CORS_ORIGIN_ALLOW_ALL = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					DATABASES = {
 | 
				
			||||||
 | 
					    'default': {
 | 
				
			||||||
 | 
					        'ENGINE': 'django.db.backends.postgresql',
 | 
				
			||||||
 | 
					        'NAME': '${POSTGRES_DB}',
 | 
				
			||||||
 | 
					        'USER': '${POSTGRES_USER}',
 | 
				
			||||||
 | 
					        'PASSWORD': '${POSTGRES_PASS}',
 | 
				
			||||||
 | 
					        'HOST': '${POSTGRES_HOST}',
 | 
				
			||||||
 | 
					        'PORT': '${POSTGRES_PORT}',
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					REST_FRAMEWORK = {
 | 
				
			||||||
 | 
					    'DATETIME_FORMAT': '%b-%d-%Y - %H:%M',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    'DEFAULT_PERMISSION_CLASSES': (
 | 
				
			||||||
 | 
					        'rest_framework.permissions.IsAuthenticated',
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					    'DEFAULT_AUTHENTICATION_CLASSES': (
 | 
				
			||||||
 | 
					        'knox.auth.TokenAuthentication',
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if not DEBUG:
 | 
				
			||||||
 | 
					    REST_FRAMEWORK.update({
 | 
				
			||||||
 | 
					        'DEFAULT_RENDERER_CLASSES': (
 | 
				
			||||||
 | 
					            'rest_framework.renderers.JSONRenderer',
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					MESH_USERNAME = '${MESH_USER}'
 | 
				
			||||||
 | 
					MESH_SITE = 'https://${MESH_HOST}'
 | 
				
			||||||
 | 
					MESH_TOKEN_KEY = '${MESH_TOKEN}'
 | 
				
			||||||
 | 
					REDIS_HOST    = '${REDIS_HOST}'
 | 
				
			||||||
 | 
					ADMIN_ENABLED = True
 | 
				
			||||||
 | 
					EOF
 | 
				
			||||||
 | 
					)"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  echo "${localvars}" > ${WORKSPACE_DIR}/api/tacticalrmm/tacticalrmm/local_settings.py
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # run migrations and init scripts
 | 
				
			||||||
 | 
					  "${VIRTUAL_ENV}"/bin/python manage.py migrate --no-input
 | 
				
			||||||
 | 
					  "${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
 | 
				
			||||||
 | 
					  "${VIRTUAL_ENV}"/bin/python manage.py load_chocos
 | 
				
			||||||
 | 
					  "${VIRTUAL_ENV}"/bin/python manage.py load_community_scripts
 | 
				
			||||||
 | 
					  "${VIRTUAL_ENV}"/bin/python manage.py reload_nats
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # 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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if [ "$1" = 'tactical-init-dev' ]; then
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # make directories if they don't exist
 | 
				
			||||||
 | 
					  mkdir -p "${TACTICAL_DIR}/tmp"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  test -f "${TACTICAL_READY_FILE}" && rm "${TACTICAL_READY_FILE}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # setup Python virtual env and install dependencies
 | 
				
			||||||
 | 
					  ! test -e "${VIRTUAL_ENV}" && python -m venv ${VIRTUAL_ENV}
 | 
				
			||||||
 | 
					  "${VIRTUAL_ENV}"/bin/pip install --no-cache-dir -r /requirements.txt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  django_setup
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # create .env file for frontend
 | 
				
			||||||
 | 
					  webenv="$(cat << EOF
 | 
				
			||||||
 | 
					PROD_URL = "${HTTP_PROTOCOL}://${API_HOST}"
 | 
				
			||||||
 | 
					DEV_URL = "${HTTP_PROTOCOL}://${API_HOST}"
 | 
				
			||||||
 | 
					APP_URL = "https://${APP_HOST}"
 | 
				
			||||||
 | 
					DOCKER_BUILD = 1
 | 
				
			||||||
 | 
					EOF
 | 
				
			||||||
 | 
					)"
 | 
				
			||||||
 | 
					  echo "${webenv}" | tee "${WORKSPACE_DIR}"/web/.env > /dev/null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # chown everything to tactical user
 | 
				
			||||||
 | 
					  chown -R "${TACTICAL_USER}":"${TACTICAL_USER}" "${WORKSPACE_DIR}"
 | 
				
			||||||
 | 
					  chown -R "${TACTICAL_USER}":"${TACTICAL_USER}" "${TACTICAL_DIR}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # create install ready file
 | 
				
			||||||
 | 
					  su -c "echo 'tactical-init' > ${TACTICAL_READY_FILE}" "${TACTICAL_USER}"
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if [ "$1" = 'tactical-api' ]; then
 | 
				
			||||||
 | 
					  check_tactical_ready
 | 
				
			||||||
 | 
					  "${VIRTUAL_ENV}"/bin/python manage.py runserver 0.0.0.0:"${API_PORT}"
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if [ "$1" = 'tactical-celery-dev' ]; then
 | 
				
			||||||
 | 
					  check_tactical_ready
 | 
				
			||||||
 | 
					  "${VIRTUAL_ENV}"/bin/celery -A tacticalrmm worker -l debug
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if [ "$1" = 'tactical-celerybeat-dev' ]; then
 | 
				
			||||||
 | 
					  check_tactical_ready
 | 
				
			||||||
 | 
					  test -f "${WORKSPACE_DIR}/api/tacticalrmm/celerybeat.pid" && rm "${WORKSPACE_DIR}/api/tacticalrmm/celerybeat.pid"
 | 
				
			||||||
 | 
					  "${VIRTUAL_ENV}"/bin/celery -A tacticalrmm beat -l debug
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if [ "$1" = 'tactical-websockets-dev' ]; then
 | 
				
			||||||
 | 
					  check_tactical_ready
 | 
				
			||||||
 | 
					  "${VIRTUAL_ENV}"/bin/daphne tacticalrmm.asgi:application --port 8383 -b 0.0.0.0
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if [ "$1" = 'tactical-mkdocs-dev' ]; then
 | 
				
			||||||
 | 
					  cd "${WORKSPACE_DIR}/docs"
 | 
				
			||||||
 | 
					  "${VIRTUAL_ENV}"/bin/mkdocs serve
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
							
								
								
									
										36
									
								
								.devcontainer/requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								.devcontainer/requirements.txt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
				
			|||||||
 | 
					# To ensure app dependencies are ported from your virtual environment/host machine into your container, run 'pip freeze > requirements.txt' in the terminal to overwrite this file
 | 
				
			||||||
 | 
					asyncio-nats-client
 | 
				
			||||||
 | 
					celery
 | 
				
			||||||
 | 
					channels
 | 
				
			||||||
 | 
					channels_redis
 | 
				
			||||||
 | 
					Django
 | 
				
			||||||
 | 
					django-cors-headers
 | 
				
			||||||
 | 
					django-rest-knox
 | 
				
			||||||
 | 
					djangorestframework
 | 
				
			||||||
 | 
					loguru
 | 
				
			||||||
 | 
					msgpack
 | 
				
			||||||
 | 
					psycopg2-binary
 | 
				
			||||||
 | 
					pycparser
 | 
				
			||||||
 | 
					pycryptodome
 | 
				
			||||||
 | 
					pyotp
 | 
				
			||||||
 | 
					pyparsing
 | 
				
			||||||
 | 
					pytz
 | 
				
			||||||
 | 
					qrcode
 | 
				
			||||||
 | 
					redis
 | 
				
			||||||
 | 
					twilio
 | 
				
			||||||
 | 
					packaging
 | 
				
			||||||
 | 
					validators
 | 
				
			||||||
 | 
					websockets
 | 
				
			||||||
 | 
					black
 | 
				
			||||||
 | 
					Werkzeug
 | 
				
			||||||
 | 
					django-extensions
 | 
				
			||||||
 | 
					coverage
 | 
				
			||||||
 | 
					coveralls
 | 
				
			||||||
 | 
					model_bakery
 | 
				
			||||||
 | 
					mkdocs
 | 
				
			||||||
 | 
					mkdocs-material
 | 
				
			||||||
 | 
					pymdown-extensions
 | 
				
			||||||
 | 
					Pygments
 | 
				
			||||||
 | 
					mypy
 | 
				
			||||||
 | 
					pysnooper
 | 
				
			||||||
 | 
					isort
 | 
				
			||||||
@@ -1,5 +1,25 @@
 | 
				
			|||||||
.git
 | 
					**/__pycache__
 | 
				
			||||||
.cache
 | 
					**/.classpath
 | 
				
			||||||
**/*.env
 | 
					**/.dockerignore
 | 
				
			||||||
**/env
 | 
					**/.env
 | 
				
			||||||
 | 
					**/.git
 | 
				
			||||||
 | 
					**/.gitignore
 | 
				
			||||||
 | 
					**/.project
 | 
				
			||||||
 | 
					**/.settings
 | 
				
			||||||
 | 
					**/.toolstarget
 | 
				
			||||||
 | 
					**/.vs
 | 
				
			||||||
 | 
					**/.vscode
 | 
				
			||||||
 | 
					**/*.*proj.user
 | 
				
			||||||
 | 
					**/*.dbmdl
 | 
				
			||||||
 | 
					**/*.jfm
 | 
				
			||||||
 | 
					**/azds.yaml
 | 
				
			||||||
 | 
					**/charts
 | 
				
			||||||
 | 
					**/docker-compose*
 | 
				
			||||||
 | 
					**/Dockerfile*
 | 
				
			||||||
**/node_modules
 | 
					**/node_modules
 | 
				
			||||||
 | 
					**/npm-debug.log
 | 
				
			||||||
 | 
					**/obj
 | 
				
			||||||
 | 
					**/secrets.dev.yaml
 | 
				
			||||||
 | 
					**/values.dev.yaml
 | 
				
			||||||
 | 
					**/env
 | 
				
			||||||
 | 
					README.md
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										12
									
								
								.github/FUNDING.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								.github/FUNDING.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					# These are supported funding model platforms
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					github: wh1te909
 | 
				
			||||||
 | 
					patreon: # Replace with a single Patreon username
 | 
				
			||||||
 | 
					open_collective: # Replace with a single Open Collective username
 | 
				
			||||||
 | 
					ko_fi: tacticalrmm
 | 
				
			||||||
 | 
					tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
 | 
				
			||||||
 | 
					community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
 | 
				
			||||||
 | 
					liberapay: # Replace with a single Liberapay username
 | 
				
			||||||
 | 
					issuehunt: # Replace with a single IssueHunt username
 | 
				
			||||||
 | 
					otechie: # Replace with a single Otechie username
 | 
				
			||||||
 | 
					custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
 | 
				
			||||||
							
								
								
									
										40
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
				
			|||||||
 | 
					---
 | 
				
			||||||
 | 
					name: Bug report
 | 
				
			||||||
 | 
					about: Create a bug report
 | 
				
			||||||
 | 
					title: ''
 | 
				
			||||||
 | 
					labels: ''
 | 
				
			||||||
 | 
					assignees: ''
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Server Info (please complete the following information):**
 | 
				
			||||||
 | 
					 - OS: [e.g. Ubuntu 20.04, Debian 10]
 | 
				
			||||||
 | 
					 - Browser: [e.g. chrome, safari]
 | 
				
			||||||
 | 
					 - RMM Version (as shown in top left of web UI):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Installation Method:**
 | 
				
			||||||
 | 
					  - [ ] Standard
 | 
				
			||||||
 | 
					  - [ ] 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]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Describe the bug**
 | 
				
			||||||
 | 
					A clear and concise description of what the bug is.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**To Reproduce**
 | 
				
			||||||
 | 
					Steps to reproduce the behavior:
 | 
				
			||||||
 | 
					1. Go to '...'
 | 
				
			||||||
 | 
					2. Click on '....'
 | 
				
			||||||
 | 
					3. Scroll down to '....'
 | 
				
			||||||
 | 
					4. See error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Expected behavior**
 | 
				
			||||||
 | 
					A clear and concise description of what you expected to happen.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Screenshots**
 | 
				
			||||||
 | 
					If applicable, add screenshots to help explain your problem.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Additional context**
 | 
				
			||||||
 | 
					Add any other context about the problem here.
 | 
				
			||||||
							
								
								
									
										20
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					---
 | 
				
			||||||
 | 
					name: Feature request
 | 
				
			||||||
 | 
					about: Suggest an idea for this project
 | 
				
			||||||
 | 
					title: ''
 | 
				
			||||||
 | 
					labels: ''
 | 
				
			||||||
 | 
					assignees: ''
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Is your feature request related to a problem? Please describe.**
 | 
				
			||||||
 | 
					A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Describe the solution you'd like**
 | 
				
			||||||
 | 
					A clear and concise description of what you want to happen.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Describe alternatives you've considered**
 | 
				
			||||||
 | 
					A clear and concise description of any alternative solutions or features you've considered.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**Additional context**
 | 
				
			||||||
 | 
					Add any other context or screenshots about the feature request here.
 | 
				
			||||||
							
								
								
									
										22
									
								
								.github/workflows/deploy-docs.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								.github/workflows/deploy-docs.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
				
			|||||||
 | 
					name: Deploy Docs
 | 
				
			||||||
 | 
					on:
 | 
				
			||||||
 | 
					  push:
 | 
				
			||||||
 | 
					    branches:
 | 
				
			||||||
 | 
					      - master
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					defaults:
 | 
				
			||||||
 | 
					  run:
 | 
				
			||||||
 | 
					    working-directory: docs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					jobs:
 | 
				
			||||||
 | 
					  deploy:
 | 
				
			||||||
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					      - uses: actions/checkout@v2
 | 
				
			||||||
 | 
					      - uses: actions/setup-python@v2
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          python-version: 3.x
 | 
				
			||||||
 | 
					      - run: pip install --upgrade pip
 | 
				
			||||||
 | 
					      - run: pip install --upgrade setuptools wheel
 | 
				
			||||||
 | 
					      - run: pip install mkdocs mkdocs-material pymdown-extensions
 | 
				
			||||||
 | 
					      - run: mkdocs gh-deploy --force
 | 
				
			||||||
							
								
								
									
										10
									
								
								.github/workflows/docker-build-push.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								.github/workflows/docker-build-push.yml
									
									
									
									
										vendored
									
									
								
							@@ -57,16 +57,6 @@ jobs:
 | 
				
			|||||||
          platforms: linux/amd64
 | 
					          platforms: linux/amd64
 | 
				
			||||||
          tags: tacticalrmm/tactical-nats:${{ steps.prep.outputs.version }},tacticalrmm/tactical-nats:latest
 | 
					          tags: tacticalrmm/tactical-nats:${{ steps.prep.outputs.version }},tacticalrmm/tactical-nats:latest
 | 
				
			||||||
          
 | 
					          
 | 
				
			||||||
      - name: Build and Push Tactical Salt Image
 | 
					 | 
				
			||||||
        uses: docker/build-push-action@v2
 | 
					 | 
				
			||||||
        with:
 | 
					 | 
				
			||||||
          context: .
 | 
					 | 
				
			||||||
          push: true
 | 
					 | 
				
			||||||
          pull: true
 | 
					 | 
				
			||||||
          file: ./docker/containers/tactical-salt/dockerfile
 | 
					 | 
				
			||||||
          platforms: linux/amd64
 | 
					 | 
				
			||||||
          tags: tacticalrmm/tactical-salt:${{ steps.prep.outputs.version }},tacticalrmm/tactical-salt:latest
 | 
					 | 
				
			||||||
          
 | 
					 | 
				
			||||||
      - name: Build and Push Tactical Frontend Image
 | 
					      - name: Build and Push Tactical Frontend Image
 | 
				
			||||||
        uses: docker/build-push-action@v2
 | 
					        uses: docker/build-push-action@v2
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -45,3 +45,6 @@ htmlcov/
 | 
				
			|||||||
docker-compose.dev.yml
 | 
					docker-compose.dev.yml
 | 
				
			||||||
docs/.vuepress/dist
 | 
					docs/.vuepress/dist
 | 
				
			||||||
nats-rmm.conf
 | 
					nats-rmm.conf
 | 
				
			||||||
 | 
					.mypy_cache
 | 
				
			||||||
 | 
					docs/site/
 | 
				
			||||||
 | 
					reset_db.sh
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										14
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
								
							@@ -14,6 +14,20 @@
 | 
				
			|||||||
                "0.0.0.0:8000"
 | 
					                "0.0.0.0:8000"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "django": true
 | 
					            "django": true
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            "name": "Django: Docker Remote Attach",
 | 
				
			||||||
 | 
					            "type": "python",
 | 
				
			||||||
 | 
					            "request": "attach",
 | 
				
			||||||
 | 
					            "port": 5678,
 | 
				
			||||||
 | 
					            "host": "localhost",
 | 
				
			||||||
 | 
					            "preLaunchTask": "docker debug",
 | 
				
			||||||
 | 
					            "pathMappings": [
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    "localRoot": "${workspaceFolder}/api/tacticalrmm",
 | 
				
			||||||
 | 
					                    "remoteRoot": "/workspace/api/tacticalrmm"
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
							
								
								
									
										26
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										26
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							@@ -3,7 +3,14 @@
 | 
				
			|||||||
    "python.languageServer": "Pylance",
 | 
					    "python.languageServer": "Pylance",
 | 
				
			||||||
    "python.analysis.extraPaths": [
 | 
					    "python.analysis.extraPaths": [
 | 
				
			||||||
        "api/tacticalrmm",
 | 
					        "api/tacticalrmm",
 | 
				
			||||||
 | 
					        "api/env",
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
 | 
					    "python.analysis.diagnosticSeverityOverrides": {
 | 
				
			||||||
 | 
					        "reportUnusedImport": "error",
 | 
				
			||||||
 | 
					        "reportDuplicateImport": "error",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "python.analysis.memory.keepLibraryAst": true,
 | 
				
			||||||
 | 
					    "python.linting.mypyEnabled": true,
 | 
				
			||||||
    "python.analysis.typeCheckingMode": "basic",
 | 
					    "python.analysis.typeCheckingMode": "basic",
 | 
				
			||||||
    "python.formatting.provider": "black",
 | 
					    "python.formatting.provider": "black",
 | 
				
			||||||
    "editor.formatOnSave": true,
 | 
					    "editor.formatOnSave": true,
 | 
				
			||||||
@@ -41,4 +48,23 @@
 | 
				
			|||||||
            "**/*.zip": true
 | 
					            "**/*.zip": true
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "go.useLanguageServer": true,
 | 
				
			||||||
 | 
					    "[go]": {
 | 
				
			||||||
 | 
					        "editor.formatOnSave": true,
 | 
				
			||||||
 | 
					        "editor.codeActionsOnSave": {
 | 
				
			||||||
 | 
					            "source.organizeImports": false,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "editor.snippetSuggestions": "none",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "[go.mod]": {
 | 
				
			||||||
 | 
					        "editor.formatOnSave": true,
 | 
				
			||||||
 | 
					        "editor.codeActionsOnSave": {
 | 
				
			||||||
 | 
					            "source.organizeImports": true,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "gopls": {
 | 
				
			||||||
 | 
					        "usePlaceholders": true,
 | 
				
			||||||
 | 
					        "completeUnimported": true,
 | 
				
			||||||
 | 
					        "staticcheck": true,
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
							
								
								
									
										23
									
								
								.vscode/tasks.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								.vscode/tasks.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					    // See https://go.microsoft.com/fwlink/?LinkId=733558
 | 
				
			||||||
 | 
					    // for the documentation about the tasks.json format
 | 
				
			||||||
 | 
					    "version": "2.0.0",
 | 
				
			||||||
 | 
					    "tasks": [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            "label": "docker debug",
 | 
				
			||||||
 | 
					            "type": "shell",
 | 
				
			||||||
 | 
					            "command": "docker-compose",
 | 
				
			||||||
 | 
					            "args": [
 | 
				
			||||||
 | 
					                "-p",
 | 
				
			||||||
 | 
					                "trmm",
 | 
				
			||||||
 | 
					                "-f",
 | 
				
			||||||
 | 
					                ".devcontainer/docker-compose.yml",
 | 
				
			||||||
 | 
					                "-f",
 | 
				
			||||||
 | 
					                ".devcontainer/docker-compose.debug.yml",
 | 
				
			||||||
 | 
					                "up",
 | 
				
			||||||
 | 
					                "-d",
 | 
				
			||||||
 | 
					                "--build"
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										105
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										105
									
								
								README.md
									
									
									
									
									
								
							@@ -6,15 +6,15 @@
 | 
				
			|||||||
[](https://github.com/python/black)
 | 
					[](https://github.com/python/black)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Tactical RMM is a remote monitoring & management tool for Windows computers, built with Django and Vue.\
 | 
					Tactical RMM is a remote monitoring & management tool for Windows computers, built with Django and Vue.\
 | 
				
			||||||
It uses an [agent](https://github.com/wh1te909/rmmagent) written in golang, as well as the [SaltStack](https://github.com/saltstack/salt) api and [MeshCentral](https://github.com/Ylianst/MeshCentral)
 | 
					It uses an [agent](https://github.com/wh1te909/rmmagent) written in golang and integrates with [MeshCentral](https://github.com/Ylianst/MeshCentral)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# [LIVE DEMO](https://rmm.xlawgaming.com/)
 | 
					# [LIVE DEMO](https://rmm.tacticalrmm.io/)
 | 
				
			||||||
Demo database resets every hour. Alot of features are disabled for obvious reasons due to the nature of this app.
 | 
					Demo database resets every hour. Alot of features are disabled for obvious reasons due to the nature of this app.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
*Tactical RMM is currently in alpha and subject to breaking changes. Use in production at your own risk.*
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### [Discord Chat](https://discord.gg/upGTkWp)
 | 
					### [Discord Chat](https://discord.gg/upGTkWp)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### [Documentation](https://wh1te909.github.io/tacticalrmm/)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Features
 | 
					## Features
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- Teamviewer-like remote desktop control
 | 
					- Teamviewer-like remote desktop control
 | 
				
			||||||
@@ -33,99 +33,6 @@ Demo database resets every hour. Alot of features are disabled for obvious reaso
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
- Windows 7, 8.1, 10, Server 2008R2, 2012R2, 2016, 2019
 | 
					- Windows 7, 8.1, 10, Server 2008R2, 2012R2, 2016, 2019
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Installation
 | 
					## Installation / Backup / Restore / Usage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Requirements
 | 
					### Refer to the [documentation](https://wh1te909.github.io/tacticalrmm/)
 | 
				
			||||||
- VPS with 4GB ram (an install script is provided for Ubuntu Server 20.04)
 | 
					 | 
				
			||||||
- A domain you own with at least 3 subdomains
 | 
					 | 
				
			||||||
- Google Authenticator app (2 factor is NOT optional)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Docker
 | 
					 | 
				
			||||||
Refer to the [docker setup](docker/readme.md)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Installation example (Ubuntu server 20.04 LTS)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Fresh VPS with latest updates\
 | 
					 | 
				
			||||||
login as root and create a user and add to sudoers group (we will be creating a user called tactical)
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
apt update && apt -y upgrade
 | 
					 | 
				
			||||||
adduser tactical
 | 
					 | 
				
			||||||
usermod -a -G sudo tactical
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
switch to the tactical user and setup the firewall
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
su - tactical
 | 
					 | 
				
			||||||
sudo ufw default deny incoming
 | 
					 | 
				
			||||||
sudo ufw default allow outgoing
 | 
					 | 
				
			||||||
sudo ufw allow ssh
 | 
					 | 
				
			||||||
sudo ufw allow http
 | 
					 | 
				
			||||||
sudo ufw allow https
 | 
					 | 
				
			||||||
sudo ufw allow proto tcp from any to any port 4505,4506
 | 
					 | 
				
			||||||
sudo ufw allow proto tcp from any to any port 4222
 | 
					 | 
				
			||||||
sudo ufw enable && sudo ufw reload
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Our domain for this example is tacticalrmm.com
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
In the DNS manager of wherever our domain is hosted, we will create three A records, all pointing to the public IP address of our VPS
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Create A record ```api.tacticalrmm.com``` for the django rest backend\
 | 
					 | 
				
			||||||
Create A record ```rmm.tacticalrmm.com``` for the vue frontend\
 | 
					 | 
				
			||||||
Create A record ```mesh.tacticalrmm.com``` for meshcentral
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Download the install script and run it
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
wget https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/install.sh
 | 
					 | 
				
			||||||
chmod +x install.sh
 | 
					 | 
				
			||||||
./install.sh
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 Links will be provided at the end of the install script.\
 | 
					 | 
				
			||||||
 Download the executable from the first link, then open ```rmm.tacticalrmm.com``` and login.\
 | 
					 | 
				
			||||||
 Upload the executable when prompted during the initial setup page.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Install an agent
 | 
					 | 
				
			||||||
From the app's dashboard, choose Agents > Install Agent to generate an installer.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Updating
 | 
					 | 
				
			||||||
Download and run [update.sh](https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/update.sh)
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
wget https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/update.sh
 | 
					 | 
				
			||||||
chmod +x update.sh
 | 
					 | 
				
			||||||
./update.sh
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Backup
 | 
					 | 
				
			||||||
Download [backup.sh](https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/backup.sh)
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
wget https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/backup.sh
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
Change the postgres username and password at the top of the file (you can find them in `/rmm/api/tacticalrmm/tacticalrmm/local_settings.py` under the DATABASES section)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Run it
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
chmod +x backup.sh
 | 
					 | 
				
			||||||
./backup.sh
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Restore
 | 
					 | 
				
			||||||
Change your 3 A records to point to new server's public IP
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Create same linux user account as old server and add to sudoers group and setup firewall (see install instructions above)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Copy backup file to new server
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Download the restore script, and edit the postgres username/password at the top of the file. Same instructions as above in the backup steps.
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
wget https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/restore.sh
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Run the restore script, passing it the backup tar file as the first argument
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
chmod +x restore.sh
 | 
					 | 
				
			||||||
./restore.sh rmm-backup-xxxxxxx.tar
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
@@ -1,457 +0,0 @@
 | 
				
			|||||||
from __future__ import absolute_import
 | 
					 | 
				
			||||||
import psutil
 | 
					 | 
				
			||||||
import os
 | 
					 | 
				
			||||||
import datetime
 | 
					 | 
				
			||||||
import zlib
 | 
					 | 
				
			||||||
import json
 | 
					 | 
				
			||||||
import base64
 | 
					 | 
				
			||||||
import wmi
 | 
					 | 
				
			||||||
import win32evtlog
 | 
					 | 
				
			||||||
import win32con
 | 
					 | 
				
			||||||
import win32evtlogutil
 | 
					 | 
				
			||||||
import winerror
 | 
					 | 
				
			||||||
from time import sleep
 | 
					 | 
				
			||||||
import requests
 | 
					 | 
				
			||||||
import subprocess
 | 
					 | 
				
			||||||
import random
 | 
					 | 
				
			||||||
import platform
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
ARCH = "64" if platform.machine().endswith("64") else "32"
 | 
					 | 
				
			||||||
PROGRAM_DIR = os.path.join(os.environ["ProgramFiles"], "TacticalAgent")
 | 
					 | 
				
			||||||
TAC_RMM = os.path.join(PROGRAM_DIR, "tacticalrmm.exe")
 | 
					 | 
				
			||||||
NSSM = os.path.join(PROGRAM_DIR, "nssm.exe" if ARCH == "64" else "nssm-x86.exe")
 | 
					 | 
				
			||||||
TEMP_DIR = os.path.join(os.environ["WINDIR"], "Temp")
 | 
					 | 
				
			||||||
SYS_DRIVE = os.environ["SystemDrive"]
 | 
					 | 
				
			||||||
PY_BIN = os.path.join(SYS_DRIVE, "\\salt", "bin", "python.exe")
 | 
					 | 
				
			||||||
SALT_CALL = os.path.join(SYS_DRIVE, "\\salt", "salt-call.bat")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def get_services():
 | 
					 | 
				
			||||||
    # see https://github.com/wh1te909/tacticalrmm/issues/38
 | 
					 | 
				
			||||||
    # for why I am manually implementing the svc.as_dict() method of psutil
 | 
					 | 
				
			||||||
    ret = []
 | 
					 | 
				
			||||||
    for svc in psutil.win_service_iter():
 | 
					 | 
				
			||||||
        i = {}
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            i["display_name"] = svc.display_name()
 | 
					 | 
				
			||||||
            i["binpath"] = svc.binpath()
 | 
					 | 
				
			||||||
            i["username"] = svc.username()
 | 
					 | 
				
			||||||
            i["start_type"] = svc.start_type()
 | 
					 | 
				
			||||||
            i["status"] = svc.status()
 | 
					 | 
				
			||||||
            i["pid"] = svc.pid()
 | 
					 | 
				
			||||||
            i["name"] = svc.name()
 | 
					 | 
				
			||||||
            i["description"] = svc.description()
 | 
					 | 
				
			||||||
        except Exception:
 | 
					 | 
				
			||||||
            continue
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            ret.append(i)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return ret
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def run_python_script(filename, timeout, script_type="userdefined"):
 | 
					 | 
				
			||||||
    # no longer used in agent version 0.11.0
 | 
					 | 
				
			||||||
    file_path = os.path.join(TEMP_DIR, filename)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if os.path.exists(file_path):
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            os.remove(file_path)
 | 
					 | 
				
			||||||
        except:
 | 
					 | 
				
			||||||
            pass
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if script_type == "userdefined":
 | 
					 | 
				
			||||||
        __salt__["cp.get_file"](f"salt://scripts/userdefined/{filename}", file_path)
 | 
					 | 
				
			||||||
    else:
 | 
					 | 
				
			||||||
        __salt__["cp.get_file"](f"salt://scripts/{filename}", file_path)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return __salt__["cmd.run_all"](f"{PY_BIN} {file_path}", timeout=timeout)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def run_script(filepath, filename, shell, timeout, args=[], bg=False):
 | 
					 | 
				
			||||||
    if shell == "powershell" or shell == "cmd":
 | 
					 | 
				
			||||||
        if args:
 | 
					 | 
				
			||||||
            return __salt__["cmd.script"](
 | 
					 | 
				
			||||||
                source=filepath,
 | 
					 | 
				
			||||||
                args=" ".join(map(lambda x: f'"{x}"', args)),
 | 
					 | 
				
			||||||
                shell=shell,
 | 
					 | 
				
			||||||
                timeout=timeout,
 | 
					 | 
				
			||||||
                bg=bg,
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            return __salt__["cmd.script"](
 | 
					 | 
				
			||||||
                source=filepath, shell=shell, timeout=timeout, bg=bg
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    elif shell == "python":
 | 
					 | 
				
			||||||
        file_path = os.path.join(TEMP_DIR, filename)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if os.path.exists(file_path):
 | 
					 | 
				
			||||||
            try:
 | 
					 | 
				
			||||||
                os.remove(file_path)
 | 
					 | 
				
			||||||
            except:
 | 
					 | 
				
			||||||
                pass
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        __salt__["cp.get_file"](filepath, file_path)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        salt_cmd = "cmd.run_bg" if bg else "cmd.run_all"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if args:
 | 
					 | 
				
			||||||
            a = " ".join(map(lambda x: f'"{x}"', args))
 | 
					 | 
				
			||||||
            cmd = f"{PY_BIN} {file_path} {a}"
 | 
					 | 
				
			||||||
            return __salt__[salt_cmd](cmd, timeout=timeout)
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            return __salt__[salt_cmd](f"{PY_BIN} {file_path}", timeout=timeout)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def uninstall_agent():
 | 
					 | 
				
			||||||
    remove_exe = os.path.join(PROGRAM_DIR, "unins000.exe")
 | 
					 | 
				
			||||||
    __salt__["cmd.run_bg"]([remove_exe, "/VERYSILENT", "/SUPPRESSMSGBOXES"])
 | 
					 | 
				
			||||||
    return "ok"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def update_salt():
 | 
					 | 
				
			||||||
    for p in psutil.process_iter():
 | 
					 | 
				
			||||||
        with p.oneshot():
 | 
					 | 
				
			||||||
            if p.name() == "tacticalrmm.exe" and "updatesalt" in p.cmdline():
 | 
					 | 
				
			||||||
                return "running"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    from subprocess import Popen, PIPE
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    CREATE_NEW_PROCESS_GROUP = 0x00000200
 | 
					 | 
				
			||||||
    DETACHED_PROCESS = 0x00000008
 | 
					 | 
				
			||||||
    cmd = [TAC_RMM, "-m", "updatesalt"]
 | 
					 | 
				
			||||||
    p = Popen(
 | 
					 | 
				
			||||||
        cmd,
 | 
					 | 
				
			||||||
        stdin=PIPE,
 | 
					 | 
				
			||||||
        stdout=PIPE,
 | 
					 | 
				
			||||||
        stderr=PIPE,
 | 
					 | 
				
			||||||
        close_fds=True,
 | 
					 | 
				
			||||||
        creationflags=DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP,
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    return p.pid
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def run_manual_checks():
 | 
					 | 
				
			||||||
    __salt__["cmd.run_bg"]([TAC_RMM, "-m", "runchecks"])
 | 
					 | 
				
			||||||
    return "ok"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def install_updates():
 | 
					 | 
				
			||||||
    for p in psutil.process_iter():
 | 
					 | 
				
			||||||
        with p.oneshot():
 | 
					 | 
				
			||||||
            if p.name() == "tacticalrmm.exe" and "winupdater" in p.cmdline():
 | 
					 | 
				
			||||||
                return "running"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return __salt__["cmd.run_bg"]([TAC_RMM, "-m", "winupdater"])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def _wait_for_service(svc, status, retries=10):
 | 
					 | 
				
			||||||
    attempts = 0
 | 
					 | 
				
			||||||
    while 1:
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            service = psutil.win_service_get(svc)
 | 
					 | 
				
			||||||
        except psutil.NoSuchProcess:
 | 
					 | 
				
			||||||
            stat = "fail"
 | 
					 | 
				
			||||||
            attempts += 1
 | 
					 | 
				
			||||||
            sleep(5)
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            stat = service.status()
 | 
					 | 
				
			||||||
            if stat != status:
 | 
					 | 
				
			||||||
                attempts += 1
 | 
					 | 
				
			||||||
                sleep(5)
 | 
					 | 
				
			||||||
            else:
 | 
					 | 
				
			||||||
                attempts = 0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if attempts == 0 or attempts > retries:
 | 
					 | 
				
			||||||
            break
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return stat
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def agent_update_v2(inno, url):
 | 
					 | 
				
			||||||
    # make sure another instance of the update is not running
 | 
					 | 
				
			||||||
    # this function spawns 2 instances of itself (because we call it twice with salt run_bg)
 | 
					 | 
				
			||||||
    # so if more than 2 running, don't continue as an update is already running
 | 
					 | 
				
			||||||
    count = 0
 | 
					 | 
				
			||||||
    for p in psutil.process_iter():
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            with p.oneshot():
 | 
					 | 
				
			||||||
                if "win_agent.agent_update_v2" in p.cmdline():
 | 
					 | 
				
			||||||
                    count += 1
 | 
					 | 
				
			||||||
        except Exception:
 | 
					 | 
				
			||||||
            continue
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if count > 2:
 | 
					 | 
				
			||||||
        return "already running"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    sleep(random.randint(1, 20))  # don't flood the rmm
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    exe = os.path.join(TEMP_DIR, inno)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if os.path.exists(exe):
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            os.remove(exe)
 | 
					 | 
				
			||||||
        except:
 | 
					 | 
				
			||||||
            pass
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    try:
 | 
					 | 
				
			||||||
        r = requests.get(url, stream=True, timeout=600)
 | 
					 | 
				
			||||||
    except Exception:
 | 
					 | 
				
			||||||
        return "failed"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if r.status_code != 200:
 | 
					 | 
				
			||||||
        return "failed"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    with open(exe, "wb") as f:
 | 
					 | 
				
			||||||
        for chunk in r.iter_content(chunk_size=1024):
 | 
					 | 
				
			||||||
            if chunk:
 | 
					 | 
				
			||||||
                f.write(chunk)
 | 
					 | 
				
			||||||
    del r
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    ret = subprocess.run([exe, "/VERYSILENT", "/SUPPRESSMSGBOXES"], timeout=120)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    tac = _wait_for_service(svc="tacticalagent", status="running")
 | 
					 | 
				
			||||||
    if tac != "running":
 | 
					 | 
				
			||||||
        subprocess.run([NSSM, "start", "tacticalagent"], timeout=30)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    chk = _wait_for_service(svc="checkrunner", status="running")
 | 
					 | 
				
			||||||
    if chk != "running":
 | 
					 | 
				
			||||||
        subprocess.run([NSSM, "start", "checkrunner"], timeout=30)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return "ok"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def do_agent_update_v2(inno, url):
 | 
					 | 
				
			||||||
    return __salt__["cmd.run_bg"](
 | 
					 | 
				
			||||||
        [
 | 
					 | 
				
			||||||
            SALT_CALL,
 | 
					 | 
				
			||||||
            "win_agent.agent_update_v2",
 | 
					 | 
				
			||||||
            f"inno={inno}",
 | 
					 | 
				
			||||||
            f"url={url}",
 | 
					 | 
				
			||||||
            "--local",
 | 
					 | 
				
			||||||
        ]
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def agent_update(version, url):
 | 
					 | 
				
			||||||
    # make sure another instance of the update is not running
 | 
					 | 
				
			||||||
    # this function spawns 2 instances of itself so if more than 2 running,
 | 
					 | 
				
			||||||
    # don't continue as an update is already running
 | 
					 | 
				
			||||||
    count = 0
 | 
					 | 
				
			||||||
    for p in psutil.process_iter():
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            with p.oneshot():
 | 
					 | 
				
			||||||
                if "win_agent.agent_update" in p.cmdline():
 | 
					 | 
				
			||||||
                    count += 1
 | 
					 | 
				
			||||||
        except Exception:
 | 
					 | 
				
			||||||
            continue
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if count > 2:
 | 
					 | 
				
			||||||
        return "already running"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    sleep(random.randint(1, 60))  # don't flood the rmm
 | 
					 | 
				
			||||||
    try:
 | 
					 | 
				
			||||||
        r = requests.get(url, stream=True, timeout=600)
 | 
					 | 
				
			||||||
    except Exception:
 | 
					 | 
				
			||||||
        return "failed"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if r.status_code != 200:
 | 
					 | 
				
			||||||
        return "failed"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    exe = os.path.join(TEMP_DIR, f"winagent-v{version}.exe")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    with open(exe, "wb") as f:
 | 
					 | 
				
			||||||
        for chunk in r.iter_content(chunk_size=1024):
 | 
					 | 
				
			||||||
            if chunk:
 | 
					 | 
				
			||||||
                f.write(chunk)
 | 
					 | 
				
			||||||
    del r
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    services = ("tacticalagent", "checkrunner")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for svc in services:
 | 
					 | 
				
			||||||
        subprocess.run([NSSM, "stop", svc], timeout=120)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    sleep(10)
 | 
					 | 
				
			||||||
    r = subprocess.run([exe, "/VERYSILENT", "/SUPPRESSMSGBOXES"], timeout=300)
 | 
					 | 
				
			||||||
    sleep(30)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for svc in services:
 | 
					 | 
				
			||||||
        subprocess.run([NSSM, "start", svc], timeout=120)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return "ok"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def do_agent_update(version, url):
 | 
					 | 
				
			||||||
    return __salt__["cmd.run_bg"](
 | 
					 | 
				
			||||||
        [
 | 
					 | 
				
			||||||
            SALT_CALL,
 | 
					 | 
				
			||||||
            "win_agent.agent_update",
 | 
					 | 
				
			||||||
            f"version={version}",
 | 
					 | 
				
			||||||
            f"url={url}",
 | 
					 | 
				
			||||||
            "--local",
 | 
					 | 
				
			||||||
        ]
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class SystemDetail:
 | 
					 | 
				
			||||||
    def __init__(self):
 | 
					 | 
				
			||||||
        self.c = wmi.WMI()
 | 
					 | 
				
			||||||
        self.comp_sys_prod = self.c.Win32_ComputerSystemProduct()
 | 
					 | 
				
			||||||
        self.comp_sys = self.c.Win32_ComputerSystem()
 | 
					 | 
				
			||||||
        self.memory = self.c.Win32_PhysicalMemory()
 | 
					 | 
				
			||||||
        self.os = self.c.Win32_OperatingSystem()
 | 
					 | 
				
			||||||
        self.base_board = self.c.Win32_BaseBoard()
 | 
					 | 
				
			||||||
        self.bios = self.c.Win32_BIOS()
 | 
					 | 
				
			||||||
        self.disk = self.c.Win32_DiskDrive()
 | 
					 | 
				
			||||||
        self.network_adapter = self.c.Win32_NetworkAdapter()
 | 
					 | 
				
			||||||
        self.network_config = self.c.Win32_NetworkAdapterConfiguration()
 | 
					 | 
				
			||||||
        self.desktop_monitor = self.c.Win32_DesktopMonitor()
 | 
					 | 
				
			||||||
        self.cpu = self.c.Win32_Processor()
 | 
					 | 
				
			||||||
        self.usb = self.c.Win32_USBController()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_all(self, obj):
 | 
					 | 
				
			||||||
        ret = []
 | 
					 | 
				
			||||||
        for i in obj:
 | 
					 | 
				
			||||||
            tmp = [
 | 
					 | 
				
			||||||
                {j: getattr(i, j)}
 | 
					 | 
				
			||||||
                for j in list(i.properties)
 | 
					 | 
				
			||||||
                if getattr(i, j) is not None
 | 
					 | 
				
			||||||
            ]
 | 
					 | 
				
			||||||
            ret.append(tmp)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return ret
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def system_info():
 | 
					 | 
				
			||||||
    info = SystemDetail()
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
        "comp_sys_prod": info.get_all(info.comp_sys_prod),
 | 
					 | 
				
			||||||
        "comp_sys": info.get_all(info.comp_sys),
 | 
					 | 
				
			||||||
        "mem": info.get_all(info.memory),
 | 
					 | 
				
			||||||
        "os": info.get_all(info.os),
 | 
					 | 
				
			||||||
        "base_board": info.get_all(info.base_board),
 | 
					 | 
				
			||||||
        "bios": info.get_all(info.bios),
 | 
					 | 
				
			||||||
        "disk": info.get_all(info.disk),
 | 
					 | 
				
			||||||
        "network_adapter": info.get_all(info.network_adapter),
 | 
					 | 
				
			||||||
        "network_config": info.get_all(info.network_config),
 | 
					 | 
				
			||||||
        "desktop_monitor": info.get_all(info.desktop_monitor),
 | 
					 | 
				
			||||||
        "cpu": info.get_all(info.cpu),
 | 
					 | 
				
			||||||
        "usb": info.get_all(info.usb),
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def local_sys_info():
 | 
					 | 
				
			||||||
    return __salt__["cmd.run_bg"]([TAC_RMM, "-m", "sysinfo"])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def get_procs():
 | 
					 | 
				
			||||||
    ret = []
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # setup
 | 
					 | 
				
			||||||
    for proc in psutil.process_iter():
 | 
					 | 
				
			||||||
        with proc.oneshot():
 | 
					 | 
				
			||||||
            proc.cpu_percent(interval=None)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # need time for psutil to record cpu percent
 | 
					 | 
				
			||||||
    sleep(1)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for c, proc in enumerate(psutil.process_iter(), 1):
 | 
					 | 
				
			||||||
        x = {}
 | 
					 | 
				
			||||||
        with proc.oneshot():
 | 
					 | 
				
			||||||
            if proc.pid == 0 or not proc.name():
 | 
					 | 
				
			||||||
                continue
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            x["name"] = proc.name()
 | 
					 | 
				
			||||||
            x["cpu_percent"] = proc.cpu_percent(interval=None) / psutil.cpu_count()
 | 
					 | 
				
			||||||
            x["memory_percent"] = proc.memory_percent()
 | 
					 | 
				
			||||||
            x["pid"] = proc.pid
 | 
					 | 
				
			||||||
            x["ppid"] = proc.ppid()
 | 
					 | 
				
			||||||
            x["status"] = proc.status()
 | 
					 | 
				
			||||||
            x["username"] = proc.username()
 | 
					 | 
				
			||||||
            x["id"] = c
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        ret.append(x)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return ret
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def _compress_json(j):
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
        "wineventlog": base64.b64encode(
 | 
					 | 
				
			||||||
            zlib.compress(json.dumps(j).encode("utf-8", errors="ignore"))
 | 
					 | 
				
			||||||
        ).decode("ascii", errors="ignore")
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def get_eventlog(logtype, last_n_days):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    start_time = datetime.datetime.now() - datetime.timedelta(days=last_n_days)
 | 
					 | 
				
			||||||
    flags = win32evtlog.EVENTLOG_BACKWARDS_READ | win32evtlog.EVENTLOG_SEQUENTIAL_READ
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    status_dict = {
 | 
					 | 
				
			||||||
        win32con.EVENTLOG_AUDIT_FAILURE: "AUDIT_FAILURE",
 | 
					 | 
				
			||||||
        win32con.EVENTLOG_AUDIT_SUCCESS: "AUDIT_SUCCESS",
 | 
					 | 
				
			||||||
        win32con.EVENTLOG_INFORMATION_TYPE: "INFO",
 | 
					 | 
				
			||||||
        win32con.EVENTLOG_WARNING_TYPE: "WARNING",
 | 
					 | 
				
			||||||
        win32con.EVENTLOG_ERROR_TYPE: "ERROR",
 | 
					 | 
				
			||||||
        0: "INFO",
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    computer = "localhost"
 | 
					 | 
				
			||||||
    hand = win32evtlog.OpenEventLog(computer, logtype)
 | 
					 | 
				
			||||||
    total = win32evtlog.GetNumberOfEventLogRecords(hand)
 | 
					 | 
				
			||||||
    log = []
 | 
					 | 
				
			||||||
    uid = 0
 | 
					 | 
				
			||||||
    done = False
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    try:
 | 
					 | 
				
			||||||
        while 1:
 | 
					 | 
				
			||||||
            events = win32evtlog.ReadEventLog(hand, flags, 0)
 | 
					 | 
				
			||||||
            for ev_obj in events:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                uid += 1
 | 
					 | 
				
			||||||
                # return once total number of events reach or we'll be stuck in an infinite loop
 | 
					 | 
				
			||||||
                if uid >= total:
 | 
					 | 
				
			||||||
                    done = True
 | 
					 | 
				
			||||||
                    break
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                the_time = ev_obj.TimeGenerated.Format()
 | 
					 | 
				
			||||||
                time_obj = datetime.datetime.strptime(the_time, "%c")
 | 
					 | 
				
			||||||
                if time_obj < start_time:
 | 
					 | 
				
			||||||
                    done = True
 | 
					 | 
				
			||||||
                    break
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                computer = str(ev_obj.ComputerName)
 | 
					 | 
				
			||||||
                src = str(ev_obj.SourceName)
 | 
					 | 
				
			||||||
                evt_type = str(status_dict[ev_obj.EventType])
 | 
					 | 
				
			||||||
                evt_id = str(winerror.HRESULT_CODE(ev_obj.EventID))
 | 
					 | 
				
			||||||
                evt_category = str(ev_obj.EventCategory)
 | 
					 | 
				
			||||||
                record = str(ev_obj.RecordNumber)
 | 
					 | 
				
			||||||
                msg = (
 | 
					 | 
				
			||||||
                    str(win32evtlogutil.SafeFormatMessage(ev_obj, logtype))
 | 
					 | 
				
			||||||
                    .replace("<", "")
 | 
					 | 
				
			||||||
                    .replace(">", "")
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                event_dict = {
 | 
					 | 
				
			||||||
                    "computer": computer,
 | 
					 | 
				
			||||||
                    "source": src,
 | 
					 | 
				
			||||||
                    "eventType": evt_type,
 | 
					 | 
				
			||||||
                    "eventID": evt_id,
 | 
					 | 
				
			||||||
                    "eventCategory": evt_category,
 | 
					 | 
				
			||||||
                    "message": msg,
 | 
					 | 
				
			||||||
                    "time": the_time,
 | 
					 | 
				
			||||||
                    "record": record,
 | 
					 | 
				
			||||||
                    "uid": uid,
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                log.append(event_dict)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if done:
 | 
					 | 
				
			||||||
                break
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    except Exception:
 | 
					 | 
				
			||||||
        pass
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    win32evtlog.CloseEventLog(hand)
 | 
					 | 
				
			||||||
    return _compress_json(log)
 | 
					 | 
				
			||||||
@@ -1,8 +1,8 @@
 | 
				
			|||||||
from django.contrib import admin
 | 
					from django.contrib import admin
 | 
				
			||||||
 | 
					 | 
				
			||||||
from rest_framework.authtoken.admin import TokenAdmin
 | 
					from rest_framework.authtoken.admin import TokenAdmin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .models import User
 | 
					from .models import User, Role
 | 
				
			||||||
 | 
					
 | 
				
			||||||
admin.site.register(User)
 | 
					admin.site.register(User)
 | 
				
			||||||
TokenAdmin.raw_id_fields = ("user",)
 | 
					TokenAdmin.raw_id_fields = ("user",)
 | 
				
			||||||
 | 
					admin.site.register(Role)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,5 @@
 | 
				
			|||||||
from django.utils import timezone as djangotime
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from django.core.management.base import BaseCommand
 | 
					from django.core.management.base import BaseCommand
 | 
				
			||||||
 | 
					from django.utils import timezone as djangotime
 | 
				
			||||||
from knox.models import AuthToken
 | 
					from knox.models import AuthToken
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,11 +1,13 @@
 | 
				
			|||||||
import pyotp
 | 
					 | 
				
			||||||
import subprocess
 | 
					import subprocess
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import pyotp
 | 
				
			||||||
from django.core.management.base import BaseCommand
 | 
					from django.core.management.base import BaseCommand
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from accounts.models import User
 | 
					from accounts.models import User
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Command(BaseCommand):
 | 
					class Command(BaseCommand):
 | 
				
			||||||
    help = "Generates barcode for Google Authenticator and creates totp for user"
 | 
					    help = "Generates barcode for Authenticator and creates totp for user"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def add_arguments(self, parser):
 | 
					    def add_arguments(self, parser):
 | 
				
			||||||
        parser.add_argument("code", type=str)
 | 
					        parser.add_argument("code", type=str)
 | 
				
			||||||
@@ -24,12 +26,10 @@ class Command(BaseCommand):
 | 
				
			|||||||
        url = pyotp.totp.TOTP(code).provisioning_uri(username, issuer_name=domain)
 | 
					        url = pyotp.totp.TOTP(code).provisioning_uri(username, issuer_name=domain)
 | 
				
			||||||
        subprocess.run(f'qr "{url}"', shell=True)
 | 
					        subprocess.run(f'qr "{url}"', shell=True)
 | 
				
			||||||
        self.stdout.write(
 | 
					        self.stdout.write(
 | 
				
			||||||
            self.style.SUCCESS(
 | 
					            self.style.SUCCESS("Scan the barcode above with your authenticator app")
 | 
				
			||||||
                "Scan the barcode above with your google authenticator app"
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        self.stdout.write(
 | 
					        self.stdout.write(
 | 
				
			||||||
            self.style.SUCCESS(
 | 
					            self.style.SUCCESS(
 | 
				
			||||||
                f"If that doesn't work you may manually enter the key: {code}"
 | 
					                f"If that doesn't work you may manually enter the setup key: {code}"
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										57
									
								
								api/tacticalrmm/accounts/management/commands/reset_2fa.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								api/tacticalrmm/accounts/management/commands/reset_2fa.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,57 @@
 | 
				
			|||||||
 | 
					import os
 | 
				
			||||||
 | 
					import subprocess
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import pyotp
 | 
				
			||||||
 | 
					from django.core.management.base import BaseCommand
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from accounts.models import User
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Command(BaseCommand):
 | 
				
			||||||
 | 
					    help = "Reset 2fa"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def add_arguments(self, parser):
 | 
				
			||||||
 | 
					        parser.add_argument("username", type=str)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def handle(self, *args, **kwargs):
 | 
				
			||||||
 | 
					        username = kwargs["username"]
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            user = User.objects.get(username=username)
 | 
				
			||||||
 | 
					        except User.DoesNotExist:
 | 
				
			||||||
 | 
					            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)
 | 
				
			||||||
 | 
					        subprocess.run(f'qr "{url}"', shell=True)
 | 
				
			||||||
 | 
					        self.stdout.write(
 | 
				
			||||||
 | 
					            self.style.WARNING("Scan the barcode above with your authenticator app")
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.stdout.write(
 | 
				
			||||||
 | 
					            self.style.WARNING(
 | 
				
			||||||
 | 
					                f"If that doesn't work you may manually enter the setup key: {code}"
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.stdout.write(
 | 
				
			||||||
 | 
					            self.style.SUCCESS(f"2fa was successfully reset for user {username}")
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
@@ -0,0 +1,22 @@
 | 
				
			|||||||
 | 
					from django.core.management.base import BaseCommand
 | 
				
			||||||
 | 
					from accounts.models import User
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Command(BaseCommand):
 | 
				
			||||||
 | 
					    help = "Reset password for user"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def add_arguments(self, parser):
 | 
				
			||||||
 | 
					        parser.add_argument("username", type=str)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def handle(self, *args, **kwargs):
 | 
				
			||||||
 | 
					        username = kwargs["username"]
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            user = User.objects.get(username=username)
 | 
				
			||||||
 | 
					        except User.DoesNotExist:
 | 
				
			||||||
 | 
					            self.stdout.write(self.style.ERROR(f"User {username} doesn't exist"))
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        passwd = input("Enter new password: ")
 | 
				
			||||||
 | 
					        user.set_password(passwd)
 | 
				
			||||||
 | 
					        user.save()
 | 
				
			||||||
 | 
					        self.stdout.write(self.style.SUCCESS(f"Password for {username} was reset!"))
 | 
				
			||||||
@@ -2,8 +2,8 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import django.contrib.auth.models
 | 
					import django.contrib.auth.models
 | 
				
			||||||
import django.contrib.auth.validators
 | 
					import django.contrib.auth.validators
 | 
				
			||||||
from django.db import migrations, models
 | 
					 | 
				
			||||||
import django.utils.timezone
 | 
					import django.utils.timezone
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Migration(migrations.Migration):
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,28 +6,28 @@ from django.db import migrations, models
 | 
				
			|||||||
class Migration(migrations.Migration):
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    dependencies = [
 | 
					    dependencies = [
 | 
				
			||||||
        ('accounts', '0002_auto_20200810_0544'),
 | 
					        ("accounts", "0002_auto_20200810_0544"),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    operations = [
 | 
					    operations = [
 | 
				
			||||||
        migrations.AddField(
 | 
					        migrations.AddField(
 | 
				
			||||||
            model_name='user',
 | 
					            model_name="user",
 | 
				
			||||||
            name='created_by',
 | 
					            name="created_by",
 | 
				
			||||||
            field=models.CharField(blank=True, max_length=100, null=True),
 | 
					            field=models.CharField(blank=True, max_length=100, null=True),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        migrations.AddField(
 | 
					        migrations.AddField(
 | 
				
			||||||
            model_name='user',
 | 
					            model_name="user",
 | 
				
			||||||
            name='created_time',
 | 
					            name="created_time",
 | 
				
			||||||
            field=models.DateTimeField(auto_now_add=True, null=True),
 | 
					            field=models.DateTimeField(auto_now_add=True, null=True),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        migrations.AddField(
 | 
					        migrations.AddField(
 | 
				
			||||||
            model_name='user',
 | 
					            model_name="user",
 | 
				
			||||||
            name='modified_by',
 | 
					            name="modified_by",
 | 
				
			||||||
            field=models.CharField(blank=True, max_length=100, null=True),
 | 
					            field=models.CharField(blank=True, max_length=100, null=True),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        migrations.AddField(
 | 
					        migrations.AddField(
 | 
				
			||||||
            model_name='user',
 | 
					            model_name="user",
 | 
				
			||||||
            name='modified_time',
 | 
					            name="modified_time",
 | 
				
			||||||
            field=models.DateTimeField(auto_now=True, null=True),
 | 
					            field=models.DateTimeField(auto_now=True, null=True),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,24 +6,24 @@ from django.db import migrations
 | 
				
			|||||||
class Migration(migrations.Migration):
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    dependencies = [
 | 
					    dependencies = [
 | 
				
			||||||
        ('accounts', '0003_auto_20200922_1344'),
 | 
					        ("accounts", "0003_auto_20200922_1344"),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    operations = [
 | 
					    operations = [
 | 
				
			||||||
        migrations.RemoveField(
 | 
					        migrations.RemoveField(
 | 
				
			||||||
            model_name='user',
 | 
					            model_name="user",
 | 
				
			||||||
            name='created_by',
 | 
					            name="created_by",
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        migrations.RemoveField(
 | 
					        migrations.RemoveField(
 | 
				
			||||||
            model_name='user',
 | 
					            model_name="user",
 | 
				
			||||||
            name='created_time',
 | 
					            name="created_time",
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        migrations.RemoveField(
 | 
					        migrations.RemoveField(
 | 
				
			||||||
            model_name='user',
 | 
					            model_name="user",
 | 
				
			||||||
            name='modified_by',
 | 
					            name="modified_by",
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        migrations.RemoveField(
 | 
					        migrations.RemoveField(
 | 
				
			||||||
            model_name='user',
 | 
					            model_name="user",
 | 
				
			||||||
            name='modified_time',
 | 
					            name="modified_time",
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,28 +6,28 @@ from django.db import migrations, models
 | 
				
			|||||||
class Migration(migrations.Migration):
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    dependencies = [
 | 
					    dependencies = [
 | 
				
			||||||
        ('accounts', '0004_auto_20201002_1257'),
 | 
					        ("accounts", "0004_auto_20201002_1257"),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    operations = [
 | 
					    operations = [
 | 
				
			||||||
        migrations.AddField(
 | 
					        migrations.AddField(
 | 
				
			||||||
            model_name='user',
 | 
					            model_name="user",
 | 
				
			||||||
            name='created_by',
 | 
					            name="created_by",
 | 
				
			||||||
            field=models.CharField(blank=True, max_length=100, null=True),
 | 
					            field=models.CharField(blank=True, max_length=100, null=True),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        migrations.AddField(
 | 
					        migrations.AddField(
 | 
				
			||||||
            model_name='user',
 | 
					            model_name="user",
 | 
				
			||||||
            name='created_time',
 | 
					            name="created_time",
 | 
				
			||||||
            field=models.DateTimeField(auto_now_add=True, null=True),
 | 
					            field=models.DateTimeField(auto_now_add=True, null=True),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        migrations.AddField(
 | 
					        migrations.AddField(
 | 
				
			||||||
            model_name='user',
 | 
					            model_name="user",
 | 
				
			||||||
            name='modified_by',
 | 
					            name="modified_by",
 | 
				
			||||||
            field=models.CharField(blank=True, max_length=100, null=True),
 | 
					            field=models.CharField(blank=True, max_length=100, null=True),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        migrations.AddField(
 | 
					        migrations.AddField(
 | 
				
			||||||
            model_name='user',
 | 
					            model_name="user",
 | 
				
			||||||
            name='modified_time',
 | 
					            name="modified_time",
 | 
				
			||||||
            field=models.DateTimeField(auto_now=True, null=True),
 | 
					            field=models.DateTimeField(auto_now=True, null=True),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
# Generated by Django 3.1.2 on 2020-11-10 20:24
 | 
					# Generated by Django 3.1.2 on 2020-11-10 20:24
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.db import migrations, models
 | 
					 | 
				
			||||||
import django.db.models.deletion
 | 
					import django.db.models.deletion
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Migration(migrations.Migration):
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,13 +6,13 @@ from django.db import migrations, models
 | 
				
			|||||||
class Migration(migrations.Migration):
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    dependencies = [
 | 
					    dependencies = [
 | 
				
			||||||
        ('accounts', '0007_update_agent_primary_key'),
 | 
					        ("accounts", "0007_update_agent_primary_key"),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    operations = [
 | 
					    operations = [
 | 
				
			||||||
        migrations.AddField(
 | 
					        migrations.AddField(
 | 
				
			||||||
            model_name='user',
 | 
					            model_name="user",
 | 
				
			||||||
            name='dark_mode',
 | 
					            name="dark_mode",
 | 
				
			||||||
            field=models.BooleanField(default=True),
 | 
					            field=models.BooleanField(default=True),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.1.4 on 2020-12-10 17:00
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ("accounts", "0008_user_dark_mode"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name="user",
 | 
				
			||||||
 | 
					            name="show_community_scripts",
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.1.4 on 2021-01-14 01:23
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ("accounts", "0009_user_show_community_scripts"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name="user",
 | 
				
			||||||
 | 
					            name="agent_dblclick_action",
 | 
				
			||||||
 | 
					            field=models.CharField(
 | 
				
			||||||
 | 
					                choices=[
 | 
				
			||||||
 | 
					                    ("editagent", "Edit Agent"),
 | 
				
			||||||
 | 
					                    ("takecontrol", "Take Control"),
 | 
				
			||||||
 | 
					                    ("remotebg", "Remote Background"),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					                default="editagent",
 | 
				
			||||||
 | 
					                max_length=50,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.1.5 on 2021-01-18 09:40
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ("accounts", "0010_user_agent_dblclick_action"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name="user",
 | 
				
			||||||
 | 
					            name="default_agent_tbl_tab",
 | 
				
			||||||
 | 
					            field=models.CharField(
 | 
				
			||||||
 | 
					                choices=[
 | 
				
			||||||
 | 
					                    ("server", "Servers"),
 | 
				
			||||||
 | 
					                    ("workstation", "Workstations"),
 | 
				
			||||||
 | 
					                    ("mixed", "Mixed"),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					                default="server",
 | 
				
			||||||
 | 
					                max_length=50,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.1.7 on 2021-02-28 06:38
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('accounts', '0011_user_default_agent_tbl_tab'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='agents_per_page',
 | 
				
			||||||
 | 
					            field=models.PositiveIntegerField(default=50),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.1.7 on 2021-03-09 02:33
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('accounts', '0012_user_agents_per_page'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='client_tree_sort',
 | 
				
			||||||
 | 
					            field=models.CharField(choices=[('alphafail', 'Move failing clients to the top'), ('alpha', 'Sort alphabetically')], default='alphafail', max_length=50),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.2 on 2021-04-11 01:43
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('accounts', '0013_user_client_tree_sort'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='client_tree_splitter',
 | 
				
			||||||
 | 
					            field=models.PositiveIntegerField(default=11),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.2 on 2021-04-11 03:03
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('accounts', '0014_user_client_tree_splitter'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='loading_bar_color',
 | 
				
			||||||
 | 
					            field=models.CharField(default='red', max_length=255),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.2.1 on 2021-05-07 15:26
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import django.db.models.deletion
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('core', '0022_urlaction'),
 | 
				
			||||||
 | 
					        ('accounts', '0015_user_loading_bar_color'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='url_action',
 | 
				
			||||||
 | 
					            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='user', to='core.urlaction'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='agent_dblclick_action',
 | 
				
			||||||
 | 
					            field=models.CharField(choices=[('editagent', 'Edit Agent'), ('takecontrol', 'Take Control'), ('remotebg', 'Remote Background'), ('urlaction', 'URL Action')], default='editagent', max_length=50),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
							
								
								
									
										173
									
								
								api/tacticalrmm/accounts/migrations/0017_auto_20210508_1716.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								api/tacticalrmm/accounts/migrations/0017_auto_20210508_1716.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,173 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.2.1 on 2021-05-08 17:16
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('accounts', '0016_auto_20210507_1526'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_code_sign',
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_do_server_maint',
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_edit_agent',
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_edit_core_settings',
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_install_agents',
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_manage_accounts',
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_manage_alerts',
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_manage_automation_policies',
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_manage_autotasks',
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_manage_checks',
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_manage_clients',
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_manage_deployments',
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_manage_notes',
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_manage_pendingactions',
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_manage_procs',
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_manage_scripts',
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_manage_sites',
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_manage_software',
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_manage_winsvcs',
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_manage_winupdates',
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_reboot_agents',
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_run_autotasks',
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_run_bulk',
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_run_checks',
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_run_scripts',
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_send_cmd',
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_uninstall_agents',
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_update_agents',
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_use_mesh',
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_view_auditlogs',
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_view_debuglogs',
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_view_eventlogs',
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
							
								
								
									
										181
									
								
								api/tacticalrmm/accounts/migrations/0018_auto_20210511_0233.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										181
									
								
								api/tacticalrmm/accounts/migrations/0018_auto_20210511_0233.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,181 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.2.1 on 2021-05-11 02:33
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('accounts', '0017_auto_20210508_1716'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name='Role',
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
				
			||||||
 | 
					                ('name', models.CharField(max_length=255, unique=True)),
 | 
				
			||||||
 | 
					                ('is_superuser', models.BooleanField(default=False)),
 | 
				
			||||||
 | 
					                ('can_use_mesh', models.BooleanField(default=False)),
 | 
				
			||||||
 | 
					                ('can_uninstall_agents', models.BooleanField(default=False)),
 | 
				
			||||||
 | 
					                ('can_update_agents', models.BooleanField(default=False)),
 | 
				
			||||||
 | 
					                ('can_edit_agent', models.BooleanField(default=False)),
 | 
				
			||||||
 | 
					                ('can_manage_procs', models.BooleanField(default=False)),
 | 
				
			||||||
 | 
					                ('can_view_eventlogs', models.BooleanField(default=False)),
 | 
				
			||||||
 | 
					                ('can_send_cmd', models.BooleanField(default=False)),
 | 
				
			||||||
 | 
					                ('can_reboot_agents', models.BooleanField(default=False)),
 | 
				
			||||||
 | 
					                ('can_install_agents', models.BooleanField(default=False)),
 | 
				
			||||||
 | 
					                ('can_run_scripts', models.BooleanField(default=False)),
 | 
				
			||||||
 | 
					                ('can_run_bulk', models.BooleanField(default=False)),
 | 
				
			||||||
 | 
					                ('can_manage_notes', models.BooleanField(default=False)),
 | 
				
			||||||
 | 
					                ('can_edit_core_settings', models.BooleanField(default=False)),
 | 
				
			||||||
 | 
					                ('can_do_server_maint', models.BooleanField(default=False)),
 | 
				
			||||||
 | 
					                ('can_code_sign', models.BooleanField(default=False)),
 | 
				
			||||||
 | 
					                ('can_manage_checks', models.BooleanField(default=False)),
 | 
				
			||||||
 | 
					                ('can_run_checks', models.BooleanField(default=False)),
 | 
				
			||||||
 | 
					                ('can_manage_clients', models.BooleanField(default=False)),
 | 
				
			||||||
 | 
					                ('can_manage_sites', models.BooleanField(default=False)),
 | 
				
			||||||
 | 
					                ('can_manage_deployments', models.BooleanField(default=False)),
 | 
				
			||||||
 | 
					                ('can_manage_automation_policies', models.BooleanField(default=False)),
 | 
				
			||||||
 | 
					                ('can_manage_autotasks', models.BooleanField(default=False)),
 | 
				
			||||||
 | 
					                ('can_run_autotasks', models.BooleanField(default=False)),
 | 
				
			||||||
 | 
					                ('can_view_auditlogs', models.BooleanField(default=False)),
 | 
				
			||||||
 | 
					                ('can_manage_pendingactions', models.BooleanField(default=False)),
 | 
				
			||||||
 | 
					                ('can_view_debuglogs', models.BooleanField(default=False)),
 | 
				
			||||||
 | 
					                ('can_manage_scripts', models.BooleanField(default=False)),
 | 
				
			||||||
 | 
					                ('can_manage_alerts', models.BooleanField(default=False)),
 | 
				
			||||||
 | 
					                ('can_manage_winsvcs', models.BooleanField(default=False)),
 | 
				
			||||||
 | 
					                ('can_manage_software', models.BooleanField(default=False)),
 | 
				
			||||||
 | 
					                ('can_manage_winupdates', models.BooleanField(default=False)),
 | 
				
			||||||
 | 
					                ('can_manage_accounts', models.BooleanField(default=False)),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_code_sign',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_do_server_maint',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_edit_agent',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_edit_core_settings',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_install_agents',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_manage_accounts',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_manage_alerts',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_manage_automation_policies',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_manage_autotasks',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_manage_checks',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_manage_clients',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_manage_deployments',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_manage_notes',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_manage_pendingactions',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_manage_procs',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_manage_scripts',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_manage_sites',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_manage_software',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_manage_winsvcs',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_manage_winupdates',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_reboot_agents',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_run_autotasks',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_run_bulk',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_run_checks',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_run_scripts',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_send_cmd',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_uninstall_agents',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_update_agents',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_use_mesh',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_view_auditlogs',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_view_debuglogs',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='user',
 | 
				
			||||||
 | 
					            name='can_view_eventlogs',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
							
								
								
									
										25
									
								
								api/tacticalrmm/accounts/migrations/0019_user_role.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								api/tacticalrmm/accounts/migrations/0019_user_role.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.2.1 on 2021-05-11 02:33
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					import django.db.models.deletion
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ("accounts", "0018_auto_20210511_0233"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name="user",
 | 
				
			||||||
 | 
					            name="role",
 | 
				
			||||||
 | 
					            field=models.ForeignKey(
 | 
				
			||||||
 | 
					                blank=True,
 | 
				
			||||||
 | 
					                null=True,
 | 
				
			||||||
 | 
					                on_delete=django.db.models.deletion.SET_NULL,
 | 
				
			||||||
 | 
					                related_name="roles",
 | 
				
			||||||
 | 
					                to="accounts.role",
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.2.1 on 2021-05-11 17:37
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('accounts', '0019_user_role'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='role',
 | 
				
			||||||
 | 
					            name='can_manage_roles',
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -1,13 +1,51 @@
 | 
				
			|||||||
from django.db import models
 | 
					 | 
				
			||||||
from django.contrib.auth.models import AbstractUser
 | 
					from django.contrib.auth.models import AbstractUser
 | 
				
			||||||
 | 
					from django.db import models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from logs.models import BaseAuditModel
 | 
					from logs.models import BaseAuditModel
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					AGENT_DBLCLICK_CHOICES = [
 | 
				
			||||||
 | 
					    ("editagent", "Edit Agent"),
 | 
				
			||||||
 | 
					    ("takecontrol", "Take Control"),
 | 
				
			||||||
 | 
					    ("remotebg", "Remote Background"),
 | 
				
			||||||
 | 
					    ("urlaction", "URL Action"),
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					AGENT_TBL_TAB_CHOICES = [
 | 
				
			||||||
 | 
					    ("server", "Servers"),
 | 
				
			||||||
 | 
					    ("workstation", "Workstations"),
 | 
				
			||||||
 | 
					    ("mixed", "Mixed"),
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CLIENT_TREE_SORT_CHOICES = [
 | 
				
			||||||
 | 
					    ("alphafail", "Move failing clients to the top"),
 | 
				
			||||||
 | 
					    ("alpha", "Sort alphabetically"),
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class User(AbstractUser, BaseAuditModel):
 | 
					class User(AbstractUser, BaseAuditModel):
 | 
				
			||||||
    is_active = models.BooleanField(default=True)
 | 
					    is_active = models.BooleanField(default=True)
 | 
				
			||||||
    totp_key = models.CharField(max_length=50, null=True, blank=True)
 | 
					    totp_key = models.CharField(max_length=50, null=True, blank=True)
 | 
				
			||||||
    dark_mode = models.BooleanField(default=True)
 | 
					    dark_mode = models.BooleanField(default=True)
 | 
				
			||||||
 | 
					    show_community_scripts = models.BooleanField(default=True)
 | 
				
			||||||
 | 
					    agent_dblclick_action = models.CharField(
 | 
				
			||||||
 | 
					        max_length=50, choices=AGENT_DBLCLICK_CHOICES, default="editagent"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    url_action = models.ForeignKey(
 | 
				
			||||||
 | 
					        "core.URLAction",
 | 
				
			||||||
 | 
					        related_name="user",
 | 
				
			||||||
 | 
					        null=True,
 | 
				
			||||||
 | 
					        blank=True,
 | 
				
			||||||
 | 
					        on_delete=models.SET_NULL,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    default_agent_tbl_tab = models.CharField(
 | 
				
			||||||
 | 
					        max_length=50, choices=AGENT_TBL_TAB_CHOICES, default="server"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    agents_per_page = models.PositiveIntegerField(default=50)  # not currently used
 | 
				
			||||||
 | 
					    client_tree_sort = models.CharField(
 | 
				
			||||||
 | 
					        max_length=50, choices=CLIENT_TREE_SORT_CHOICES, default="alphafail"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    client_tree_splitter = models.PositiveIntegerField(default=11)
 | 
				
			||||||
 | 
					    loading_bar_color = models.CharField(max_length=255, default="red")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    agent = models.OneToOneField(
 | 
					    agent = models.OneToOneField(
 | 
				
			||||||
        "agents.Agent",
 | 
					        "agents.Agent",
 | 
				
			||||||
@@ -17,9 +55,123 @@ class User(AbstractUser, BaseAuditModel):
 | 
				
			|||||||
        on_delete=models.CASCADE,
 | 
					        on_delete=models.CASCADE,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    role = models.ForeignKey(
 | 
				
			||||||
 | 
					        "accounts.Role",
 | 
				
			||||||
 | 
					        null=True,
 | 
				
			||||||
 | 
					        blank=True,
 | 
				
			||||||
 | 
					        related_name="roles",
 | 
				
			||||||
 | 
					        on_delete=models.SET_NULL,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @staticmethod
 | 
					    @staticmethod
 | 
				
			||||||
    def serialize(user):
 | 
					    def serialize(user):
 | 
				
			||||||
        # serializes the task and returns json
 | 
					        # serializes the task and returns json
 | 
				
			||||||
        from .serializers import UserSerializer
 | 
					        from .serializers import UserSerializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return UserSerializer(user).data
 | 
					        return UserSerializer(user).data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Role(models.Model):
 | 
				
			||||||
 | 
					    name = models.CharField(max_length=255, unique=True)
 | 
				
			||||||
 | 
					    is_superuser = models.BooleanField(default=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # agents
 | 
				
			||||||
 | 
					    can_use_mesh = models.BooleanField(default=False)
 | 
				
			||||||
 | 
					    can_uninstall_agents = models.BooleanField(default=False)
 | 
				
			||||||
 | 
					    can_update_agents = models.BooleanField(default=False)
 | 
				
			||||||
 | 
					    can_edit_agent = models.BooleanField(default=False)
 | 
				
			||||||
 | 
					    can_manage_procs = models.BooleanField(default=False)
 | 
				
			||||||
 | 
					    can_view_eventlogs = models.BooleanField(default=False)
 | 
				
			||||||
 | 
					    can_send_cmd = models.BooleanField(default=False)
 | 
				
			||||||
 | 
					    can_reboot_agents = models.BooleanField(default=False)
 | 
				
			||||||
 | 
					    can_install_agents = models.BooleanField(default=False)
 | 
				
			||||||
 | 
					    can_run_scripts = models.BooleanField(default=False)
 | 
				
			||||||
 | 
					    can_run_bulk = models.BooleanField(default=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # core
 | 
				
			||||||
 | 
					    can_manage_notes = models.BooleanField(default=False)
 | 
				
			||||||
 | 
					    can_edit_core_settings = models.BooleanField(default=False)
 | 
				
			||||||
 | 
					    can_do_server_maint = models.BooleanField(default=False)
 | 
				
			||||||
 | 
					    can_code_sign = models.BooleanField(default=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # checks
 | 
				
			||||||
 | 
					    can_manage_checks = models.BooleanField(default=False)
 | 
				
			||||||
 | 
					    can_run_checks = models.BooleanField(default=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # clients
 | 
				
			||||||
 | 
					    can_manage_clients = models.BooleanField(default=False)
 | 
				
			||||||
 | 
					    can_manage_sites = models.BooleanField(default=False)
 | 
				
			||||||
 | 
					    can_manage_deployments = models.BooleanField(default=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # automation
 | 
				
			||||||
 | 
					    can_manage_automation_policies = models.BooleanField(default=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # automated tasks
 | 
				
			||||||
 | 
					    can_manage_autotasks = models.BooleanField(default=False)
 | 
				
			||||||
 | 
					    can_run_autotasks = models.BooleanField(default=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # logs
 | 
				
			||||||
 | 
					    can_view_auditlogs = models.BooleanField(default=False)
 | 
				
			||||||
 | 
					    can_manage_pendingactions = models.BooleanField(default=False)
 | 
				
			||||||
 | 
					    can_view_debuglogs = models.BooleanField(default=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # scripts
 | 
				
			||||||
 | 
					    can_manage_scripts = models.BooleanField(default=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # alerts
 | 
				
			||||||
 | 
					    can_manage_alerts = models.BooleanField(default=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # win services
 | 
				
			||||||
 | 
					    can_manage_winsvcs = models.BooleanField(default=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # software
 | 
				
			||||||
 | 
					    can_manage_software = models.BooleanField(default=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # windows updates
 | 
				
			||||||
 | 
					    can_manage_winupdates = models.BooleanField(default=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # accounts
 | 
				
			||||||
 | 
					    can_manage_accounts = models.BooleanField(default=False)
 | 
				
			||||||
 | 
					    can_manage_roles = models.BooleanField(default=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __str__(self):
 | 
				
			||||||
 | 
					        return self.name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @staticmethod
 | 
				
			||||||
 | 
					    def perms():
 | 
				
			||||||
 | 
					        return [
 | 
				
			||||||
 | 
					            "is_superuser",
 | 
				
			||||||
 | 
					            "can_use_mesh",
 | 
				
			||||||
 | 
					            "can_uninstall_agents",
 | 
				
			||||||
 | 
					            "can_update_agents",
 | 
				
			||||||
 | 
					            "can_edit_agent",
 | 
				
			||||||
 | 
					            "can_manage_procs",
 | 
				
			||||||
 | 
					            "can_view_eventlogs",
 | 
				
			||||||
 | 
					            "can_send_cmd",
 | 
				
			||||||
 | 
					            "can_reboot_agents",
 | 
				
			||||||
 | 
					            "can_install_agents",
 | 
				
			||||||
 | 
					            "can_run_scripts",
 | 
				
			||||||
 | 
					            "can_run_bulk",
 | 
				
			||||||
 | 
					            "can_manage_notes",
 | 
				
			||||||
 | 
					            "can_edit_core_settings",
 | 
				
			||||||
 | 
					            "can_do_server_maint",
 | 
				
			||||||
 | 
					            "can_code_sign",
 | 
				
			||||||
 | 
					            "can_manage_checks",
 | 
				
			||||||
 | 
					            "can_run_checks",
 | 
				
			||||||
 | 
					            "can_manage_clients",
 | 
				
			||||||
 | 
					            "can_manage_sites",
 | 
				
			||||||
 | 
					            "can_manage_deployments",
 | 
				
			||||||
 | 
					            "can_manage_automation_policies",
 | 
				
			||||||
 | 
					            "can_manage_autotasks",
 | 
				
			||||||
 | 
					            "can_run_autotasks",
 | 
				
			||||||
 | 
					            "can_view_auditlogs",
 | 
				
			||||||
 | 
					            "can_manage_pendingactions",
 | 
				
			||||||
 | 
					            "can_view_debuglogs",
 | 
				
			||||||
 | 
					            "can_manage_scripts",
 | 
				
			||||||
 | 
					            "can_manage_alerts",
 | 
				
			||||||
 | 
					            "can_manage_winsvcs",
 | 
				
			||||||
 | 
					            "can_manage_software",
 | 
				
			||||||
 | 
					            "can_manage_winupdates",
 | 
				
			||||||
 | 
					            "can_manage_accounts",
 | 
				
			||||||
 | 
					            "can_manage_roles",
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										19
									
								
								api/tacticalrmm/accounts/permissions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								api/tacticalrmm/accounts/permissions.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					from rest_framework import permissions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from tacticalrmm.permissions import _has_perm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AccountsPerms(permissions.BasePermission):
 | 
				
			||||||
 | 
					    def has_permission(self, r, view):
 | 
				
			||||||
 | 
					        if r.method == "GET":
 | 
				
			||||||
 | 
					            return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return _has_perm(r, "can_manage_accounts")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class RolesPerms(permissions.BasePermission):
 | 
				
			||||||
 | 
					    def has_permission(self, r, view):
 | 
				
			||||||
 | 
					        if r.method == "GET":
 | 
				
			||||||
 | 
					            return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return _has_perm(r, "can_manage_roles")
 | 
				
			||||||
@@ -1,17 +1,28 @@
 | 
				
			|||||||
import pyotp
 | 
					import pyotp
 | 
				
			||||||
 | 
					from rest_framework.serializers import ModelSerializer, SerializerMethodField
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from rest_framework.serializers import (
 | 
					from .models import User, Role
 | 
				
			||||||
    ModelSerializer,
 | 
					 | 
				
			||||||
    SerializerMethodField,
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .models import User
 | 
					
 | 
				
			||||||
 | 
					class UserUISerializer(ModelSerializer):
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        model = User
 | 
				
			||||||
 | 
					        fields = [
 | 
				
			||||||
 | 
					            "dark_mode",
 | 
				
			||||||
 | 
					            "show_community_scripts",
 | 
				
			||||||
 | 
					            "agent_dblclick_action",
 | 
				
			||||||
 | 
					            "url_action",
 | 
				
			||||||
 | 
					            "default_agent_tbl_tab",
 | 
				
			||||||
 | 
					            "client_tree_sort",
 | 
				
			||||||
 | 
					            "client_tree_splitter",
 | 
				
			||||||
 | 
					            "loading_bar_color",
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UserSerializer(ModelSerializer):
 | 
					class UserSerializer(ModelSerializer):
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = User
 | 
					        model = User
 | 
				
			||||||
        fields = (
 | 
					        fields = [
 | 
				
			||||||
            "id",
 | 
					            "id",
 | 
				
			||||||
            "username",
 | 
					            "username",
 | 
				
			||||||
            "first_name",
 | 
					            "first_name",
 | 
				
			||||||
@@ -19,7 +30,8 @@ class UserSerializer(ModelSerializer):
 | 
				
			|||||||
            "email",
 | 
					            "email",
 | 
				
			||||||
            "is_active",
 | 
					            "is_active",
 | 
				
			||||||
            "last_login",
 | 
					            "last_login",
 | 
				
			||||||
        )
 | 
					            "role",
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TOTPSetupSerializer(ModelSerializer):
 | 
					class TOTPSetupSerializer(ModelSerializer):
 | 
				
			||||||
@@ -38,3 +50,9 @@ class TOTPSetupSerializer(ModelSerializer):
 | 
				
			|||||||
        return pyotp.totp.TOTP(obj.totp_key).provisioning_uri(
 | 
					        return pyotp.totp.TOTP(obj.totp_key).provisioning_uri(
 | 
				
			||||||
            obj.username, issuer_name="Tactical RMM"
 | 
					            obj.username, issuer_name="Tactical RMM"
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class RoleSerializer(ModelSerializer):
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        model = Role
 | 
				
			||||||
 | 
					        fields = "__all__"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,8 +1,9 @@
 | 
				
			|||||||
from unittest.mock import patch
 | 
					from unittest.mock import patch
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.test import override_settings
 | 
					from django.test import override_settings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from tacticalrmm.test import TacticalTestCase
 | 
					 | 
				
			||||||
from accounts.models import User
 | 
					from accounts.models import User
 | 
				
			||||||
 | 
					from tacticalrmm.test import TacticalTestCase
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TestAccounts(TacticalTestCase):
 | 
					class TestAccounts(TacticalTestCase):
 | 
				
			||||||
@@ -155,6 +156,33 @@ class GetUpdateDeleteUser(TacticalTestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        self.check_not_authenticated("put", url)
 | 
					        self.check_not_authenticated("put", url)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @override_settings(ROOT_USER="john")
 | 
				
			||||||
 | 
					    def test_put_root_user(self):
 | 
				
			||||||
 | 
					        url = f"/accounts/{self.john.pk}/users/"
 | 
				
			||||||
 | 
					        data = {
 | 
				
			||||||
 | 
					            "id": self.john.pk,
 | 
				
			||||||
 | 
					            "username": "john",
 | 
				
			||||||
 | 
					            "email": "johndoe@xlawgaming.com",
 | 
				
			||||||
 | 
					            "first_name": "John",
 | 
				
			||||||
 | 
					            "last_name": "Doe",
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        r = self.client.put(url, data, format="json")
 | 
				
			||||||
 | 
					        self.assertEqual(r.status_code, 200)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @override_settings(ROOT_USER="john")
 | 
				
			||||||
 | 
					    def test_put_not_root_user(self):
 | 
				
			||||||
 | 
					        url = f"/accounts/{self.john.pk}/users/"
 | 
				
			||||||
 | 
					        data = {
 | 
				
			||||||
 | 
					            "id": self.john.pk,
 | 
				
			||||||
 | 
					            "username": "john",
 | 
				
			||||||
 | 
					            "email": "johndoe@xlawgaming.com",
 | 
				
			||||||
 | 
					            "first_name": "John",
 | 
				
			||||||
 | 
					            "last_name": "Doe",
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        self.client.force_authenticate(user=self.alice)
 | 
				
			||||||
 | 
					        r = self.client.put(url, data, format="json")
 | 
				
			||||||
 | 
					        self.assertEqual(r.status_code, 400)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_delete(self):
 | 
					    def test_delete(self):
 | 
				
			||||||
        url = f"/accounts/{self.john.pk}/users/"
 | 
					        url = f"/accounts/{self.john.pk}/users/"
 | 
				
			||||||
        r = self.client.delete(url)
 | 
					        r = self.client.delete(url)
 | 
				
			||||||
@@ -166,6 +194,19 @@ class GetUpdateDeleteUser(TacticalTestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        self.check_not_authenticated("delete", url)
 | 
					        self.check_not_authenticated("delete", url)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @override_settings(ROOT_USER="john")
 | 
				
			||||||
 | 
					    def test_delete_root_user(self):
 | 
				
			||||||
 | 
					        url = f"/accounts/{self.john.pk}/users/"
 | 
				
			||||||
 | 
					        r = self.client.delete(url)
 | 
				
			||||||
 | 
					        self.assertEqual(r.status_code, 200)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @override_settings(ROOT_USER="john")
 | 
				
			||||||
 | 
					    def test_delete_non_root_user(self):
 | 
				
			||||||
 | 
					        url = f"/accounts/{self.john.pk}/users/"
 | 
				
			||||||
 | 
					        self.client.force_authenticate(user=self.alice)
 | 
				
			||||||
 | 
					        r = self.client.delete(url)
 | 
				
			||||||
 | 
					        self.assertEqual(r.status_code, 400)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TestUserAction(TacticalTestCase):
 | 
					class TestUserAction(TacticalTestCase):
 | 
				
			||||||
    def setUp(self):
 | 
					    def setUp(self):
 | 
				
			||||||
@@ -184,6 +225,21 @@ class TestUserAction(TacticalTestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        self.check_not_authenticated("post", url)
 | 
					        self.check_not_authenticated("post", url)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @override_settings(ROOT_USER="john")
 | 
				
			||||||
 | 
					    def test_post_root_user(self):
 | 
				
			||||||
 | 
					        url = "/accounts/users/reset/"
 | 
				
			||||||
 | 
					        data = {"id": self.john.pk, "password": "3ASDjh2345kJA!@#)#@__123"}
 | 
				
			||||||
 | 
					        r = self.client.post(url, data, format="json")
 | 
				
			||||||
 | 
					        self.assertEqual(r.status_code, 200)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @override_settings(ROOT_USER="john")
 | 
				
			||||||
 | 
					    def test_post_non_root_user(self):
 | 
				
			||||||
 | 
					        url = "/accounts/users/reset/"
 | 
				
			||||||
 | 
					        data = {"id": self.john.pk, "password": "3ASDjh2345kJA!@#)#@__123"}
 | 
				
			||||||
 | 
					        self.client.force_authenticate(user=self.alice)
 | 
				
			||||||
 | 
					        r = self.client.post(url, data, format="json")
 | 
				
			||||||
 | 
					        self.assertEqual(r.status_code, 400)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_put(self):
 | 
					    def test_put(self):
 | 
				
			||||||
        url = "/accounts/users/reset/"
 | 
					        url = "/accounts/users/reset/"
 | 
				
			||||||
        data = {"id": self.john.pk}
 | 
					        data = {"id": self.john.pk}
 | 
				
			||||||
@@ -195,9 +251,36 @@ class TestUserAction(TacticalTestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        self.check_not_authenticated("put", url)
 | 
					        self.check_not_authenticated("put", url)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_darkmode(self):
 | 
					    @override_settings(ROOT_USER="john")
 | 
				
			||||||
 | 
					    def test_put_root_user(self):
 | 
				
			||||||
 | 
					        url = "/accounts/users/reset/"
 | 
				
			||||||
 | 
					        data = {"id": self.john.pk}
 | 
				
			||||||
 | 
					        r = self.client.put(url, data, format="json")
 | 
				
			||||||
 | 
					        self.assertEqual(r.status_code, 200)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        user = User.objects.get(pk=self.john.pk)
 | 
				
			||||||
 | 
					        self.assertEqual(user.totp_key, "")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @override_settings(ROOT_USER="john")
 | 
				
			||||||
 | 
					    def test_put_non_root_user(self):
 | 
				
			||||||
 | 
					        url = "/accounts/users/reset/"
 | 
				
			||||||
 | 
					        data = {"id": self.john.pk}
 | 
				
			||||||
 | 
					        self.client.force_authenticate(user=self.alice)
 | 
				
			||||||
 | 
					        r = self.client.put(url, data, format="json")
 | 
				
			||||||
 | 
					        self.assertEqual(r.status_code, 400)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_user_ui(self):
 | 
				
			||||||
        url = "/accounts/users/ui/"
 | 
					        url = "/accounts/users/ui/"
 | 
				
			||||||
        data = {"dark_mode": False}
 | 
					
 | 
				
			||||||
 | 
					        data = {
 | 
				
			||||||
 | 
					            "dark_mode": True,
 | 
				
			||||||
 | 
					            "show_community_scripts": True,
 | 
				
			||||||
 | 
					            "agent_dblclick_action": "editagent",
 | 
				
			||||||
 | 
					            "default_agent_tbl_tab": "mixed",
 | 
				
			||||||
 | 
					            "client_tree_sort": "alpha",
 | 
				
			||||||
 | 
					            "client_tree_splitter": 14,
 | 
				
			||||||
 | 
					            "loading_bar_color": "green",
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        r = self.client.patch(url, data, format="json")
 | 
					        r = self.client.patch(url, data, format="json")
 | 
				
			||||||
        self.assertEqual(r.status_code, 200)
 | 
					        self.assertEqual(r.status_code, 200)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,5 @@
 | 
				
			|||||||
from django.urls import path
 | 
					from django.urls import path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from . import views
 | 
					from . import views
 | 
				
			||||||
 | 
					
 | 
				
			||||||
urlpatterns = [
 | 
					urlpatterns = [
 | 
				
			||||||
@@ -8,4 +9,7 @@ urlpatterns = [
 | 
				
			|||||||
    path("users/reset_totp/", views.UserActions.as_view()),
 | 
					    path("users/reset_totp/", views.UserActions.as_view()),
 | 
				
			||||||
    path("users/setup_totp/", views.TOTPSetup.as_view()),
 | 
					    path("users/setup_totp/", views.TOTPSetup.as_view()),
 | 
				
			||||||
    path("users/ui/", views.UserUI.as_view()),
 | 
					    path("users/ui/", views.UserUI.as_view()),
 | 
				
			||||||
 | 
					    path("permslist/", views.PermsList.as_view()),
 | 
				
			||||||
 | 
					    path("roles/", views.GetAddRoles.as_view()),
 | 
				
			||||||
 | 
					    path("<int:pk>/role/", views.GetUpdateDeleteRole.as_view()),
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,23 +1,34 @@
 | 
				
			|||||||
import pyotp
 | 
					import pyotp
 | 
				
			||||||
 | 
					 | 
				
			||||||
from django.contrib.auth import login
 | 
					 | 
				
			||||||
from django.conf import settings
 | 
					from django.conf import settings
 | 
				
			||||||
from django.shortcuts import get_object_or_404
 | 
					from django.contrib.auth import login
 | 
				
			||||||
from django.db import IntegrityError
 | 
					from django.db import IntegrityError
 | 
				
			||||||
 | 
					from django.shortcuts import get_object_or_404
 | 
				
			||||||
from rest_framework.views import APIView
 | 
					 | 
				
			||||||
from rest_framework.authtoken.serializers import AuthTokenSerializer
 | 
					 | 
				
			||||||
from knox.views import LoginView as KnoxLoginView
 | 
					from knox.views import LoginView as KnoxLoginView
 | 
				
			||||||
from rest_framework.permissions import AllowAny
 | 
					 | 
				
			||||||
from rest_framework.response import Response
 | 
					 | 
				
			||||||
from rest_framework import status
 | 
					from rest_framework import status
 | 
				
			||||||
 | 
					from rest_framework.authtoken.serializers import AuthTokenSerializer
 | 
				
			||||||
 | 
					from rest_framework.permissions import AllowAny, IsAuthenticated
 | 
				
			||||||
 | 
					from rest_framework.response import Response
 | 
				
			||||||
 | 
					from rest_framework.views import APIView
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .models import User
 | 
					 | 
				
			||||||
from agents.models import Agent
 | 
					 | 
				
			||||||
from logs.models import AuditLog
 | 
					from logs.models import AuditLog
 | 
				
			||||||
from tacticalrmm.utils import notify_error
 | 
					from tacticalrmm.utils import notify_error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .serializers import UserSerializer, TOTPSetupSerializer
 | 
					from .models import User, Role
 | 
				
			||||||
 | 
					from .permissions import AccountsPerms, RolesPerms
 | 
				
			||||||
 | 
					from .serializers import (
 | 
				
			||||||
 | 
					    TOTPSetupSerializer,
 | 
				
			||||||
 | 
					    UserSerializer,
 | 
				
			||||||
 | 
					    UserUISerializer,
 | 
				
			||||||
 | 
					    RoleSerializer,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def _is_root_user(request, user) -> bool:
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        hasattr(settings, "ROOT_USER")
 | 
				
			||||||
 | 
					        and request.user != user
 | 
				
			||||||
 | 
					        and user.username == settings.ROOT_USER
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CheckCreds(KnoxLoginView):
 | 
					class CheckCreds(KnoxLoginView):
 | 
				
			||||||
@@ -60,7 +71,7 @@ class LoginView(KnoxLoginView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        if settings.DEBUG and token == "sekret":
 | 
					        if settings.DEBUG and token == "sekret":
 | 
				
			||||||
            valid = True
 | 
					            valid = True
 | 
				
			||||||
        elif totp.verify(token, valid_window=1):
 | 
					        elif totp.verify(token, valid_window=10):
 | 
				
			||||||
            valid = True
 | 
					            valid = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if valid:
 | 
					        if valid:
 | 
				
			||||||
@@ -73,6 +84,8 @@ class LoginView(KnoxLoginView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class GetAddUsers(APIView):
 | 
					class GetAddUsers(APIView):
 | 
				
			||||||
 | 
					    permission_classes = [IsAuthenticated, AccountsPerms]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get(self, request):
 | 
					    def get(self, request):
 | 
				
			||||||
        users = User.objects.filter(agent=None)
 | 
					        users = User.objects.filter(agent=None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -81,7 +94,7 @@ class GetAddUsers(APIView):
 | 
				
			|||||||
    def post(self, request):
 | 
					    def post(self, request):
 | 
				
			||||||
        # add new user
 | 
					        # add new user
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            user = User.objects.create_user(
 | 
					            user = User.objects.create_user(  # type: ignore
 | 
				
			||||||
                request.data["username"],
 | 
					                request.data["username"],
 | 
				
			||||||
                request.data["email"],
 | 
					                request.data["email"],
 | 
				
			||||||
                request.data["password"],
 | 
					                request.data["password"],
 | 
				
			||||||
@@ -93,13 +106,17 @@ class GetAddUsers(APIView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        user.first_name = request.data["first_name"]
 | 
					        user.first_name = request.data["first_name"]
 | 
				
			||||||
        user.last_name = request.data["last_name"]
 | 
					        user.last_name = request.data["last_name"]
 | 
				
			||||||
        # Can be changed once permissions and groups are introduced
 | 
					        if "role" in request.data.keys() and isinstance(request.data["role"], int):
 | 
				
			||||||
        user.is_superuser = True
 | 
					            role = get_object_or_404(Role, pk=request.data["role"])
 | 
				
			||||||
 | 
					            user.role = role
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        user.save()
 | 
					        user.save()
 | 
				
			||||||
        return Response(user.username)
 | 
					        return Response(user.username)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class GetUpdateDeleteUser(APIView):
 | 
					class GetUpdateDeleteUser(APIView):
 | 
				
			||||||
 | 
					    permission_classes = [IsAuthenticated, AccountsPerms]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get(self, request, pk):
 | 
					    def get(self, request, pk):
 | 
				
			||||||
        user = get_object_or_404(User, pk=pk)
 | 
					        user = get_object_or_404(User, pk=pk)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -108,6 +125,9 @@ class GetUpdateDeleteUser(APIView):
 | 
				
			|||||||
    def put(self, request, pk):
 | 
					    def put(self, request, pk):
 | 
				
			||||||
        user = get_object_or_404(User, pk=pk)
 | 
					        user = get_object_or_404(User, pk=pk)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if _is_root_user(request, user):
 | 
				
			||||||
 | 
					            return notify_error("The root user cannot be modified from the UI")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        serializer = UserSerializer(instance=user, data=request.data, partial=True)
 | 
					        serializer = UserSerializer(instance=user, data=request.data, partial=True)
 | 
				
			||||||
        serializer.is_valid(raise_exception=True)
 | 
					        serializer.is_valid(raise_exception=True)
 | 
				
			||||||
        serializer.save()
 | 
					        serializer.save()
 | 
				
			||||||
@@ -115,17 +135,23 @@ class GetUpdateDeleteUser(APIView):
 | 
				
			|||||||
        return Response("ok")
 | 
					        return Response("ok")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def delete(self, request, pk):
 | 
					    def delete(self, request, pk):
 | 
				
			||||||
        get_object_or_404(User, pk=pk).delete()
 | 
					        user = get_object_or_404(User, pk=pk)
 | 
				
			||||||
 | 
					        if _is_root_user(request, user):
 | 
				
			||||||
 | 
					            return notify_error("The root user cannot be deleted from the UI")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        user.delete()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return Response("ok")
 | 
					        return Response("ok")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UserActions(APIView):
 | 
					class UserActions(APIView):
 | 
				
			||||||
 | 
					    permission_classes = [IsAuthenticated, AccountsPerms]
 | 
				
			||||||
    # reset password
 | 
					    # reset password
 | 
				
			||||||
    def post(self, request):
 | 
					    def post(self, request):
 | 
				
			||||||
 | 
					 | 
				
			||||||
        user = get_object_or_404(User, pk=request.data["id"])
 | 
					        user = get_object_or_404(User, pk=request.data["id"])
 | 
				
			||||||
 | 
					        if _is_root_user(request, user):
 | 
				
			||||||
 | 
					            return notify_error("The root user cannot be modified from the UI")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        user.set_password(request.data["password"])
 | 
					        user.set_password(request.data["password"])
 | 
				
			||||||
        user.save()
 | 
					        user.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -133,8 +159,10 @@ class UserActions(APIView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    # reset two factor token
 | 
					    # reset two factor token
 | 
				
			||||||
    def put(self, request):
 | 
					    def put(self, request):
 | 
				
			||||||
 | 
					 | 
				
			||||||
        user = get_object_or_404(User, pk=request.data["id"])
 | 
					        user = get_object_or_404(User, pk=request.data["id"])
 | 
				
			||||||
 | 
					        if _is_root_user(request, user):
 | 
				
			||||||
 | 
					            return notify_error("The root user cannot be modified from the UI")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        user.totp_key = ""
 | 
					        user.totp_key = ""
 | 
				
			||||||
        user.save()
 | 
					        user.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -160,7 +188,48 @@ class TOTPSetup(APIView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class UserUI(APIView):
 | 
					class UserUI(APIView):
 | 
				
			||||||
    def patch(self, request):
 | 
					    def patch(self, request):
 | 
				
			||||||
        user = request.user
 | 
					        serializer = UserUISerializer(
 | 
				
			||||||
        user.dark_mode = request.data["dark_mode"]
 | 
					            instance=request.user, data=request.data, partial=True
 | 
				
			||||||
        user.save(update_fields=["dark_mode"])
 | 
					        )
 | 
				
			||||||
        return Response("ok")
 | 
					        serializer.is_valid(raise_exception=True)
 | 
				
			||||||
 | 
					        serializer.save()
 | 
				
			||||||
 | 
					        return Response("ok")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PermsList(APIView):
 | 
				
			||||||
 | 
					    def get(self, request):
 | 
				
			||||||
 | 
					        return Response(Role.perms())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class GetAddRoles(APIView):
 | 
				
			||||||
 | 
					    permission_classes = [IsAuthenticated, RolesPerms]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get(self, request):
 | 
				
			||||||
 | 
					        roles = Role.objects.all()
 | 
				
			||||||
 | 
					        return Response(RoleSerializer(roles, many=True).data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def post(self, request):
 | 
				
			||||||
 | 
					        serializer = RoleSerializer(data=request.data)
 | 
				
			||||||
 | 
					        serializer.is_valid(raise_exception=True)
 | 
				
			||||||
 | 
					        serializer.save()
 | 
				
			||||||
 | 
					        return Response("ok")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class GetUpdateDeleteRole(APIView):
 | 
				
			||||||
 | 
					    permission_classes = [IsAuthenticated, RolesPerms]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get(self, request, pk):
 | 
				
			||||||
 | 
					        role = get_object_or_404(Role, pk=pk)
 | 
				
			||||||
 | 
					        return Response(RoleSerializer(role).data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def put(self, request, pk):
 | 
				
			||||||
 | 
					        role = get_object_or_404(Role, pk=pk)
 | 
				
			||||||
 | 
					        serializer = RoleSerializer(instance=role, data=request.data)
 | 
				
			||||||
 | 
					        serializer.is_valid(raise_exception=True)
 | 
				
			||||||
 | 
					        serializer.save()
 | 
				
			||||||
 | 
					        return Response("ok")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def delete(self, request, pk):
 | 
				
			||||||
 | 
					        role = get_object_or_404(Role, pk=pk)
 | 
				
			||||||
 | 
					        role.delete()
 | 
				
			||||||
 | 
					        return Response("ok")
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,8 +1,8 @@
 | 
				
			|||||||
from django.contrib import admin
 | 
					from django.contrib import admin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .models import Agent, AgentOutage, RecoveryAction, Note
 | 
					from .models import Agent, AgentCustomField, Note, RecoveryAction
 | 
				
			||||||
 | 
					
 | 
				
			||||||
admin.site.register(Agent)
 | 
					admin.site.register(Agent)
 | 
				
			||||||
admin.site.register(AgentOutage)
 | 
					 | 
				
			||||||
admin.site.register(RecoveryAction)
 | 
					admin.site.register(RecoveryAction)
 | 
				
			||||||
admin.site.register(Note)
 | 
					admin.site.register(Note)
 | 
				
			||||||
 | 
					admin.site.register(AgentCustomField)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,14 +1,12 @@
 | 
				
			|||||||
 | 
					import json
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
import random
 | 
					import random
 | 
				
			||||||
import string
 | 
					import string
 | 
				
			||||||
import os
 | 
					 | 
				
			||||||
import json
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from model_bakery.recipe import Recipe, seq
 | 
					 | 
				
			||||||
from itertools import cycle
 | 
					from itertools import cycle
 | 
				
			||||||
from django.utils import timezone as djangotime
 | 
					 | 
				
			||||||
from django.conf import settings
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .models import Agent
 | 
					from django.conf import settings
 | 
				
			||||||
 | 
					from django.utils import timezone as djangotime
 | 
				
			||||||
 | 
					from model_bakery.recipe import Recipe, foreign_key, seq
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def generate_agent_id(hostname):
 | 
					def generate_agent_id(hostname):
 | 
				
			||||||
@@ -16,6 +14,9 @@ def generate_agent_id(hostname):
 | 
				
			|||||||
    return f"{rand}-{hostname}"
 | 
					    return f"{rand}-{hostname}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					site = Recipe("clients.Site")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_wmi_data():
 | 
					def get_wmi_data():
 | 
				
			||||||
    with open(
 | 
					    with open(
 | 
				
			||||||
        os.path.join(settings.BASE_DIR, "tacticalrmm/test_data/wmi_python_agent.json")
 | 
					        os.path.join(settings.BASE_DIR, "tacticalrmm/test_data/wmi_python_agent.json")
 | 
				
			||||||
@@ -24,12 +25,12 @@ def get_wmi_data():
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
agent = Recipe(
 | 
					agent = Recipe(
 | 
				
			||||||
    Agent,
 | 
					    "agents.Agent",
 | 
				
			||||||
 | 
					    site=foreign_key(site),
 | 
				
			||||||
    hostname="DESKTOP-TEST123",
 | 
					    hostname="DESKTOP-TEST123",
 | 
				
			||||||
    version="1.1.1",
 | 
					    version="1.3.0",
 | 
				
			||||||
    monitoring_type=cycle(["workstation", "server"]),
 | 
					    monitoring_type=cycle(["workstation", "server"]),
 | 
				
			||||||
    salt_id=generate_agent_id("DESKTOP-TEST123"),
 | 
					    agent_id=seq("asdkj3h4234-1234hg3h4g34-234jjh34|DESKTOP-TEST123"),
 | 
				
			||||||
    agent_id="71AHC-AA813-HH1BC-AAHH5-00013|DESKTOP-TEST123",
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
server_agent = agent.extend(
 | 
					server_agent = agent.extend(
 | 
				
			||||||
@@ -42,8 +43,12 @@ workstation_agent = agent.extend(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
online_agent = agent.extend(last_seen=djangotime.now())
 | 
					online_agent = agent.extend(last_seen=djangotime.now())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					offline_agent = agent.extend(
 | 
				
			||||||
 | 
					    last_seen=djangotime.now() - djangotime.timedelta(minutes=7)
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
overdue_agent = agent.extend(
 | 
					overdue_agent = agent.extend(
 | 
				
			||||||
    last_seen=djangotime.now() - djangotime.timedelta(minutes=6)
 | 
					    last_seen=djangotime.now() - djangotime.timedelta(minutes=35)
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
agent_with_services = agent.extend(
 | 
					agent_with_services = agent.extend(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,93 @@
 | 
				
			|||||||
 | 
					from django.core.management.base import BaseCommand
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from agents.models import Agent
 | 
				
			||||||
 | 
					from clients.models import Client, Site
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Command(BaseCommand):
 | 
				
			||||||
 | 
					    help = "Bulk update agent offline/overdue time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def add_arguments(self, parser):
 | 
				
			||||||
 | 
					        parser.add_argument("time", type=int, help="Time in minutes")
 | 
				
			||||||
 | 
					        parser.add_argument(
 | 
				
			||||||
 | 
					            "--client",
 | 
				
			||||||
 | 
					            type=str,
 | 
				
			||||||
 | 
					            help="Client Name",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        parser.add_argument(
 | 
				
			||||||
 | 
					            "--site",
 | 
				
			||||||
 | 
					            type=str,
 | 
				
			||||||
 | 
					            help="Site Name",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        parser.add_argument(
 | 
				
			||||||
 | 
					            "--offline",
 | 
				
			||||||
 | 
					            action="store_true",
 | 
				
			||||||
 | 
					            help="Offline",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        parser.add_argument(
 | 
				
			||||||
 | 
					            "--overdue",
 | 
				
			||||||
 | 
					            action="store_true",
 | 
				
			||||||
 | 
					            help="Overdue",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        parser.add_argument(
 | 
				
			||||||
 | 
					            "--all",
 | 
				
			||||||
 | 
					            action="store_true",
 | 
				
			||||||
 | 
					            help="All agents",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def handle(self, *args, **kwargs):
 | 
				
			||||||
 | 
					        time = kwargs["time"]
 | 
				
			||||||
 | 
					        client_name = kwargs["client"]
 | 
				
			||||||
 | 
					        site_name = kwargs["site"]
 | 
				
			||||||
 | 
					        all_agents = kwargs["all"]
 | 
				
			||||||
 | 
					        offline = kwargs["offline"]
 | 
				
			||||||
 | 
					        overdue = kwargs["overdue"]
 | 
				
			||||||
 | 
					        agents = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if offline and time < 2:
 | 
				
			||||||
 | 
					            self.stdout.write(self.style.ERROR("Minimum offline time is 2 minutes"))
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if overdue and time < 3:
 | 
				
			||||||
 | 
					            self.stdout.write(self.style.ERROR("Minimum overdue time is 3 minutes"))
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if client_name:
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                client = Client.objects.get(name=client_name)
 | 
				
			||||||
 | 
					            except Client.DoesNotExist:
 | 
				
			||||||
 | 
					                self.stdout.write(
 | 
				
			||||||
 | 
					                    self.style.ERROR(f"Client {client_name} doesn't exist")
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            agents = Agent.objects.filter(site__client=client)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        elif site_name:
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                site = Site.objects.get(name=site_name)
 | 
				
			||||||
 | 
					            except Site.DoesNotExist:
 | 
				
			||||||
 | 
					                self.stdout.write(self.style.ERROR(f"Site {site_name} doesn't exist"))
 | 
				
			||||||
 | 
					                return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            agents = Agent.objects.filter(site=site)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        elif all_agents:
 | 
				
			||||||
 | 
					            agents = Agent.objects.all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if agents:
 | 
				
			||||||
 | 
					            if offline:
 | 
				
			||||||
 | 
					                agents.update(offline_time=time)
 | 
				
			||||||
 | 
					                self.stdout.write(
 | 
				
			||||||
 | 
					                    self.style.SUCCESS(
 | 
				
			||||||
 | 
					                        f"Changed offline time on {len(agents)} agents to {time} minutes"
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if overdue:
 | 
				
			||||||
 | 
					                agents.update(overdue_time=time)
 | 
				
			||||||
 | 
					                self.stdout.write(
 | 
				
			||||||
 | 
					                    self.style.SUCCESS(
 | 
				
			||||||
 | 
					                        f"Changed overdue time on {len(agents)} agents to {time} minutes"
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					from django.conf import settings
 | 
				
			||||||
 | 
					from django.core.management.base import BaseCommand
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from agents.models import Agent
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Command(BaseCommand):
 | 
				
			||||||
 | 
					    help = "Shows online agents that are not on the latest version"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def handle(self, *args, **kwargs):
 | 
				
			||||||
 | 
					        q = Agent.objects.exclude(version=settings.LATEST_AGENT_VER).only(
 | 
				
			||||||
 | 
					            "pk", "version", "last_seen", "overdue_time", "offline_time"
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        agents = [i for i in q if i.status == "online"]
 | 
				
			||||||
 | 
					        for agent in agents:
 | 
				
			||||||
 | 
					            self.stdout.write(
 | 
				
			||||||
 | 
					                self.style.SUCCESS(f"{agent.hostname} - v{agent.version}")
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
@@ -1,8 +1,8 @@
 | 
				
			|||||||
# Generated by Django 3.0.6 on 2020-05-31 01:23
 | 
					# Generated by Django 3.0.6 on 2020-05-31 01:23
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import django.contrib.postgres.fields.jsonb
 | 
					import django.contrib.postgres.fields.jsonb
 | 
				
			||||||
from django.db import migrations, models
 | 
					 | 
				
			||||||
import django.db.models.deletion
 | 
					import django.db.models.deletion
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Migration(migrations.Migration):
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
# Generated by Django 3.0.7 on 2020-06-09 16:07
 | 
					# Generated by Django 3.0.7 on 2020-06-09 16:07
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.db import migrations, models
 | 
					 | 
				
			||||||
import django.db.models.deletion
 | 
					import django.db.models.deletion
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Migration(migrations.Migration):
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
# Generated by Django 3.0.8 on 2020-08-09 05:31
 | 
					# Generated by Django 3.0.8 on 2020-08-09 05:31
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.db import migrations, models
 | 
					 | 
				
			||||||
import django.db.models.deletion
 | 
					import django.db.models.deletion
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Migration(migrations.Migration):
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,8 +1,8 @@
 | 
				
			|||||||
# Generated by Django 3.1.1 on 2020-09-22 20:57
 | 
					# Generated by Django 3.1.1 on 2020-09-22 20:57
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import django.db.models.deletion
 | 
				
			||||||
from django.conf import settings
 | 
					from django.conf import settings
 | 
				
			||||||
from django.db import migrations, models
 | 
					from django.db import migrations, models
 | 
				
			||||||
import django.db.models.deletion
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Migration(migrations.Migration):
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,20 +1,26 @@
 | 
				
			|||||||
# Generated by Django 3.1.2 on 2020-11-01 22:53
 | 
					# Generated by Django 3.1.2 on 2020-11-01 22:53
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.db import migrations, models
 | 
					 | 
				
			||||||
import django.db.models.deletion
 | 
					import django.db.models.deletion
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Migration(migrations.Migration):
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    dependencies = [
 | 
					    dependencies = [
 | 
				
			||||||
        ('clients', '0006_deployment'),
 | 
					        ("clients", "0006_deployment"),
 | 
				
			||||||
        ('agents', '0020_auto_20201025_2129'),
 | 
					        ("agents", "0020_auto_20201025_2129"),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    operations = [
 | 
					    operations = [
 | 
				
			||||||
        migrations.AddField(
 | 
					        migrations.AddField(
 | 
				
			||||||
            model_name='agent',
 | 
					            model_name="agent",
 | 
				
			||||||
            name='site_link',
 | 
					            name="site_link",
 | 
				
			||||||
            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='agents', to='clients.site'),
 | 
					            field=models.ForeignKey(
 | 
				
			||||||
 | 
					                blank=True,
 | 
				
			||||||
 | 
					                null=True,
 | 
				
			||||||
 | 
					                on_delete=django.db.models.deletion.SET_NULL,
 | 
				
			||||||
 | 
					                related_name="agents",
 | 
				
			||||||
 | 
					                to="clients.site",
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,16 +6,16 @@ from django.db import migrations
 | 
				
			|||||||
class Migration(migrations.Migration):
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    dependencies = [
 | 
					    dependencies = [
 | 
				
			||||||
        ('agents', '0022_update_site_primary_key'),
 | 
					        ("agents", "0022_update_site_primary_key"),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    operations = [
 | 
					    operations = [
 | 
				
			||||||
        migrations.RemoveField(
 | 
					        migrations.RemoveField(
 | 
				
			||||||
            model_name='agent',
 | 
					            model_name="agent",
 | 
				
			||||||
            name='client',
 | 
					            name="client",
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        migrations.RemoveField(
 | 
					        migrations.RemoveField(
 | 
				
			||||||
            model_name='agent',
 | 
					            model_name="agent",
 | 
				
			||||||
            name='site',
 | 
					            name="site",
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,13 +6,13 @@ from django.db import migrations
 | 
				
			|||||||
class Migration(migrations.Migration):
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    dependencies = [
 | 
					    dependencies = [
 | 
				
			||||||
        ('agents', '0023_auto_20201101_2312'),
 | 
					        ("agents", "0023_auto_20201101_2312"),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    operations = [
 | 
					    operations = [
 | 
				
			||||||
        migrations.RenameField(
 | 
					        migrations.RenameField(
 | 
				
			||||||
            model_name='agent',
 | 
					            model_name="agent",
 | 
				
			||||||
            old_name='site_link',
 | 
					            old_name="site_link",
 | 
				
			||||||
            new_name='site',
 | 
					            new_name="site",
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,13 +6,22 @@ from django.db import migrations, models
 | 
				
			|||||||
class Migration(migrations.Migration):
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    dependencies = [
 | 
					    dependencies = [
 | 
				
			||||||
        ('agents', '0024_auto_20201101_2319'),
 | 
					        ("agents", "0024_auto_20201101_2319"),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    operations = [
 | 
					    operations = [
 | 
				
			||||||
        migrations.AlterField(
 | 
					        migrations.AlterField(
 | 
				
			||||||
            model_name='recoveryaction',
 | 
					            model_name="recoveryaction",
 | 
				
			||||||
            name='mode',
 | 
					            name="mode",
 | 
				
			||||||
            field=models.CharField(choices=[('salt', 'Salt'), ('mesh', 'Mesh'), ('command', 'Command'), ('rpc', 'Nats RPC')], default='mesh', max_length=50),
 | 
					            field=models.CharField(
 | 
				
			||||||
 | 
					                choices=[
 | 
				
			||||||
 | 
					                    ("salt", "Salt"),
 | 
				
			||||||
 | 
					                    ("mesh", "Mesh"),
 | 
				
			||||||
 | 
					                    ("command", "Command"),
 | 
				
			||||||
 | 
					                    ("rpc", "Nats RPC"),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					                default="mesh",
 | 
				
			||||||
 | 
					                max_length=50,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,13 +6,23 @@ from django.db import migrations, models
 | 
				
			|||||||
class Migration(migrations.Migration):
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    dependencies = [
 | 
					    dependencies = [
 | 
				
			||||||
        ('agents', '0025_auto_20201122_0407'),
 | 
					        ("agents", "0025_auto_20201122_0407"),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    operations = [
 | 
					    operations = [
 | 
				
			||||||
        migrations.AlterField(
 | 
					        migrations.AlterField(
 | 
				
			||||||
            model_name='recoveryaction',
 | 
					            model_name="recoveryaction",
 | 
				
			||||||
            name='mode',
 | 
					            name="mode",
 | 
				
			||||||
            field=models.CharField(choices=[('salt', 'Salt'), ('mesh', 'Mesh'), ('command', 'Command'), ('rpc', 'Nats RPC'), ('checkrunner', 'Checkrunner')], default='mesh', max_length=50),
 | 
					            field=models.CharField(
 | 
				
			||||||
 | 
					                choices=[
 | 
				
			||||||
 | 
					                    ("salt", "Salt"),
 | 
				
			||||||
 | 
					                    ("mesh", "Mesh"),
 | 
				
			||||||
 | 
					                    ("command", "Command"),
 | 
				
			||||||
 | 
					                    ("rpc", "Nats RPC"),
 | 
				
			||||||
 | 
					                    ("checkrunner", "Checkrunner"),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					                default="mesh",
 | 
				
			||||||
 | 
					                max_length=50,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.1.4 on 2021-01-29 21:11
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('agents', '0026_auto_20201125_2334'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='agent',
 | 
				
			||||||
 | 
					            name='overdue_dashboard_alert',
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
							
								
								
									
										23
									
								
								api/tacticalrmm/agents/migrations/0028_auto_20210206_1534.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								api/tacticalrmm/agents/migrations/0028_auto_20210206_1534.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.1.4 on 2021-02-06 15:34
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('agents', '0027_agent_overdue_dashboard_alert'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='agentoutage',
 | 
				
			||||||
 | 
					            name='outage_email_sent_time',
 | 
				
			||||||
 | 
					            field=models.DateTimeField(blank=True, null=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='agentoutage',
 | 
				
			||||||
 | 
					            name='outage_sms_sent_time',
 | 
				
			||||||
 | 
					            field=models.DateTimeField(blank=True, null=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
							
								
								
									
										16
									
								
								api/tacticalrmm/agents/migrations/0029_delete_agentoutage.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								api/tacticalrmm/agents/migrations/0029_delete_agentoutage.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.1.4 on 2021-02-10 21:56
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('agents', '0028_auto_20210206_1534'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.DeleteModel(
 | 
				
			||||||
 | 
					            name='AgentOutage',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
							
								
								
									
										18
									
								
								api/tacticalrmm/agents/migrations/0030_agent_offline_time.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								api/tacticalrmm/agents/migrations/0030_agent_offline_time.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.1.6 on 2021-02-16 08:50
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('agents', '0029_delete_agentoutage'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='agent',
 | 
				
			||||||
 | 
					            name='offline_time',
 | 
				
			||||||
 | 
					            field=models.PositiveIntegerField(default=4),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.1.7 on 2021-03-04 03:57
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import django.db.models.deletion
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('alerts', '0006_auto_20210217_1736'),
 | 
				
			||||||
 | 
					        ('agents', '0030_agent_offline_time'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='agent',
 | 
				
			||||||
 | 
					            name='alert_template',
 | 
				
			||||||
 | 
					            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='agents', to='alerts.alerttemplate'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
							
								
								
									
										24
									
								
								api/tacticalrmm/agents/migrations/0032_agentcustomfield.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								api/tacticalrmm/agents/migrations/0032_agentcustomfield.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.1.7 on 2021-03-17 14:45
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import django.db.models.deletion
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('core', '0014_customfield'),
 | 
				
			||||||
 | 
					        ('agents', '0031_agent_alert_template'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name='AgentCustomField',
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
				
			||||||
 | 
					                ('value', models.TextField(blank=True, null=True)),
 | 
				
			||||||
 | 
					                ('agent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='custom_fields', to='agents.agent')),
 | 
				
			||||||
 | 
					                ('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='agent_fields', to='core.customfield')),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.1.7 on 2021-03-29 02:51
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import django.contrib.postgres.fields
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('agents', '0032_agentcustomfield'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='agentcustomfield',
 | 
				
			||||||
 | 
					            name='multiple_value',
 | 
				
			||||||
 | 
					            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,18 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.1.7 on 2021-03-29 03:01
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('agents', '0033_agentcustomfield_multiple_value'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='agentcustomfield',
 | 
				
			||||||
 | 
					            name='checkbox_value',
 | 
				
			||||||
 | 
					            field=models.BooleanField(blank=True, default=False),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
							
								
								
									
										23
									
								
								api/tacticalrmm/agents/migrations/0035_auto_20210329_1709.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								api/tacticalrmm/agents/migrations/0035_auto_20210329_1709.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.1.7 on 2021-03-29 17:09
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('agents', '0034_agentcustomfield_checkbox_value'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.RenameField(
 | 
				
			||||||
 | 
					            model_name='agentcustomfield',
 | 
				
			||||||
 | 
					            old_name='checkbox_value',
 | 
				
			||||||
 | 
					            new_name='bool_value',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RenameField(
 | 
				
			||||||
 | 
					            model_name='agentcustomfield',
 | 
				
			||||||
 | 
					            old_name='value',
 | 
				
			||||||
 | 
					            new_name='string_value',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.1.7 on 2021-04-17 01:28
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('agents', '0035_auto_20210329_1709'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='agent',
 | 
				
			||||||
 | 
					            name='block_policy_inheritance',
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -1,25 +1,26 @@
 | 
				
			|||||||
import requests
 | 
					import asyncio
 | 
				
			||||||
import time
 | 
					 | 
				
			||||||
import base64
 | 
					import base64
 | 
				
			||||||
from Crypto.Cipher import AES
 | 
					 | 
				
			||||||
from Crypto.Random import get_random_bytes
 | 
					 | 
				
			||||||
from Crypto.Hash import SHA3_384
 | 
					 | 
				
			||||||
from Crypto.Util.Padding import pad
 | 
					 | 
				
			||||||
import validators
 | 
					 | 
				
			||||||
import msgpack
 | 
					 | 
				
			||||||
import re
 | 
					import re
 | 
				
			||||||
 | 
					import time
 | 
				
			||||||
from collections import Counter
 | 
					from collections import Counter
 | 
				
			||||||
from loguru import logger
 | 
					 | 
				
			||||||
from packaging import version as pyver
 | 
					 | 
				
			||||||
from distutils.version import LooseVersion
 | 
					from distutils.version import LooseVersion
 | 
				
			||||||
 | 
					from typing import Any
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import msgpack
 | 
				
			||||||
 | 
					import validators
 | 
				
			||||||
 | 
					from Crypto.Cipher import AES
 | 
				
			||||||
 | 
					from Crypto.Hash import SHA3_384
 | 
				
			||||||
 | 
					from Crypto.Random import get_random_bytes
 | 
				
			||||||
 | 
					from Crypto.Util.Padding import pad
 | 
				
			||||||
 | 
					from django.conf import settings
 | 
				
			||||||
 | 
					from django.contrib.postgres.fields import ArrayField
 | 
				
			||||||
 | 
					from django.db import models
 | 
				
			||||||
 | 
					from django.utils import timezone as djangotime
 | 
				
			||||||
 | 
					from loguru import logger
 | 
				
			||||||
from nats.aio.client import Client as NATS
 | 
					from nats.aio.client import Client as NATS
 | 
				
			||||||
from nats.aio.errors import ErrTimeout
 | 
					from nats.aio.errors import ErrTimeout
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.db import models
 | 
					from core.models import TZ_CHOICES, CoreSettings
 | 
				
			||||||
from django.conf import settings
 | 
					 | 
				
			||||||
from django.utils import timezone as djangotime
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from core.models import CoreSettings, TZ_CHOICES
 | 
					 | 
				
			||||||
from logs.models import BaseAuditModel
 | 
					from logs.models import BaseAuditModel
 | 
				
			||||||
 | 
					
 | 
				
			||||||
logger.configure(**settings.LOG_CONFIG)
 | 
					logger.configure(**settings.LOG_CONFIG)
 | 
				
			||||||
@@ -50,6 +51,8 @@ class Agent(BaseAuditModel):
 | 
				
			|||||||
    mesh_node_id = models.CharField(null=True, blank=True, max_length=255)
 | 
					    mesh_node_id = models.CharField(null=True, blank=True, max_length=255)
 | 
				
			||||||
    overdue_email_alert = models.BooleanField(default=False)
 | 
					    overdue_email_alert = models.BooleanField(default=False)
 | 
				
			||||||
    overdue_text_alert = models.BooleanField(default=False)
 | 
					    overdue_text_alert = models.BooleanField(default=False)
 | 
				
			||||||
 | 
					    overdue_dashboard_alert = models.BooleanField(default=False)
 | 
				
			||||||
 | 
					    offline_time = models.PositiveIntegerField(default=4)
 | 
				
			||||||
    overdue_time = models.PositiveIntegerField(default=30)
 | 
					    overdue_time = models.PositiveIntegerField(default=30)
 | 
				
			||||||
    check_interval = models.PositiveIntegerField(default=120)
 | 
					    check_interval = models.PositiveIntegerField(default=120)
 | 
				
			||||||
    needs_reboot = models.BooleanField(default=False)
 | 
					    needs_reboot = models.BooleanField(default=False)
 | 
				
			||||||
@@ -60,6 +63,14 @@ class Agent(BaseAuditModel):
 | 
				
			|||||||
        max_length=255, choices=TZ_CHOICES, null=True, blank=True
 | 
					        max_length=255, choices=TZ_CHOICES, null=True, blank=True
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    maintenance_mode = models.BooleanField(default=False)
 | 
					    maintenance_mode = models.BooleanField(default=False)
 | 
				
			||||||
 | 
					    block_policy_inheritance = models.BooleanField(default=False)
 | 
				
			||||||
 | 
					    alert_template = models.ForeignKey(
 | 
				
			||||||
 | 
					        "alerts.AlertTemplate",
 | 
				
			||||||
 | 
					        related_name="agents",
 | 
				
			||||||
 | 
					        null=True,
 | 
				
			||||||
 | 
					        blank=True,
 | 
				
			||||||
 | 
					        on_delete=models.SET_NULL,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
    site = models.ForeignKey(
 | 
					    site = models.ForeignKey(
 | 
				
			||||||
        "clients.Site",
 | 
					        "clients.Site",
 | 
				
			||||||
        related_name="agents",
 | 
					        related_name="agents",
 | 
				
			||||||
@@ -75,6 +86,24 @@ class Agent(BaseAuditModel):
 | 
				
			|||||||
        on_delete=models.SET_NULL,
 | 
					        on_delete=models.SET_NULL,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def save(self, *args, **kwargs):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # get old agent if exists
 | 
				
			||||||
 | 
					        old_agent = type(self).objects.get(pk=self.pk) if self.pk else None
 | 
				
			||||||
 | 
					        super(BaseAuditModel, self).save(*args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # check if new agent has been created
 | 
				
			||||||
 | 
					        # or check if policy have changed on agent
 | 
				
			||||||
 | 
					        # or if site has changed on agent and if so generate-policies
 | 
				
			||||||
 | 
					        if (
 | 
				
			||||||
 | 
					            not old_agent
 | 
				
			||||||
 | 
					            or (old_agent and old_agent.policy != self.policy)
 | 
				
			||||||
 | 
					            or (old_agent.site != self.site)
 | 
				
			||||||
 | 
					            or (old_agent.block_policy_inheritance != self.block_policy_inheritance)
 | 
				
			||||||
 | 
					        ):
 | 
				
			||||||
 | 
					            self.generate_checks_from_policies()
 | 
				
			||||||
 | 
					            self.generate_tasks_from_policies()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __str__(self):
 | 
					    def __str__(self):
 | 
				
			||||||
        return self.hostname
 | 
					        return self.hostname
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -82,14 +111,6 @@ class Agent(BaseAuditModel):
 | 
				
			|||||||
    def client(self):
 | 
					    def client(self):
 | 
				
			||||||
        return self.site.client
 | 
					        return self.site.client
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					 | 
				
			||||||
    def has_nats(self):
 | 
					 | 
				
			||||||
        return pyver.parse(self.version) >= pyver.parse("1.1.0")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @property
 | 
					 | 
				
			||||||
    def has_gotasks(self):
 | 
					 | 
				
			||||||
        return pyver.parse(self.version) >= pyver.parse("1.1.1")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def timezone(self):
 | 
					    def timezone(self):
 | 
				
			||||||
        # return the default timezone unless the timezone is explicity set per agent
 | 
					        # return the default timezone unless the timezone is explicity set per agent
 | 
				
			||||||
@@ -117,14 +138,6 @@ class Agent(BaseAuditModel):
 | 
				
			|||||||
            return settings.DL_32
 | 
					            return settings.DL_32
 | 
				
			||||||
        return None
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					 | 
				
			||||||
    def winsalt_dl(self):
 | 
					 | 
				
			||||||
        if self.arch == "64":
 | 
					 | 
				
			||||||
            return settings.SALT_64
 | 
					 | 
				
			||||||
        elif self.arch == "32":
 | 
					 | 
				
			||||||
            return settings.SALT_32
 | 
					 | 
				
			||||||
        return None
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def win_inno_exe(self):
 | 
					    def win_inno_exe(self):
 | 
				
			||||||
        if self.arch == "64":
 | 
					        if self.arch == "64":
 | 
				
			||||||
@@ -135,7 +148,7 @@ class Agent(BaseAuditModel):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def status(self):
 | 
					    def status(self):
 | 
				
			||||||
        offline = djangotime.now() - djangotime.timedelta(minutes=6)
 | 
					        offline = djangotime.now() - djangotime.timedelta(minutes=self.offline_time)
 | 
				
			||||||
        overdue = djangotime.now() - djangotime.timedelta(minutes=self.overdue_time)
 | 
					        overdue = djangotime.now() - djangotime.timedelta(minutes=self.overdue_time)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if self.last_seen is not None:
 | 
					        if self.last_seen is not None:
 | 
				
			||||||
@@ -150,25 +163,32 @@ class Agent(BaseAuditModel):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def has_patches_pending(self):
 | 
					    def has_patches_pending(self):
 | 
				
			||||||
        return self.winupdates.filter(action="approve").filter(installed=False).exists()
 | 
					        return self.winupdates.filter(action="approve").filter(installed=False).exists()  # type: ignore
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def checks(self):
 | 
					    def checks(self):
 | 
				
			||||||
        total, passing, failing = 0, 0, 0
 | 
					        total, passing, failing, warning, info = 0, 0, 0, 0, 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if self.agentchecks.exists():
 | 
					        if self.agentchecks.exists():  # type: ignore
 | 
				
			||||||
            for i in self.agentchecks.all():
 | 
					            for i in self.agentchecks.all():  # type: ignore
 | 
				
			||||||
                total += 1
 | 
					                total += 1
 | 
				
			||||||
                if i.status == "passing":
 | 
					                if i.status == "passing":
 | 
				
			||||||
                    passing += 1
 | 
					                    passing += 1
 | 
				
			||||||
                elif i.status == "failing":
 | 
					                elif i.status == "failing":
 | 
				
			||||||
                    failing += 1
 | 
					                    if i.alert_severity == "error":
 | 
				
			||||||
 | 
					                        failing += 1
 | 
				
			||||||
 | 
					                    elif i.alert_severity == "warning":
 | 
				
			||||||
 | 
					                        warning += 1
 | 
				
			||||||
 | 
					                    elif i.alert_severity == "info":
 | 
				
			||||||
 | 
					                        info += 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        ret = {
 | 
					        ret = {
 | 
				
			||||||
            "total": total,
 | 
					            "total": total,
 | 
				
			||||||
            "passing": passing,
 | 
					            "passing": passing,
 | 
				
			||||||
            "failing": failing,
 | 
					            "failing": failing,
 | 
				
			||||||
            "has_failing_checks": failing > 0,
 | 
					            "warning": warning,
 | 
				
			||||||
 | 
					            "info": info,
 | 
				
			||||||
 | 
					            "has_failing_checks": failing > 0 or warning > 0,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        return ret
 | 
					        return ret
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -183,6 +203,27 @@ class Agent(BaseAuditModel):
 | 
				
			|||||||
        except:
 | 
					        except:
 | 
				
			||||||
            return ["unknown cpu model"]
 | 
					            return ["unknown cpu model"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def graphics(self):
 | 
				
			||||||
 | 
					        ret, mrda = [], []
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            graphics = self.wmi_detail["graphics"]
 | 
				
			||||||
 | 
					            for i in graphics:
 | 
				
			||||||
 | 
					                caption = [x["Caption"] for x in i if "Caption" in x][0]
 | 
				
			||||||
 | 
					                if "microsoft remote display adapter" in caption.lower():
 | 
				
			||||||
 | 
					                    mrda.append("yes")
 | 
				
			||||||
 | 
					                    continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                ret.append([x["Caption"] for x in i if "Caption" in x][0])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # only return this if no other graphics cards
 | 
				
			||||||
 | 
					            if not ret and mrda:
 | 
				
			||||||
 | 
					                return "Microsoft Remote Display Adapter"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return ", ".join(ret)
 | 
				
			||||||
 | 
					        except:
 | 
				
			||||||
 | 
					            return "Graphics info requires agent v1.4.14"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def local_ips(self):
 | 
					    def local_ips(self):
 | 
				
			||||||
        ret = []
 | 
					        ret = []
 | 
				
			||||||
@@ -222,11 +263,17 @@ class Agent(BaseAuditModel):
 | 
				
			|||||||
                make = [x["Manufacturer"] for x in mobo if "Manufacturer" in x][0]
 | 
					                make = [x["Manufacturer"] for x in mobo if "Manufacturer" in x][0]
 | 
				
			||||||
                model = [x["Product"] for x in mobo if "Product" in x][0]
 | 
					                model = [x["Product"] for x in mobo if "Product" in x][0]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if make.lower() == "lenovo":
 | 
				
			||||||
 | 
					                sysfam = [x["SystemFamily"] for x in comp_sys if "SystemFamily" in x][0]
 | 
				
			||||||
 | 
					                if "to be filled" not in sysfam.lower():
 | 
				
			||||||
 | 
					                    model = sysfam
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return f"{make} {model}"
 | 
					            return f"{make} {model}"
 | 
				
			||||||
        except:
 | 
					        except:
 | 
				
			||||||
            pass
 | 
					            pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
 | 
					            comp_sys_prod = self.wmi_detail["comp_sys_prod"][0]
 | 
				
			||||||
            return [x["Version"] for x in comp_sys_prod if "Version" in x][0]
 | 
					            return [x["Version"] for x in comp_sys_prod if "Version" in x][0]
 | 
				
			||||||
        except:
 | 
					        except:
 | 
				
			||||||
            pass
 | 
					            pass
 | 
				
			||||||
@@ -256,33 +303,107 @@ class Agent(BaseAuditModel):
 | 
				
			|||||||
        except:
 | 
					        except:
 | 
				
			||||||
            return ["unknown disk"]
 | 
					            return ["unknown disk"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def check_run_interval(self) -> int:
 | 
				
			||||||
 | 
					        interval = self.check_interval
 | 
				
			||||||
 | 
					        # determine if any agent checks have a custom interval and set the lowest interval
 | 
				
			||||||
 | 
					        for check in self.agentchecks.filter(overriden_by_policy=False):  # type: ignore
 | 
				
			||||||
 | 
					            if check.run_interval and check.run_interval < interval:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                # don't allow check runs less than 15s
 | 
				
			||||||
 | 
					                if check.run_interval < 15:
 | 
				
			||||||
 | 
					                    interval = 15
 | 
				
			||||||
 | 
					                else:
 | 
				
			||||||
 | 
					                    interval = check.run_interval
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return interval
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def run_script(
 | 
				
			||||||
 | 
					        self,
 | 
				
			||||||
 | 
					        scriptpk: int,
 | 
				
			||||||
 | 
					        args: list[str] = [],
 | 
				
			||||||
 | 
					        timeout: int = 120,
 | 
				
			||||||
 | 
					        full: bool = False,
 | 
				
			||||||
 | 
					        wait: bool = False,
 | 
				
			||||||
 | 
					        run_on_any: bool = False,
 | 
				
			||||||
 | 
					    ) -> Any:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        from scripts.models import Script
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        script = Script.objects.get(pk=scriptpk)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        parsed_args = script.parse_script_args(self, script.shell, args)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        data = {
 | 
				
			||||||
 | 
					            "func": "runscriptfull" if full else "runscript",
 | 
				
			||||||
 | 
					            "timeout": timeout,
 | 
				
			||||||
 | 
					            "script_args": parsed_args,
 | 
				
			||||||
 | 
					            "payload": {
 | 
				
			||||||
 | 
					                "code": script.code,
 | 
				
			||||||
 | 
					                "shell": script.shell,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        running_agent = self
 | 
				
			||||||
 | 
					        if run_on_any:
 | 
				
			||||||
 | 
					            nats_ping = {"func": "ping"}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # try on self first
 | 
				
			||||||
 | 
					            r = asyncio.run(self.nats_cmd(nats_ping, timeout=1))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if r == "pong":
 | 
				
			||||||
 | 
					                running_agent = self
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                online = [
 | 
				
			||||||
 | 
					                    agent
 | 
				
			||||||
 | 
					                    for agent in Agent.objects.only(
 | 
				
			||||||
 | 
					                        "pk", "agent_id", "last_seen", "overdue_time", "offline_time"
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                    if agent.status == "online"
 | 
				
			||||||
 | 
					                ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                for agent in online:
 | 
				
			||||||
 | 
					                    r = asyncio.run(agent.nats_cmd(nats_ping, timeout=1))
 | 
				
			||||||
 | 
					                    if r == "pong":
 | 
				
			||||||
 | 
					                        running_agent = agent
 | 
				
			||||||
 | 
					                        break
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if running_agent.pk == self.pk:
 | 
				
			||||||
 | 
					                    return "Unable to find an online agent"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if wait:
 | 
				
			||||||
 | 
					            return asyncio.run(running_agent.nats_cmd(data, timeout=timeout, wait=True))
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            asyncio.run(running_agent.nats_cmd(data, wait=False))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return "ok"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # auto approves updates
 | 
					    # auto approves updates
 | 
				
			||||||
    def approve_updates(self):
 | 
					    def approve_updates(self):
 | 
				
			||||||
        patch_policy = self.get_patch_policy()
 | 
					        patch_policy = self.get_patch_policy()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        updates = list()
 | 
					        updates = list()
 | 
				
			||||||
        if patch_policy.critical == "approve":
 | 
					        if patch_policy.critical == "approve":
 | 
				
			||||||
            updates += self.winupdates.filter(
 | 
					            updates += self.winupdates.filter(  # type: ignore
 | 
				
			||||||
                severity="Critical", installed=False
 | 
					                severity="Critical", installed=False
 | 
				
			||||||
            ).exclude(action="approve")
 | 
					            ).exclude(action="approve")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if patch_policy.important == "approve":
 | 
					        if patch_policy.important == "approve":
 | 
				
			||||||
            updates += self.winupdates.filter(
 | 
					            updates += self.winupdates.filter(  # type: ignore
 | 
				
			||||||
                severity="Important", installed=False
 | 
					                severity="Important", installed=False
 | 
				
			||||||
            ).exclude(action="approve")
 | 
					            ).exclude(action="approve")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if patch_policy.moderate == "approve":
 | 
					        if patch_policy.moderate == "approve":
 | 
				
			||||||
            updates += self.winupdates.filter(
 | 
					            updates += self.winupdates.filter(  # type: ignore
 | 
				
			||||||
                severity="Moderate", installed=False
 | 
					                severity="Moderate", installed=False
 | 
				
			||||||
            ).exclude(action="approve")
 | 
					            ).exclude(action="approve")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if patch_policy.low == "approve":
 | 
					        if patch_policy.low == "approve":
 | 
				
			||||||
            updates += self.winupdates.filter(severity="Low", installed=False).exclude(
 | 
					            updates += self.winupdates.filter(severity="Low", installed=False).exclude(  # type: ignore
 | 
				
			||||||
                action="approve"
 | 
					                action="approve"
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if patch_policy.other == "approve":
 | 
					        if patch_policy.other == "approve":
 | 
				
			||||||
            updates += self.winupdates.filter(severity="", installed=False).exclude(
 | 
					            updates += self.winupdates.filter(severity="", installed=False).exclude(  # type: ignore
 | 
				
			||||||
                action="approve"
 | 
					                action="approve"
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -297,7 +418,7 @@ class Agent(BaseAuditModel):
 | 
				
			|||||||
        site = self.site
 | 
					        site = self.site
 | 
				
			||||||
        core_settings = CoreSettings.objects.first()
 | 
					        core_settings = CoreSettings.objects.first()
 | 
				
			||||||
        patch_policy = None
 | 
					        patch_policy = None
 | 
				
			||||||
        agent_policy = self.winupdatepolicy.get()
 | 
					        agent_policy = self.winupdatepolicy.get()  # type: ignore
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if self.monitoring_type == "server":
 | 
					        if self.monitoring_type == "server":
 | 
				
			||||||
            # check agent policy first which should override client or site policy
 | 
					            # check agent policy first which should override client or site policy
 | 
				
			||||||
@@ -306,21 +427,34 @@ class Agent(BaseAuditModel):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            # check site policy if agent policy doesn't have one
 | 
					            # check site policy if agent policy doesn't have one
 | 
				
			||||||
            elif site.server_policy and site.server_policy.winupdatepolicy.exists():
 | 
					            elif site.server_policy and site.server_policy.winupdatepolicy.exists():
 | 
				
			||||||
                patch_policy = site.server_policy.winupdatepolicy.get()
 | 
					                # make sure agent isn;t blocking policy inheritance
 | 
				
			||||||
 | 
					                if not self.block_policy_inheritance:
 | 
				
			||||||
 | 
					                    patch_policy = site.server_policy.winupdatepolicy.get()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # if site doesn't have a patch policy check the client
 | 
					            # if site doesn't have a patch policy check the client
 | 
				
			||||||
            elif (
 | 
					            elif (
 | 
				
			||||||
                site.client.server_policy
 | 
					                site.client.server_policy
 | 
				
			||||||
                and site.client.server_policy.winupdatepolicy.exists()
 | 
					                and site.client.server_policy.winupdatepolicy.exists()
 | 
				
			||||||
            ):
 | 
					            ):
 | 
				
			||||||
                patch_policy = site.client.server_policy.winupdatepolicy.get()
 | 
					                # make sure agent and site are not blocking inheritance
 | 
				
			||||||
 | 
					                if (
 | 
				
			||||||
 | 
					                    not self.block_policy_inheritance
 | 
				
			||||||
 | 
					                    and not site.block_policy_inheritance
 | 
				
			||||||
 | 
					                ):
 | 
				
			||||||
 | 
					                    patch_policy = site.client.server_policy.winupdatepolicy.get()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # if patch policy still doesn't exist check default policy
 | 
					            # if patch policy still doesn't exist check default policy
 | 
				
			||||||
            elif (
 | 
					            elif (
 | 
				
			||||||
                core_settings.server_policy
 | 
					                core_settings.server_policy
 | 
				
			||||||
                and core_settings.server_policy.winupdatepolicy.exists()
 | 
					                and core_settings.server_policy.winupdatepolicy.exists()
 | 
				
			||||||
            ):
 | 
					            ):
 | 
				
			||||||
                patch_policy = core_settings.server_policy.winupdatepolicy.get()
 | 
					                # make sure agent site and client are not blocking inheritance
 | 
				
			||||||
 | 
					                if (
 | 
				
			||||||
 | 
					                    not self.block_policy_inheritance
 | 
				
			||||||
 | 
					                    and not site.block_policy_inheritance
 | 
				
			||||||
 | 
					                    and not site.client.block_policy_inheritance
 | 
				
			||||||
 | 
					                ):
 | 
				
			||||||
 | 
					                    patch_policy = core_settings.server_policy.winupdatepolicy.get()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        elif self.monitoring_type == "workstation":
 | 
					        elif self.monitoring_type == "workstation":
 | 
				
			||||||
            # check agent policy first which should override client or site policy
 | 
					            # check agent policy first which should override client or site policy
 | 
				
			||||||
@@ -331,21 +465,36 @@ class Agent(BaseAuditModel):
 | 
				
			|||||||
                site.workstation_policy
 | 
					                site.workstation_policy
 | 
				
			||||||
                and site.workstation_policy.winupdatepolicy.exists()
 | 
					                and site.workstation_policy.winupdatepolicy.exists()
 | 
				
			||||||
            ):
 | 
					            ):
 | 
				
			||||||
                patch_policy = site.workstation_policy.winupdatepolicy.get()
 | 
					                # make sure agent isn;t blocking policy inheritance
 | 
				
			||||||
 | 
					                if not self.block_policy_inheritance:
 | 
				
			||||||
 | 
					                    patch_policy = site.workstation_policy.winupdatepolicy.get()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # if site doesn't have a patch policy check the client
 | 
					            # if site doesn't have a patch policy check the client
 | 
				
			||||||
            elif (
 | 
					            elif (
 | 
				
			||||||
                site.client.workstation_policy
 | 
					                site.client.workstation_policy
 | 
				
			||||||
                and site.client.workstation_policy.winupdatepolicy.exists()
 | 
					                and site.client.workstation_policy.winupdatepolicy.exists()
 | 
				
			||||||
            ):
 | 
					            ):
 | 
				
			||||||
                patch_policy = site.client.workstation_policy.winupdatepolicy.get()
 | 
					                # make sure agent and site are not blocking inheritance
 | 
				
			||||||
 | 
					                if (
 | 
				
			||||||
 | 
					                    not self.block_policy_inheritance
 | 
				
			||||||
 | 
					                    and not site.block_policy_inheritance
 | 
				
			||||||
 | 
					                ):
 | 
				
			||||||
 | 
					                    patch_policy = site.client.workstation_policy.winupdatepolicy.get()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # if patch policy still doesn't exist check default policy
 | 
					            # if patch policy still doesn't exist check default policy
 | 
				
			||||||
            elif (
 | 
					            elif (
 | 
				
			||||||
                core_settings.workstation_policy
 | 
					                core_settings.workstation_policy
 | 
				
			||||||
                and core_settings.workstation_policy.winupdatepolicy.exists()
 | 
					                and core_settings.workstation_policy.winupdatepolicy.exists()
 | 
				
			||||||
            ):
 | 
					            ):
 | 
				
			||||||
                patch_policy = core_settings.workstation_policy.winupdatepolicy.get()
 | 
					                # make sure agent site and client are not blocking inheritance
 | 
				
			||||||
 | 
					                if (
 | 
				
			||||||
 | 
					                    not self.block_policy_inheritance
 | 
				
			||||||
 | 
					                    and not site.block_policy_inheritance
 | 
				
			||||||
 | 
					                    and not site.client.block_policy_inheritance
 | 
				
			||||||
 | 
					                ):
 | 
				
			||||||
 | 
					                    patch_policy = (
 | 
				
			||||||
 | 
					                        core_settings.workstation_policy.winupdatepolicy.get()
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # if policy still doesn't exist return the agent patch policy
 | 
					        # if policy still doesn't exist return the agent patch policy
 | 
				
			||||||
        if not patch_policy:
 | 
					        if not patch_policy:
 | 
				
			||||||
@@ -382,32 +531,162 @@ class Agent(BaseAuditModel):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        return patch_policy
 | 
					        return patch_policy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # clear is used to delete managed policy checks from agent
 | 
					    def get_approved_update_guids(self) -> list[str]:
 | 
				
			||||||
    # parent_checks specifies a list of checks to delete from agent with matching parent_check field
 | 
					        return list(
 | 
				
			||||||
    def generate_checks_from_policies(self, clear=False):
 | 
					            self.winupdates.filter(action="approve", installed=False).values_list(  # type: ignore
 | 
				
			||||||
 | 
					                "guid", flat=True
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # sets alert template assigned in the following order: policy, site, client, global
 | 
				
			||||||
 | 
					    # sets None if nothing is found
 | 
				
			||||||
 | 
					    def set_alert_template(self):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        site = self.site
 | 
				
			||||||
 | 
					        client = self.client
 | 
				
			||||||
 | 
					        core = CoreSettings.objects.first()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        templates = list()
 | 
				
			||||||
 | 
					        # check if alert template is on a policy assigned to agent
 | 
				
			||||||
 | 
					        if (
 | 
				
			||||||
 | 
					            self.policy
 | 
				
			||||||
 | 
					            and self.policy.alert_template
 | 
				
			||||||
 | 
					            and self.policy.alert_template.is_active
 | 
				
			||||||
 | 
					        ):
 | 
				
			||||||
 | 
					            templates.append(self.policy.alert_template)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # check if policy with alert template is assigned to the site
 | 
				
			||||||
 | 
					        if (
 | 
				
			||||||
 | 
					            self.monitoring_type == "server"
 | 
				
			||||||
 | 
					            and site.server_policy
 | 
				
			||||||
 | 
					            and site.server_policy.alert_template
 | 
				
			||||||
 | 
					            and site.server_policy.alert_template.is_active
 | 
				
			||||||
 | 
					            and not self.block_policy_inheritance
 | 
				
			||||||
 | 
					        ):
 | 
				
			||||||
 | 
					            templates.append(site.server_policy.alert_template)
 | 
				
			||||||
 | 
					        if (
 | 
				
			||||||
 | 
					            self.monitoring_type == "workstation"
 | 
				
			||||||
 | 
					            and site.workstation_policy
 | 
				
			||||||
 | 
					            and site.workstation_policy.alert_template
 | 
				
			||||||
 | 
					            and site.workstation_policy.alert_template.is_active
 | 
				
			||||||
 | 
					            and not self.block_policy_inheritance
 | 
				
			||||||
 | 
					        ):
 | 
				
			||||||
 | 
					            templates.append(site.workstation_policy.alert_template)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # check if alert template is assigned to site
 | 
				
			||||||
 | 
					        if site.alert_template and site.alert_template.is_active:
 | 
				
			||||||
 | 
					            templates.append(site.alert_template)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # check if policy with alert template is assigned to the client
 | 
				
			||||||
 | 
					        if (
 | 
				
			||||||
 | 
					            self.monitoring_type == "server"
 | 
				
			||||||
 | 
					            and client.server_policy
 | 
				
			||||||
 | 
					            and client.server_policy.alert_template
 | 
				
			||||||
 | 
					            and client.server_policy.alert_template.is_active
 | 
				
			||||||
 | 
					            and not self.block_policy_inheritance
 | 
				
			||||||
 | 
					            and not site.block_policy_inheritance
 | 
				
			||||||
 | 
					        ):
 | 
				
			||||||
 | 
					            templates.append(client.server_policy.alert_template)
 | 
				
			||||||
 | 
					        if (
 | 
				
			||||||
 | 
					            self.monitoring_type == "workstation"
 | 
				
			||||||
 | 
					            and client.workstation_policy
 | 
				
			||||||
 | 
					            and client.workstation_policy.alert_template
 | 
				
			||||||
 | 
					            and client.workstation_policy.alert_template.is_active
 | 
				
			||||||
 | 
					            and not self.block_policy_inheritance
 | 
				
			||||||
 | 
					            and not site.block_policy_inheritance
 | 
				
			||||||
 | 
					        ):
 | 
				
			||||||
 | 
					            templates.append(client.workstation_policy.alert_template)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # check if alert template is on client and return
 | 
				
			||||||
 | 
					        if (
 | 
				
			||||||
 | 
					            client.alert_template
 | 
				
			||||||
 | 
					            and client.alert_template.is_active
 | 
				
			||||||
 | 
					            and not self.block_policy_inheritance
 | 
				
			||||||
 | 
					            and not site.block_policy_inheritance
 | 
				
			||||||
 | 
					        ):
 | 
				
			||||||
 | 
					            templates.append(client.alert_template)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # check if alert template is applied globally and return
 | 
				
			||||||
 | 
					        if (
 | 
				
			||||||
 | 
					            core.alert_template
 | 
				
			||||||
 | 
					            and core.alert_template.is_active
 | 
				
			||||||
 | 
					            and not self.block_policy_inheritance
 | 
				
			||||||
 | 
					            and not site.block_policy_inheritance
 | 
				
			||||||
 | 
					            and not client.block_policy_inheritance
 | 
				
			||||||
 | 
					        ):
 | 
				
			||||||
 | 
					            templates.append(core.alert_template)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # if agent is a workstation, check if policy with alert template is assigned to the site, client, or core
 | 
				
			||||||
 | 
					        if (
 | 
				
			||||||
 | 
					            self.monitoring_type == "server"
 | 
				
			||||||
 | 
					            and core.server_policy
 | 
				
			||||||
 | 
					            and core.server_policy.alert_template
 | 
				
			||||||
 | 
					            and core.server_policy.alert_template.is_active
 | 
				
			||||||
 | 
					            and not self.block_policy_inheritance
 | 
				
			||||||
 | 
					            and not site.block_policy_inheritance
 | 
				
			||||||
 | 
					            and not client.block_policy_inheritance
 | 
				
			||||||
 | 
					        ):
 | 
				
			||||||
 | 
					            templates.append(core.server_policy.alert_template)
 | 
				
			||||||
 | 
					        if (
 | 
				
			||||||
 | 
					            self.monitoring_type == "workstation"
 | 
				
			||||||
 | 
					            and core.workstation_policy
 | 
				
			||||||
 | 
					            and core.workstation_policy.alert_template
 | 
				
			||||||
 | 
					            and core.workstation_policy.alert_template.is_active
 | 
				
			||||||
 | 
					            and not self.block_policy_inheritance
 | 
				
			||||||
 | 
					            and not site.block_policy_inheritance
 | 
				
			||||||
 | 
					            and not client.block_policy_inheritance
 | 
				
			||||||
 | 
					        ):
 | 
				
			||||||
 | 
					            templates.append(core.workstation_policy.alert_template)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # go through the templates and return the first one that isn't excluded
 | 
				
			||||||
 | 
					        for template in templates:
 | 
				
			||||||
 | 
					            # check if client, site, or agent has been excluded from template
 | 
				
			||||||
 | 
					            if (
 | 
				
			||||||
 | 
					                client.pk
 | 
				
			||||||
 | 
					                in template.excluded_clients.all().values_list("pk", flat=True)
 | 
				
			||||||
 | 
					                or site.pk in template.excluded_sites.all().values_list("pk", flat=True)
 | 
				
			||||||
 | 
					                or self.pk
 | 
				
			||||||
 | 
					                in template.excluded_agents.all()
 | 
				
			||||||
 | 
					                .only("pk")
 | 
				
			||||||
 | 
					                .values_list("pk", flat=True)
 | 
				
			||||||
 | 
					            ):
 | 
				
			||||||
 | 
					                continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # check if template is excluding desktops
 | 
				
			||||||
 | 
					            elif (
 | 
				
			||||||
 | 
					                self.monitoring_type == "workstation" and template.exclude_workstations
 | 
				
			||||||
 | 
					            ):
 | 
				
			||||||
 | 
					                continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # check if template is excluding servers
 | 
				
			||||||
 | 
					            elif self.monitoring_type == "server" and template.exclude_servers:
 | 
				
			||||||
 | 
					                continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                # save alert_template to agent cache field
 | 
				
			||||||
 | 
					                self.alert_template = template
 | 
				
			||||||
 | 
					                self.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                return template
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # no alert templates found or agent has been excluded
 | 
				
			||||||
 | 
					        self.alert_template = None
 | 
				
			||||||
 | 
					        self.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def generate_checks_from_policies(self):
 | 
				
			||||||
        from automation.models import Policy
 | 
					        from automation.models import Policy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Clear agent checks managed by policy
 | 
					 | 
				
			||||||
        if clear:
 | 
					 | 
				
			||||||
            self.agentchecks.filter(managed_by_policy=True).delete()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Clear agent checks that have overriden_by_policy set
 | 
					        # Clear agent checks that have overriden_by_policy set
 | 
				
			||||||
        self.agentchecks.update(overriden_by_policy=False)
 | 
					        self.agentchecks.update(overriden_by_policy=False)  # type: ignore
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Generate checks based on policies
 | 
					        # Generate checks based on policies
 | 
				
			||||||
        Policy.generate_policy_checks(self)
 | 
					        Policy.generate_policy_checks(self)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # clear is used to delete managed policy tasks from agent
 | 
					    def generate_tasks_from_policies(self):
 | 
				
			||||||
    # parent_tasks specifies a list of tasks to delete from agent with matching parent_task field
 | 
					 | 
				
			||||||
    def generate_tasks_from_policies(self, clear=False):
 | 
					 | 
				
			||||||
        from autotasks.tasks import delete_win_task_schedule
 | 
					 | 
				
			||||||
        from automation.models import Policy
 | 
					        from automation.models import Policy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Clear agent tasks managed by policy
 | 
					 | 
				
			||||||
        if clear:
 | 
					 | 
				
			||||||
            for task in self.autotasks.filter(managed_by_policy=True):
 | 
					 | 
				
			||||||
                delete_win_task_schedule.delay(task.pk)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Generate tasks based on policies
 | 
					        # Generate tasks based on policies
 | 
				
			||||||
        Policy.generate_policy_tasks(self)
 | 
					        Policy.generate_policy_tasks(self)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -435,7 +714,7 @@ class Agent(BaseAuditModel):
 | 
				
			|||||||
        except Exception:
 | 
					        except Exception:
 | 
				
			||||||
            return "err"
 | 
					            return "err"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async def nats_cmd(self, data, timeout=30, wait=True):
 | 
					    async def nats_cmd(self, data: dict, timeout: int = 30, wait: bool = True):
 | 
				
			||||||
        nc = NATS()
 | 
					        nc = NATS()
 | 
				
			||||||
        options = {
 | 
					        options = {
 | 
				
			||||||
            "servers": f"tls://{settings.ALLOWED_HOSTS[0]}:4222",
 | 
					            "servers": f"tls://{settings.ALLOWED_HOSTS[0]}:4222",
 | 
				
			||||||
@@ -457,7 +736,11 @@ class Agent(BaseAuditModel):
 | 
				
			|||||||
            except ErrTimeout:
 | 
					            except ErrTimeout:
 | 
				
			||||||
                ret = "timeout"
 | 
					                ret = "timeout"
 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
                ret = msgpack.loads(msg.data)
 | 
					                try:
 | 
				
			||||||
 | 
					                    ret = msgpack.loads(msg.data)  # type: ignore
 | 
				
			||||||
 | 
					                except Exception as e:
 | 
				
			||||||
 | 
					                    logger.error(e)
 | 
				
			||||||
 | 
					                    ret = str(e)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            await nc.close()
 | 
					            await nc.close()
 | 
				
			||||||
            return ret
 | 
					            return ret
 | 
				
			||||||
@@ -466,77 +749,6 @@ class Agent(BaseAuditModel):
 | 
				
			|||||||
            await nc.flush()
 | 
					            await nc.flush()
 | 
				
			||||||
            await nc.close()
 | 
					            await nc.close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def salt_api_cmd(self, **kwargs):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # salt should always timeout first before the requests' timeout
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            timeout = kwargs["timeout"]
 | 
					 | 
				
			||||||
        except KeyError:
 | 
					 | 
				
			||||||
            # default timeout
 | 
					 | 
				
			||||||
            timeout = 15
 | 
					 | 
				
			||||||
            salt_timeout = 12
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            if timeout < 8:
 | 
					 | 
				
			||||||
                timeout = 8
 | 
					 | 
				
			||||||
                salt_timeout = 5
 | 
					 | 
				
			||||||
            else:
 | 
					 | 
				
			||||||
                salt_timeout = timeout - 3
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        json = {
 | 
					 | 
				
			||||||
            "client": "local",
 | 
					 | 
				
			||||||
            "tgt": self.salt_id,
 | 
					 | 
				
			||||||
            "fun": kwargs["func"],
 | 
					 | 
				
			||||||
            "timeout": salt_timeout,
 | 
					 | 
				
			||||||
            "username": settings.SALT_USERNAME,
 | 
					 | 
				
			||||||
            "password": settings.SALT_PASSWORD,
 | 
					 | 
				
			||||||
            "eauth": "pam",
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if "arg" in kwargs:
 | 
					 | 
				
			||||||
            json.update({"arg": kwargs["arg"]})
 | 
					 | 
				
			||||||
        if "kwargs" in kwargs:
 | 
					 | 
				
			||||||
            json.update({"kwarg": kwargs["kwargs"]})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            resp = requests.post(
 | 
					 | 
				
			||||||
                f"http://{settings.SALT_HOST}:8123/run",
 | 
					 | 
				
			||||||
                json=[json],
 | 
					 | 
				
			||||||
                timeout=timeout,
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        except Exception:
 | 
					 | 
				
			||||||
            return "timeout"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            ret = resp.json()["return"][0][self.salt_id]
 | 
					 | 
				
			||||||
        except Exception as e:
 | 
					 | 
				
			||||||
            logger.error(f"{self.salt_id}: {e}")
 | 
					 | 
				
			||||||
            return "error"
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            return ret
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def salt_api_async(self, **kwargs):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        json = {
 | 
					 | 
				
			||||||
            "client": "local_async",
 | 
					 | 
				
			||||||
            "tgt": self.salt_id,
 | 
					 | 
				
			||||||
            "fun": kwargs["func"],
 | 
					 | 
				
			||||||
            "username": settings.SALT_USERNAME,
 | 
					 | 
				
			||||||
            "password": settings.SALT_PASSWORD,
 | 
					 | 
				
			||||||
            "eauth": "pam",
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if "arg" in kwargs:
 | 
					 | 
				
			||||||
            json.update({"arg": kwargs["arg"]})
 | 
					 | 
				
			||||||
        if "kwargs" in kwargs:
 | 
					 | 
				
			||||||
            json.update({"kwarg": kwargs["kwargs"]})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            resp = requests.post(f"http://{settings.SALT_HOST}:8123/run", json=[json])
 | 
					 | 
				
			||||||
        except Exception:
 | 
					 | 
				
			||||||
            return "timeout"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return resp
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @staticmethod
 | 
					    @staticmethod
 | 
				
			||||||
    def serialize(agent):
 | 
					    def serialize(agent):
 | 
				
			||||||
        # serializes the agent and returns json
 | 
					        # serializes the agent and returns json
 | 
				
			||||||
@@ -547,41 +759,15 @@ class Agent(BaseAuditModel):
 | 
				
			|||||||
        del ret["client"]
 | 
					        del ret["client"]
 | 
				
			||||||
        return ret
 | 
					        return ret
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @staticmethod
 | 
					 | 
				
			||||||
    def salt_batch_async(**kwargs):
 | 
					 | 
				
			||||||
        assert isinstance(kwargs["minions"], list)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        json = {
 | 
					 | 
				
			||||||
            "client": "local_async",
 | 
					 | 
				
			||||||
            "tgt_type": "list",
 | 
					 | 
				
			||||||
            "tgt": kwargs["minions"],
 | 
					 | 
				
			||||||
            "fun": kwargs["func"],
 | 
					 | 
				
			||||||
            "username": settings.SALT_USERNAME,
 | 
					 | 
				
			||||||
            "password": settings.SALT_PASSWORD,
 | 
					 | 
				
			||||||
            "eauth": "pam",
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if "arg" in kwargs:
 | 
					 | 
				
			||||||
            json.update({"arg": kwargs["arg"]})
 | 
					 | 
				
			||||||
        if "kwargs" in kwargs:
 | 
					 | 
				
			||||||
            json.update({"kwarg": kwargs["kwargs"]})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            resp = requests.post(f"http://{settings.SALT_HOST}:8123/run", json=[json])
 | 
					 | 
				
			||||||
        except Exception:
 | 
					 | 
				
			||||||
            return "timeout"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return resp
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def delete_superseded_updates(self):
 | 
					    def delete_superseded_updates(self):
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            pks = []  # list of pks to delete
 | 
					            pks = []  # list of pks to delete
 | 
				
			||||||
            kbs = list(self.winupdates.values_list("kb", flat=True))
 | 
					            kbs = list(self.winupdates.values_list("kb", flat=True))  # type: ignore
 | 
				
			||||||
            d = Counter(kbs)
 | 
					            d = Counter(kbs)
 | 
				
			||||||
            dupes = [k for k, v in d.items() if v > 1]
 | 
					            dupes = [k for k, v in d.items() if v > 1]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            for dupe in dupes:
 | 
					            for dupe in dupes:
 | 
				
			||||||
                titles = self.winupdates.filter(kb=dupe).values_list("title", flat=True)
 | 
					                titles = self.winupdates.filter(kb=dupe).values_list("title", flat=True)  # type: ignore
 | 
				
			||||||
                # extract the version from the title and sort from oldest to newest
 | 
					                # extract the version from the title and sort from oldest to newest
 | 
				
			||||||
                # skip if no version info is available therefore nothing to parse
 | 
					                # skip if no version info is available therefore nothing to parse
 | 
				
			||||||
                try:
 | 
					                try:
 | 
				
			||||||
@@ -594,69 +780,42 @@ class Agent(BaseAuditModel):
 | 
				
			|||||||
                    continue
 | 
					                    continue
 | 
				
			||||||
                # append all but the latest version to our list of pks to delete
 | 
					                # append all but the latest version to our list of pks to delete
 | 
				
			||||||
                for ver in sorted_vers[:-1]:
 | 
					                for ver in sorted_vers[:-1]:
 | 
				
			||||||
                    q = self.winupdates.filter(kb=dupe).filter(title__contains=ver)
 | 
					                    q = self.winupdates.filter(kb=dupe).filter(title__contains=ver)  # type: ignore
 | 
				
			||||||
                    pks.append(q.first().pk)
 | 
					                    pks.append(q.first().pk)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            pks = list(set(pks))
 | 
					            pks = list(set(pks))
 | 
				
			||||||
            self.winupdates.filter(pk__in=pks).delete()
 | 
					            self.winupdates.filter(pk__in=pks).delete()  # type: ignore
 | 
				
			||||||
        except:
 | 
					        except:
 | 
				
			||||||
            pass
 | 
					            pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # define how the agent should handle pending actions
 | 
					    def should_create_alert(self, alert_template=None):
 | 
				
			||||||
    def handle_pending_actions(self):
 | 
					        return (
 | 
				
			||||||
        pending_actions = self.pendingactions.filter(status="pending")
 | 
					            self.overdue_dashboard_alert
 | 
				
			||||||
 | 
					            or self.overdue_email_alert
 | 
				
			||||||
        for action in pending_actions:
 | 
					            or self.overdue_text_alert
 | 
				
			||||||
            if action.action_type == "taskaction":
 | 
					            or (
 | 
				
			||||||
                from autotasks.tasks import (
 | 
					                alert_template
 | 
				
			||||||
                    create_win_task_schedule,
 | 
					                and (
 | 
				
			||||||
                    enable_or_disable_win_task,
 | 
					                    alert_template.agent_always_alert
 | 
				
			||||||
                    delete_win_task_schedule,
 | 
					                    or alert_template.agent_always_email
 | 
				
			||||||
 | 
					                    or alert_template.agent_always_text
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
                task_id = action.details["task_id"]
 | 
					        )
 | 
				
			||||||
 | 
					 | 
				
			||||||
                if action.details["action"] == "taskcreate":
 | 
					 | 
				
			||||||
                    create_win_task_schedule.delay(task_id, pending_action=action.id)
 | 
					 | 
				
			||||||
                elif action.details["action"] == "tasktoggle":
 | 
					 | 
				
			||||||
                    enable_or_disable_win_task.delay(
 | 
					 | 
				
			||||||
                        task_id, action.details["value"], pending_action=action.id
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                elif action.details["action"] == "taskdelete":
 | 
					 | 
				
			||||||
                    delete_win_task_schedule.delay(task_id, pending_action=action.id)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class AgentOutage(models.Model):
 | 
					 | 
				
			||||||
    agent = models.ForeignKey(
 | 
					 | 
				
			||||||
        Agent,
 | 
					 | 
				
			||||||
        related_name="agentoutages",
 | 
					 | 
				
			||||||
        null=True,
 | 
					 | 
				
			||||||
        blank=True,
 | 
					 | 
				
			||||||
        on_delete=models.CASCADE,
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    outage_time = models.DateTimeField(auto_now_add=True)
 | 
					 | 
				
			||||||
    recovery_time = models.DateTimeField(null=True, blank=True)
 | 
					 | 
				
			||||||
    outage_email_sent = models.BooleanField(default=False)
 | 
					 | 
				
			||||||
    outage_sms_sent = models.BooleanField(default=False)
 | 
					 | 
				
			||||||
    recovery_email_sent = models.BooleanField(default=False)
 | 
					 | 
				
			||||||
    recovery_sms_sent = models.BooleanField(default=False)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @property
 | 
					 | 
				
			||||||
    def is_active(self):
 | 
					 | 
				
			||||||
        return False if self.recovery_time else True
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def send_outage_email(self):
 | 
					    def send_outage_email(self):
 | 
				
			||||||
        from core.models import CoreSettings
 | 
					        from core.models import CoreSettings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        CORE = CoreSettings.objects.first()
 | 
					        CORE = CoreSettings.objects.first()
 | 
				
			||||||
        CORE.send_mail(
 | 
					        CORE.send_mail(
 | 
				
			||||||
            f"{self.agent.client.name}, {self.agent.site.name}, {self.agent.hostname} - data overdue",
 | 
					            f"{self.client.name}, {self.site.name}, {self.hostname} - data overdue",
 | 
				
			||||||
            (
 | 
					            (
 | 
				
			||||||
                f"Data has not been received from client {self.agent.client.name}, "
 | 
					                f"Data has not been received from client {self.client.name}, "
 | 
				
			||||||
                f"site {self.agent.site.name}, "
 | 
					                f"site {self.site.name}, "
 | 
				
			||||||
                f"agent {self.agent.hostname} "
 | 
					                f"agent {self.hostname} "
 | 
				
			||||||
                "within the expected time."
 | 
					                "within the expected time."
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
 | 
					            alert_template=self.alert_template,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def send_recovery_email(self):
 | 
					    def send_recovery_email(self):
 | 
				
			||||||
@@ -664,13 +823,14 @@ class AgentOutage(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        CORE = CoreSettings.objects.first()
 | 
					        CORE = CoreSettings.objects.first()
 | 
				
			||||||
        CORE.send_mail(
 | 
					        CORE.send_mail(
 | 
				
			||||||
            f"{self.agent.client.name}, {self.agent.site.name}, {self.agent.hostname} - data received",
 | 
					            f"{self.client.name}, {self.site.name}, {self.hostname} - data received",
 | 
				
			||||||
            (
 | 
					            (
 | 
				
			||||||
                f"Data has been received from client {self.agent.client.name}, "
 | 
					                f"Data has been received from client {self.client.name}, "
 | 
				
			||||||
                f"site {self.agent.site.name}, "
 | 
					                f"site {self.site.name}, "
 | 
				
			||||||
                f"agent {self.agent.hostname} "
 | 
					                f"agent {self.hostname} "
 | 
				
			||||||
                "after an interruption in data transmission."
 | 
					                "after an interruption in data transmission."
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
 | 
					            alert_template=self.alert_template,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def send_outage_sms(self):
 | 
					    def send_outage_sms(self):
 | 
				
			||||||
@@ -678,7 +838,8 @@ class AgentOutage(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        CORE = CoreSettings.objects.first()
 | 
					        CORE = CoreSettings.objects.first()
 | 
				
			||||||
        CORE.send_sms(
 | 
					        CORE.send_sms(
 | 
				
			||||||
            f"{self.agent.client.name}, {self.agent.site.name}, {self.agent.hostname} - data overdue"
 | 
					            f"{self.client.name}, {self.site.name}, {self.hostname} - data overdue",
 | 
				
			||||||
 | 
					            alert_template=self.alert_template,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def send_recovery_sms(self):
 | 
					    def send_recovery_sms(self):
 | 
				
			||||||
@@ -686,12 +847,10 @@ class AgentOutage(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        CORE = CoreSettings.objects.first()
 | 
					        CORE = CoreSettings.objects.first()
 | 
				
			||||||
        CORE.send_sms(
 | 
					        CORE.send_sms(
 | 
				
			||||||
            f"{self.agent.client.name}, {self.agent.site.name}, {self.agent.hostname} - data received"
 | 
					            f"{self.client.name}, {self.site.name}, {self.hostname} - data received",
 | 
				
			||||||
 | 
					            alert_template=self.alert_template,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __str__(self):
 | 
					 | 
				
			||||||
        return self.agent.hostname
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
RECOVERY_CHOICES = [
 | 
					RECOVERY_CHOICES = [
 | 
				
			||||||
    ("salt", "Salt"),
 | 
					    ("salt", "Salt"),
 | 
				
			||||||
@@ -715,12 +874,6 @@ class RecoveryAction(models.Model):
 | 
				
			|||||||
    def __str__(self):
 | 
					    def __str__(self):
 | 
				
			||||||
        return f"{self.agent.hostname} - {self.mode}"
 | 
					        return f"{self.agent.hostname} - {self.mode}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def send(self):
 | 
					 | 
				
			||||||
        ret = {"recovery": self.mode}
 | 
					 | 
				
			||||||
        if self.mode == "command":
 | 
					 | 
				
			||||||
            ret["cmd"] = self.command
 | 
					 | 
				
			||||||
        return ret
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Note(models.Model):
 | 
					class Note(models.Model):
 | 
				
			||||||
    agent = models.ForeignKey(
 | 
					    agent = models.ForeignKey(
 | 
				
			||||||
@@ -740,3 +893,38 @@ class Note(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def __str__(self):
 | 
					    def __str__(self):
 | 
				
			||||||
        return self.agent.hostname
 | 
					        return self.agent.hostname
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AgentCustomField(models.Model):
 | 
				
			||||||
 | 
					    agent = models.ForeignKey(
 | 
				
			||||||
 | 
					        Agent,
 | 
				
			||||||
 | 
					        related_name="custom_fields",
 | 
				
			||||||
 | 
					        on_delete=models.CASCADE,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    field = models.ForeignKey(
 | 
				
			||||||
 | 
					        "core.CustomField",
 | 
				
			||||||
 | 
					        related_name="agent_fields",
 | 
				
			||||||
 | 
					        on_delete=models.CASCADE,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    string_value = models.TextField(null=True, blank=True)
 | 
				
			||||||
 | 
					    bool_value = models.BooleanField(blank=True, default=False)
 | 
				
			||||||
 | 
					    multiple_value = ArrayField(
 | 
				
			||||||
 | 
					        models.TextField(null=True, blank=True),
 | 
				
			||||||
 | 
					        null=True,
 | 
				
			||||||
 | 
					        blank=True,
 | 
				
			||||||
 | 
					        default=list,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __str__(self):
 | 
				
			||||||
 | 
					        return self.field
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def value(self):
 | 
				
			||||||
 | 
					        if self.field.type == "multiple":
 | 
				
			||||||
 | 
					            return self.multiple_value
 | 
				
			||||||
 | 
					        elif self.field.type == "checkbox":
 | 
				
			||||||
 | 
					            return self.bool_value
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            return self.string_value
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										63
									
								
								api/tacticalrmm/agents/permissions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								api/tacticalrmm/agents/permissions.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,63 @@
 | 
				
			|||||||
 | 
					from rest_framework import permissions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from tacticalrmm.permissions import _has_perm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class MeshPerms(permissions.BasePermission):
 | 
				
			||||||
 | 
					    def has_permission(self, r, view):
 | 
				
			||||||
 | 
					        return _has_perm(r, "can_use_mesh")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class UninstallPerms(permissions.BasePermission):
 | 
				
			||||||
 | 
					    def has_permission(self, r, view):
 | 
				
			||||||
 | 
					        return _has_perm(r, "can_uninstall_agents")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class UpdateAgentPerms(permissions.BasePermission):
 | 
				
			||||||
 | 
					    def has_permission(self, r, view):
 | 
				
			||||||
 | 
					        return _has_perm(r, "can_update_agents")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class EditAgentPerms(permissions.BasePermission):
 | 
				
			||||||
 | 
					    def has_permission(self, r, view):
 | 
				
			||||||
 | 
					        return _has_perm(r, "can_edit_agent")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ManageProcPerms(permissions.BasePermission):
 | 
				
			||||||
 | 
					    def has_permission(self, r, view):
 | 
				
			||||||
 | 
					        return _has_perm(r, "can_manage_procs")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class EvtLogPerms(permissions.BasePermission):
 | 
				
			||||||
 | 
					    def has_permission(self, r, view):
 | 
				
			||||||
 | 
					        return _has_perm(r, "can_view_eventlogs")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SendCMDPerms(permissions.BasePermission):
 | 
				
			||||||
 | 
					    def has_permission(self, r, view):
 | 
				
			||||||
 | 
					        return _has_perm(r, "can_send_cmd")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class RebootAgentPerms(permissions.BasePermission):
 | 
				
			||||||
 | 
					    def has_permission(self, r, view):
 | 
				
			||||||
 | 
					        return _has_perm(r, "can_reboot_agents")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class InstallAgentPerms(permissions.BasePermission):
 | 
				
			||||||
 | 
					    def has_permission(self, r, view):
 | 
				
			||||||
 | 
					        return _has_perm(r, "can_install_agents")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class RunScriptPerms(permissions.BasePermission):
 | 
				
			||||||
 | 
					    def has_permission(self, r, view):
 | 
				
			||||||
 | 
					        return _has_perm(r, "can_run_scripts")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ManageNotesPerms(permissions.BasePermission):
 | 
				
			||||||
 | 
					    def has_permission(self, r, view):
 | 
				
			||||||
 | 
					        return _has_perm(r, "can_manage_notes")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class RunBulkPerms(permissions.BasePermission):
 | 
				
			||||||
 | 
					    def has_permission(self, r, view):
 | 
				
			||||||
 | 
					        return _has_perm(r, "can_run_bulk")
 | 
				
			||||||
@@ -1,12 +1,10 @@
 | 
				
			|||||||
import pytz
 | 
					import pytz
 | 
				
			||||||
 | 
					 | 
				
			||||||
from rest_framework import serializers
 | 
					from rest_framework import serializers
 | 
				
			||||||
from rest_framework.fields import ReadOnlyField
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .models import Agent, Note
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from winupdate.serializers import WinUpdatePolicySerializer
 | 
					 | 
				
			||||||
from clients.serializers import ClientSerializer
 | 
					from clients.serializers import ClientSerializer
 | 
				
			||||||
 | 
					from winupdate.serializers import WinUpdatePolicySerializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .models import Agent, AgentCustomField, Note
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AgentSerializer(serializers.ModelSerializer):
 | 
					class AgentSerializer(serializers.ModelSerializer):
 | 
				
			||||||
@@ -18,6 +16,7 @@ class AgentSerializer(serializers.ModelSerializer):
 | 
				
			|||||||
    local_ips = serializers.ReadOnlyField()
 | 
					    local_ips = serializers.ReadOnlyField()
 | 
				
			||||||
    make_model = serializers.ReadOnlyField()
 | 
					    make_model = serializers.ReadOnlyField()
 | 
				
			||||||
    physical_disks = serializers.ReadOnlyField()
 | 
					    physical_disks = serializers.ReadOnlyField()
 | 
				
			||||||
 | 
					    graphics = serializers.ReadOnlyField()
 | 
				
			||||||
    checks = serializers.ReadOnlyField()
 | 
					    checks = serializers.ReadOnlyField()
 | 
				
			||||||
    timezone = serializers.ReadOnlyField()
 | 
					    timezone = serializers.ReadOnlyField()
 | 
				
			||||||
    all_timezones = serializers.SerializerMethodField()
 | 
					    all_timezones = serializers.SerializerMethodField()
 | 
				
			||||||
@@ -34,6 +33,17 @@ class AgentSerializer(serializers.ModelSerializer):
 | 
				
			|||||||
        ]
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AgentOverdueActionSerializer(serializers.ModelSerializer):
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        model = Agent
 | 
				
			||||||
 | 
					        fields = [
 | 
				
			||||||
 | 
					            "pk",
 | 
				
			||||||
 | 
					            "overdue_email_alert",
 | 
				
			||||||
 | 
					            "overdue_text_alert",
 | 
				
			||||||
 | 
					            "overdue_dashboard_alert",
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AgentTableSerializer(serializers.ModelSerializer):
 | 
					class AgentTableSerializer(serializers.ModelSerializer):
 | 
				
			||||||
    patches_pending = serializers.ReadOnlyField(source="has_patches_pending")
 | 
					    patches_pending = serializers.ReadOnlyField(source="has_patches_pending")
 | 
				
			||||||
    pending_actions = serializers.SerializerMethodField()
 | 
					    pending_actions = serializers.SerializerMethodField()
 | 
				
			||||||
@@ -42,22 +52,50 @@ class AgentTableSerializer(serializers.ModelSerializer):
 | 
				
			|||||||
    last_seen = serializers.SerializerMethodField()
 | 
					    last_seen = serializers.SerializerMethodField()
 | 
				
			||||||
    client_name = serializers.ReadOnlyField(source="client.name")
 | 
					    client_name = serializers.ReadOnlyField(source="client.name")
 | 
				
			||||||
    site_name = serializers.ReadOnlyField(source="site.name")
 | 
					    site_name = serializers.ReadOnlyField(source="site.name")
 | 
				
			||||||
 | 
					    logged_username = serializers.SerializerMethodField()
 | 
				
			||||||
 | 
					    italic = serializers.SerializerMethodField()
 | 
				
			||||||
 | 
					    policy = serializers.ReadOnlyField(source="policy.id")
 | 
				
			||||||
 | 
					    alert_template = serializers.SerializerMethodField()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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,
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_pending_actions(self, obj):
 | 
					    def get_pending_actions(self, obj):
 | 
				
			||||||
        return obj.pendingactions.filter(status="pending").count()
 | 
					        return obj.pendingactions.filter(status="pending").count()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_last_seen(self, obj):
 | 
					    def get_last_seen(self, obj) -> str:
 | 
				
			||||||
        if obj.time_zone is not None:
 | 
					        if obj.time_zone is not None:
 | 
				
			||||||
            agent_tz = pytz.timezone(obj.time_zone)
 | 
					            agent_tz = pytz.timezone(obj.time_zone)
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            agent_tz = self.context["default_tz"]
 | 
					            agent_tz = self.context["default_tz"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return obj.last_seen.astimezone(agent_tz).strftime("%m %d %Y %H:%M:%S")
 | 
					        return obj.last_seen.astimezone(agent_tz).strftime("%m %d %Y %H:%M")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_logged_username(self, obj) -> str:
 | 
				
			||||||
 | 
					        if obj.logged_in_username == "None" and obj.status == "online":
 | 
				
			||||||
 | 
					            return obj.last_logged_in_user
 | 
				
			||||||
 | 
					        elif obj.logged_in_username != "None":
 | 
				
			||||||
 | 
					            return obj.logged_in_username
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            return "-"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_italic(self, obj) -> bool:
 | 
				
			||||||
 | 
					        return obj.logged_in_username == "None" and obj.status == "online"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = Agent
 | 
					        model = Agent
 | 
				
			||||||
        fields = [
 | 
					        fields = [
 | 
				
			||||||
            "id",
 | 
					            "id",
 | 
				
			||||||
 | 
					            "alert_template",
 | 
				
			||||||
            "hostname",
 | 
					            "hostname",
 | 
				
			||||||
            "agent_id",
 | 
					            "agent_id",
 | 
				
			||||||
            "site_name",
 | 
					            "site_name",
 | 
				
			||||||
@@ -70,20 +108,43 @@ class AgentTableSerializer(serializers.ModelSerializer):
 | 
				
			|||||||
            "status",
 | 
					            "status",
 | 
				
			||||||
            "overdue_text_alert",
 | 
					            "overdue_text_alert",
 | 
				
			||||||
            "overdue_email_alert",
 | 
					            "overdue_email_alert",
 | 
				
			||||||
 | 
					            "overdue_dashboard_alert",
 | 
				
			||||||
            "last_seen",
 | 
					            "last_seen",
 | 
				
			||||||
            "boot_time",
 | 
					            "boot_time",
 | 
				
			||||||
            "checks",
 | 
					            "checks",
 | 
				
			||||||
            "logged_in_username",
 | 
					 | 
				
			||||||
            "last_logged_in_user",
 | 
					 | 
				
			||||||
            "maintenance_mode",
 | 
					            "maintenance_mode",
 | 
				
			||||||
 | 
					            "logged_username",
 | 
				
			||||||
 | 
					            "italic",
 | 
				
			||||||
 | 
					            "policy",
 | 
				
			||||||
 | 
					            "block_policy_inheritance",
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
        depth = 2
 | 
					        depth = 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AgentCustomFieldSerializer(serializers.ModelSerializer):
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        model = AgentCustomField
 | 
				
			||||||
 | 
					        fields = (
 | 
				
			||||||
 | 
					            "id",
 | 
				
			||||||
 | 
					            "field",
 | 
				
			||||||
 | 
					            "agent",
 | 
				
			||||||
 | 
					            "value",
 | 
				
			||||||
 | 
					            "string_value",
 | 
				
			||||||
 | 
					            "bool_value",
 | 
				
			||||||
 | 
					            "multiple_value",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        extra_kwargs = {
 | 
				
			||||||
 | 
					            "string_value": {"write_only": True},
 | 
				
			||||||
 | 
					            "bool_value": {"write_only": True},
 | 
				
			||||||
 | 
					            "multiple_value": {"write_only": True},
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AgentEditSerializer(serializers.ModelSerializer):
 | 
					class AgentEditSerializer(serializers.ModelSerializer):
 | 
				
			||||||
    winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True)
 | 
					    winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True)
 | 
				
			||||||
    all_timezones = serializers.SerializerMethodField()
 | 
					    all_timezones = serializers.SerializerMethodField()
 | 
				
			||||||
    client = ClientSerializer(read_only=True)
 | 
					    client = ClientSerializer(read_only=True)
 | 
				
			||||||
 | 
					    custom_fields = AgentCustomFieldSerializer(many=True, read_only=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_all_timezones(self, obj):
 | 
					    def get_all_timezones(self, obj):
 | 
				
			||||||
        return pytz.all_timezones
 | 
					        return pytz.all_timezones
 | 
				
			||||||
@@ -101,10 +162,13 @@ class AgentEditSerializer(serializers.ModelSerializer):
 | 
				
			|||||||
            "timezone",
 | 
					            "timezone",
 | 
				
			||||||
            "check_interval",
 | 
					            "check_interval",
 | 
				
			||||||
            "overdue_time",
 | 
					            "overdue_time",
 | 
				
			||||||
 | 
					            "offline_time",
 | 
				
			||||||
            "overdue_text_alert",
 | 
					            "overdue_text_alert",
 | 
				
			||||||
            "overdue_email_alert",
 | 
					            "overdue_email_alert",
 | 
				
			||||||
            "all_timezones",
 | 
					            "all_timezones",
 | 
				
			||||||
            "winupdatepolicy",
 | 
					            "winupdatepolicy",
 | 
				
			||||||
 | 
					            "policy",
 | 
				
			||||||
 | 
					            "custom_fields",
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,248 +1,325 @@
 | 
				
			|||||||
import asyncio
 | 
					import asyncio
 | 
				
			||||||
from loguru import logger
 | 
					import datetime as dt
 | 
				
			||||||
from time import sleep
 | 
					 | 
				
			||||||
import random
 | 
					import random
 | 
				
			||||||
import requests
 | 
					import urllib.parse
 | 
				
			||||||
from packaging import version as pyver
 | 
					from time import sleep
 | 
				
			||||||
from typing import List
 | 
					from typing import Union
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.conf import settings
 | 
					from django.conf import settings
 | 
				
			||||||
 | 
					from django.utils import timezone as djangotime
 | 
				
			||||||
 | 
					from loguru import logger
 | 
				
			||||||
 | 
					from packaging import version as pyver
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from tacticalrmm.celery import app
 | 
					from agents.models import Agent
 | 
				
			||||||
from agents.models import Agent, AgentOutage
 | 
					from core.models import CodeSignToken, CoreSettings
 | 
				
			||||||
from core.models import CoreSettings
 | 
					 | 
				
			||||||
from logs.models import PendingAction
 | 
					from logs.models import PendingAction
 | 
				
			||||||
 | 
					from scripts.models import Script
 | 
				
			||||||
 | 
					from tacticalrmm.celery import app
 | 
				
			||||||
 | 
					from tacticalrmm.utils import run_nats_api_cmd
 | 
				
			||||||
 | 
					
 | 
				
			||||||
logger.configure(**settings.LOG_CONFIG)
 | 
					logger.configure(**settings.LOG_CONFIG)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def agent_update(pk: int) -> str:
 | 
					def agent_update(pk: int, codesigntoken: str = None, force: bool = False) -> str:
 | 
				
			||||||
 | 
					    from agents.utils import get_exegen_url
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    agent = Agent.objects.get(pk=pk)
 | 
					    agent = Agent.objects.get(pk=pk)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if pyver.parse(agent.version) <= pyver.parse("1.3.0"):
 | 
				
			||||||
 | 
					        return "not supported"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # skip if we can't determine the arch
 | 
					    # skip if we can't determine the arch
 | 
				
			||||||
    if agent.arch is None:
 | 
					    if agent.arch is None:
 | 
				
			||||||
        logger.warning(f"Unable to determine arch on {agent.hostname}. Skipping.")
 | 
					        logger.warning(
 | 
				
			||||||
 | 
					            f"Unable to determine arch on {agent.hostname}. Skipping agent update."
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
        return "noarch"
 | 
					        return "noarch"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if agent.has_nats:
 | 
					    version = settings.LATEST_AGENT_VER
 | 
				
			||||||
 | 
					    inno = agent.win_inno_exe
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if codesigntoken is not None and pyver.parse(version) >= pyver.parse("1.5.0"):
 | 
				
			||||||
 | 
					        base_url = get_exegen_url() + "/api/v1/winagents/?"
 | 
				
			||||||
 | 
					        params = {"version": version, "arch": agent.arch, "token": codesigntoken}
 | 
				
			||||||
 | 
					        url = base_url + urllib.parse.urlencode(params)
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        url = agent.winagent_dl
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not force:
 | 
				
			||||||
        if agent.pendingactions.filter(
 | 
					        if agent.pendingactions.filter(
 | 
				
			||||||
            action_type="agentupdate", status="pending"
 | 
					            action_type="agentupdate", status="pending"
 | 
				
			||||||
        ).exists():
 | 
					        ).exists():
 | 
				
			||||||
            action = agent.pendingactions.filter(
 | 
					            agent.pendingactions.filter(
 | 
				
			||||||
                action_type="agentupdate", status="pending"
 | 
					                action_type="agentupdate", status="pending"
 | 
				
			||||||
            ).last()
 | 
					            ).delete()
 | 
				
			||||||
            if pyver.parse(action.details["version"]) < pyver.parse(
 | 
					 | 
				
			||||||
                settings.LATEST_AGENT_VER
 | 
					 | 
				
			||||||
            ):
 | 
					 | 
				
			||||||
                action.delete()
 | 
					 | 
				
			||||||
            else:
 | 
					 | 
				
			||||||
                return "pending"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        PendingAction.objects.create(
 | 
					        PendingAction.objects.create(
 | 
				
			||||||
            agent=agent,
 | 
					            agent=agent,
 | 
				
			||||||
            action_type="agentupdate",
 | 
					            action_type="agentupdate",
 | 
				
			||||||
            details={
 | 
					            details={
 | 
				
			||||||
                "url": agent.winagent_dl,
 | 
					                "url": url,
 | 
				
			||||||
                "version": settings.LATEST_AGENT_VER,
 | 
					                "version": version,
 | 
				
			||||||
                "inno": agent.win_inno_exe,
 | 
					                "inno": inno,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        return "created"
 | 
					
 | 
				
			||||||
    # TODO
 | 
					    nats_data = {
 | 
				
			||||||
    # Salt is deprecated, remove this once salt is gone
 | 
					        "func": "agentupdate",
 | 
				
			||||||
    else:
 | 
					        "payload": {
 | 
				
			||||||
        agent.salt_api_async(
 | 
					            "url": url,
 | 
				
			||||||
            func="win_agent.do_agent_update_v2",
 | 
					            "version": version,
 | 
				
			||||||
            kwargs={
 | 
					            "inno": inno,
 | 
				
			||||||
                "inno": agent.win_inno_exe,
 | 
					        },
 | 
				
			||||||
                "url": agent.winagent_dl,
 | 
					    }
 | 
				
			||||||
            },
 | 
					    asyncio.run(agent.nats_cmd(nats_data, wait=False))
 | 
				
			||||||
        )
 | 
					    return "created"
 | 
				
			||||||
        return "salt"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@app.task
 | 
					@app.task
 | 
				
			||||||
def send_agent_update_task(pks: List[int], version: str) -> None:
 | 
					def force_code_sign(pks: list[int]) -> None:
 | 
				
			||||||
    q = Agent.objects.filter(pk__in=pks)
 | 
					    try:
 | 
				
			||||||
    agents: List[int] = [
 | 
					        token = CodeSignToken.objects.first().token
 | 
				
			||||||
        i.pk for i in q if pyver.parse(i.version) < pyver.parse(version)
 | 
					    except:
 | 
				
			||||||
    ]
 | 
					        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for pk in agents:
 | 
					    chunks = (pks[i : i + 50] for i in range(0, len(pks), 50))
 | 
				
			||||||
        agent_update(pk)
 | 
					    for chunk in chunks:
 | 
				
			||||||
 | 
					        for pk in chunk:
 | 
				
			||||||
 | 
					            agent_update(pk=pk, codesigntoken=token, force=True)
 | 
				
			||||||
 | 
					            sleep(0.05)
 | 
				
			||||||
 | 
					        sleep(4)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.task
 | 
				
			||||||
 | 
					def send_agent_update_task(pks: list[int]) -> None:
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        codesigntoken = CodeSignToken.objects.first().token
 | 
				
			||||||
 | 
					    except:
 | 
				
			||||||
 | 
					        codesigntoken = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    chunks = (pks[i : i + 30] for i in range(0, len(pks), 30))
 | 
				
			||||||
 | 
					    for chunk in chunks:
 | 
				
			||||||
 | 
					        for pk in chunk:
 | 
				
			||||||
 | 
					            agent_update(pk, codesigntoken)
 | 
				
			||||||
 | 
					            sleep(0.05)
 | 
				
			||||||
 | 
					        sleep(4)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@app.task
 | 
					@app.task
 | 
				
			||||||
def auto_self_agent_update_task() -> None:
 | 
					def auto_self_agent_update_task() -> None:
 | 
				
			||||||
    core = CoreSettings.objects.first()
 | 
					    core = CoreSettings.objects.first()
 | 
				
			||||||
    if not core.agent_auto_update:
 | 
					    if not core.agent_auto_update:
 | 
				
			||||||
        logger.info("Agent auto update is disabled. Skipping.")
 | 
					 | 
				
			||||||
        return
 | 
					        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        codesigntoken = CodeSignToken.objects.first().token
 | 
				
			||||||
 | 
					    except:
 | 
				
			||||||
 | 
					        codesigntoken = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    q = Agent.objects.only("pk", "version")
 | 
					    q = Agent.objects.only("pk", "version")
 | 
				
			||||||
    pks: List[int] = [
 | 
					    pks: list[int] = [
 | 
				
			||||||
        i.pk
 | 
					        i.pk
 | 
				
			||||||
        for i in q
 | 
					        for i in q
 | 
				
			||||||
        if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
 | 
					        if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for pk in pks:
 | 
					    chunks = (pks[i : i + 30] for i in range(0, len(pks), 30))
 | 
				
			||||||
        agent_update(pk)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@app.task
 | 
					 | 
				
			||||||
def sync_sysinfo_task():
 | 
					 | 
				
			||||||
    agents = Agent.objects.all()
 | 
					 | 
				
			||||||
    online = [
 | 
					 | 
				
			||||||
        i
 | 
					 | 
				
			||||||
        for i in agents
 | 
					 | 
				
			||||||
        if pyver.parse(i.version) >= pyver.parse("1.1.3") and i.status == "online"
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
    for agent in online:
 | 
					 | 
				
			||||||
        asyncio.run(agent.nats_cmd({"func": "sync"}, wait=False))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@app.task
 | 
					 | 
				
			||||||
def sync_salt_modules_task(pk):
 | 
					 | 
				
			||||||
    agent = Agent.objects.get(pk=pk)
 | 
					 | 
				
			||||||
    r = agent.salt_api_cmd(timeout=35, func="saltutil.sync_modules")
 | 
					 | 
				
			||||||
    # successful sync if new/charnged files: {'return': [{'MINION-15': ['modules.get_eventlog', 'modules.win_agent', 'etc...']}]}
 | 
					 | 
				
			||||||
    # successful sync with no new/changed files: {'return': [{'MINION-15': []}]}
 | 
					 | 
				
			||||||
    if r == "timeout" or r == "error":
 | 
					 | 
				
			||||||
        return f"Unable to sync modules {agent.salt_id}"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return f"Successfully synced salt modules on {agent.hostname}"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@app.task
 | 
					 | 
				
			||||||
def batch_sync_modules_task():
 | 
					 | 
				
			||||||
    # sync modules, split into chunks of 50 agents to not overload salt
 | 
					 | 
				
			||||||
    agents = Agent.objects.all()
 | 
					 | 
				
			||||||
    online = [i.salt_id for i in agents]
 | 
					 | 
				
			||||||
    chunks = (online[i : i + 50] for i in range(0, len(online), 50))
 | 
					 | 
				
			||||||
    for chunk in chunks:
 | 
					    for chunk in chunks:
 | 
				
			||||||
        Agent.salt_batch_async(minions=chunk, func="saltutil.sync_modules")
 | 
					        for pk in chunk:
 | 
				
			||||||
        sleep(10)
 | 
					            agent_update(pk, codesigntoken)
 | 
				
			||||||
 | 
					            sleep(0.05)
 | 
				
			||||||
 | 
					        sleep(4)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@app.task
 | 
					@app.task
 | 
				
			||||||
def uninstall_agent_task(salt_id, has_nats):
 | 
					def agent_outage_email_task(pk: int, alert_interval: Union[float, None] = None) -> str:
 | 
				
			||||||
    attempts = 0
 | 
					    from alerts.models import Alert
 | 
				
			||||||
    error = False
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if not has_nats:
 | 
					    alert = Alert.objects.get(pk=pk)
 | 
				
			||||||
        while 1:
 | 
					 | 
				
			||||||
            try:
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                r = requests.post(
 | 
					    if not alert.email_sent:
 | 
				
			||||||
                    f"http://{settings.SALT_HOST}:8123/run",
 | 
					        sleep(random.randint(1, 15))
 | 
				
			||||||
                    json=[
 | 
					        alert.agent.send_outage_email()
 | 
				
			||||||
                        {
 | 
					        alert.email_sent = djangotime.now()
 | 
				
			||||||
                            "client": "local",
 | 
					        alert.save(update_fields=["email_sent"])
 | 
				
			||||||
                            "tgt": salt_id,
 | 
					 | 
				
			||||||
                            "fun": "win_agent.uninstall_agent",
 | 
					 | 
				
			||||||
                            "timeout": 8,
 | 
					 | 
				
			||||||
                            "username": settings.SALT_USERNAME,
 | 
					 | 
				
			||||||
                            "password": settings.SALT_PASSWORD,
 | 
					 | 
				
			||||||
                            "eauth": "pam",
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    ],
 | 
					 | 
				
			||||||
                    timeout=10,
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                ret = r.json()["return"][0][salt_id]
 | 
					 | 
				
			||||||
            except Exception:
 | 
					 | 
				
			||||||
                attempts += 1
 | 
					 | 
				
			||||||
            else:
 | 
					 | 
				
			||||||
                if ret != "ok":
 | 
					 | 
				
			||||||
                    attempts += 1
 | 
					 | 
				
			||||||
                else:
 | 
					 | 
				
			||||||
                    attempts = 0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if attempts >= 10:
 | 
					 | 
				
			||||||
                error = True
 | 
					 | 
				
			||||||
                break
 | 
					 | 
				
			||||||
            elif attempts == 0:
 | 
					 | 
				
			||||||
                break
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if error:
 | 
					 | 
				
			||||||
        logger.error(f"{salt_id} uninstall failed")
 | 
					 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
        logger.info(f"{salt_id} was successfully uninstalled")
 | 
					        if alert_interval:
 | 
				
			||||||
 | 
					            # send an email only if the last email sent is older than alert interval
 | 
				
			||||||
    try:
 | 
					            delta = djangotime.now() - dt.timedelta(days=alert_interval)
 | 
				
			||||||
        r = requests.post(
 | 
					            if alert.email_sent < delta:
 | 
				
			||||||
            f"http://{settings.SALT_HOST}:8123/run",
 | 
					                sleep(random.randint(1, 10))
 | 
				
			||||||
            json=[
 | 
					                alert.agent.send_outage_email()
 | 
				
			||||||
                {
 | 
					                alert.email_sent = djangotime.now()
 | 
				
			||||||
                    "client": "wheel",
 | 
					                alert.save(update_fields=["email_sent"])
 | 
				
			||||||
                    "fun": "key.delete",
 | 
					 | 
				
			||||||
                    "match": salt_id,
 | 
					 | 
				
			||||||
                    "username": settings.SALT_USERNAME,
 | 
					 | 
				
			||||||
                    "password": settings.SALT_PASSWORD,
 | 
					 | 
				
			||||||
                    "eauth": "pam",
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            ],
 | 
					 | 
				
			||||||
            timeout=30,
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
    except Exception:
 | 
					 | 
				
			||||||
        logger.error(f"{salt_id} unable to remove salt-key")
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return "ok"
 | 
					    return "ok"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@app.task
 | 
					@app.task
 | 
				
			||||||
def agent_outage_email_task(pk):
 | 
					def agent_recovery_email_task(pk: int) -> str:
 | 
				
			||||||
 | 
					    from alerts.models import Alert
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    sleep(random.randint(1, 15))
 | 
					    sleep(random.randint(1, 15))
 | 
				
			||||||
    outage = AgentOutage.objects.get(pk=pk)
 | 
					    alert = Alert.objects.get(pk=pk)
 | 
				
			||||||
    outage.send_outage_email()
 | 
					    alert.agent.send_recovery_email()
 | 
				
			||||||
    outage.outage_email_sent = True
 | 
					    alert.resolved_email_sent = djangotime.now()
 | 
				
			||||||
    outage.save(update_fields=["outage_email_sent"])
 | 
					    alert.save(update_fields=["resolved_email_sent"])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return "ok"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@app.task
 | 
					@app.task
 | 
				
			||||||
def agent_recovery_email_task(pk):
 | 
					def agent_outage_sms_task(pk: int, alert_interval: Union[float, None] = None) -> str:
 | 
				
			||||||
    sleep(random.randint(1, 15))
 | 
					    from alerts.models import Alert
 | 
				
			||||||
    outage = AgentOutage.objects.get(pk=pk)
 | 
					
 | 
				
			||||||
    outage.send_recovery_email()
 | 
					    alert = Alert.objects.get(pk=pk)
 | 
				
			||||||
    outage.recovery_email_sent = True
 | 
					
 | 
				
			||||||
    outage.save(update_fields=["recovery_email_sent"])
 | 
					    if not alert.sms_sent:
 | 
				
			||||||
 | 
					        sleep(random.randint(1, 15))
 | 
				
			||||||
 | 
					        alert.agent.send_outage_sms()
 | 
				
			||||||
 | 
					        alert.sms_sent = djangotime.now()
 | 
				
			||||||
 | 
					        alert.save(update_fields=["sms_sent"])
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        if alert_interval:
 | 
				
			||||||
 | 
					            # send an sms only if the last sms sent is older than alert interval
 | 
				
			||||||
 | 
					            delta = djangotime.now() - dt.timedelta(days=alert_interval)
 | 
				
			||||||
 | 
					            if alert.sms_sent < delta:
 | 
				
			||||||
 | 
					                sleep(random.randint(1, 10))
 | 
				
			||||||
 | 
					                alert.agent.send_outage_sms()
 | 
				
			||||||
 | 
					                alert.sms_sent = djangotime.now()
 | 
				
			||||||
 | 
					                alert.save(update_fields=["sms_sent"])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return "ok"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@app.task
 | 
					@app.task
 | 
				
			||||||
def agent_outage_sms_task(pk):
 | 
					def agent_recovery_sms_task(pk: int) -> str:
 | 
				
			||||||
 | 
					    from alerts.models import Alert
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    sleep(random.randint(1, 3))
 | 
					    sleep(random.randint(1, 3))
 | 
				
			||||||
    outage = AgentOutage.objects.get(pk=pk)
 | 
					    alert = Alert.objects.get(pk=pk)
 | 
				
			||||||
    outage.send_outage_sms()
 | 
					    alert.agent.send_recovery_sms()
 | 
				
			||||||
    outage.outage_sms_sent = True
 | 
					    alert.resolved_sms_sent = djangotime.now()
 | 
				
			||||||
    outage.save(update_fields=["outage_sms_sent"])
 | 
					    alert.save(update_fields=["resolved_sms_sent"])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return "ok"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@app.task
 | 
					@app.task
 | 
				
			||||||
def agent_recovery_sms_task(pk):
 | 
					def agent_outages_task() -> None:
 | 
				
			||||||
    sleep(random.randint(1, 3))
 | 
					    from alerts.models import Alert
 | 
				
			||||||
    outage = AgentOutage.objects.get(pk=pk)
 | 
					 | 
				
			||||||
    outage.send_recovery_sms()
 | 
					 | 
				
			||||||
    outage.recovery_sms_sent = True
 | 
					 | 
				
			||||||
    outage.save(update_fields=["recovery_sms_sent"])
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
@app.task
 | 
					 | 
				
			||||||
def agent_outages_task():
 | 
					 | 
				
			||||||
    agents = Agent.objects.only(
 | 
					    agents = Agent.objects.only(
 | 
				
			||||||
        "pk", "last_seen", "overdue_time", "overdue_email_alert", "overdue_text_alert"
 | 
					        "pk",
 | 
				
			||||||
 | 
					        "agent_id",
 | 
				
			||||||
 | 
					        "last_seen",
 | 
				
			||||||
 | 
					        "offline_time",
 | 
				
			||||||
 | 
					        "overdue_time",
 | 
				
			||||||
 | 
					        "overdue_email_alert",
 | 
				
			||||||
 | 
					        "overdue_text_alert",
 | 
				
			||||||
 | 
					        "overdue_dashboard_alert",
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for agent in agents:
 | 
					    for agent in agents:
 | 
				
			||||||
        if agent.overdue_email_alert or agent.overdue_text_alert:
 | 
					        if agent.status == "overdue":
 | 
				
			||||||
            if agent.status == "overdue":
 | 
					            Alert.handle_alert_failure(agent)
 | 
				
			||||||
                outages = AgentOutage.objects.filter(agent=agent)
 | 
					 | 
				
			||||||
                if outages and outages.last().is_active:
 | 
					 | 
				
			||||||
                    continue
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                outage = AgentOutage(agent=agent)
 | 
					 | 
				
			||||||
                outage.save()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if agent.overdue_email_alert and not agent.maintenance_mode:
 | 
					@app.task
 | 
				
			||||||
                    agent_outage_email_task.delay(pk=outage.pk)
 | 
					def run_script_email_results_task(
 | 
				
			||||||
 | 
					    agentpk: int,
 | 
				
			||||||
 | 
					    scriptpk: int,
 | 
				
			||||||
 | 
					    nats_timeout: int,
 | 
				
			||||||
 | 
					    emails: list[str],
 | 
				
			||||||
 | 
					    args: list[str] = [],
 | 
				
			||||||
 | 
					):
 | 
				
			||||||
 | 
					    agent = Agent.objects.get(pk=agentpk)
 | 
				
			||||||
 | 
					    script = Script.objects.get(pk=scriptpk)
 | 
				
			||||||
 | 
					    r = agent.run_script(
 | 
				
			||||||
 | 
					        scriptpk=script.pk, args=args, full=True, timeout=nats_timeout, wait=True
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    if r == "timeout":
 | 
				
			||||||
 | 
					        logger.error(f"{agent.hostname} timed out running script.")
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if agent.overdue_text_alert and not agent.maintenance_mode:
 | 
					    CORE = CoreSettings.objects.first()
 | 
				
			||||||
                    agent_outage_sms_task.delay(pk=outage.pk)
 | 
					    subject = f"{agent.hostname} {script.name} Results"
 | 
				
			||||||
 | 
					    exec_time = "{:.4f}".format(r["execution_time"])
 | 
				
			||||||
 | 
					    body = (
 | 
				
			||||||
 | 
					        subject
 | 
				
			||||||
 | 
					        + f"\nReturn code: {r['retcode']}\nExecution time: {exec_time} seconds\nStdout: {r['stdout']}\nStderr: {r['stderr']}"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    import smtplib
 | 
				
			||||||
 | 
					    from email.message import EmailMessage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    msg = EmailMessage()
 | 
				
			||||||
 | 
					    msg["Subject"] = subject
 | 
				
			||||||
 | 
					    msg["From"] = CORE.smtp_from_email
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if emails:
 | 
				
			||||||
 | 
					        msg["To"] = ", ".join(emails)
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        msg["To"] = ", ".join(CORE.email_alert_recipients)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    msg.set_content(body)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        with smtplib.SMTP(CORE.smtp_host, CORE.smtp_port, timeout=20) as server:
 | 
				
			||||||
 | 
					            if CORE.smtp_requires_auth:
 | 
				
			||||||
 | 
					                server.ehlo()
 | 
				
			||||||
 | 
					                server.starttls()
 | 
				
			||||||
 | 
					                server.login(CORE.smtp_host_user, CORE.smtp_host_password)
 | 
				
			||||||
 | 
					                server.send_message(msg)
 | 
				
			||||||
 | 
					                server.quit()
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                server.send_message(msg)
 | 
				
			||||||
 | 
					                server.quit()
 | 
				
			||||||
 | 
					    except Exception as e:
 | 
				
			||||||
 | 
					        logger.error(e)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.task
 | 
				
			||||||
 | 
					def clear_faults_task(older_than_days: int) -> None:
 | 
				
			||||||
 | 
					    # https://github.com/wh1te909/tacticalrmm/issues/484
 | 
				
			||||||
 | 
					    agents = Agent.objects.exclude(last_seen__isnull=True).filter(
 | 
				
			||||||
 | 
					        last_seen__lt=djangotime.now() - djangotime.timedelta(days=older_than_days)
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    for agent in agents:
 | 
				
			||||||
 | 
					        if agent.agentchecks.exists():
 | 
				
			||||||
 | 
					            for check in agent.agentchecks.all():
 | 
				
			||||||
 | 
					                # reset check status
 | 
				
			||||||
 | 
					                check.status = "passing"
 | 
				
			||||||
 | 
					                check.save(update_fields=["status"])
 | 
				
			||||||
 | 
					                if check.alert.filter(resolved=False).exists():
 | 
				
			||||||
 | 
					                    check.alert.get(resolved=False).resolve()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # reset overdue alerts
 | 
				
			||||||
 | 
					        agent.overdue_email_alert = False
 | 
				
			||||||
 | 
					        agent.overdue_text_alert = False
 | 
				
			||||||
 | 
					        agent.overdue_dashboard_alert = False
 | 
				
			||||||
 | 
					        agent.save(
 | 
				
			||||||
 | 
					            update_fields=[
 | 
				
			||||||
 | 
					                "overdue_email_alert",
 | 
				
			||||||
 | 
					                "overdue_text_alert",
 | 
				
			||||||
 | 
					                "overdue_dashboard_alert",
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.task
 | 
				
			||||||
 | 
					def monitor_agents_task() -> None:
 | 
				
			||||||
 | 
					    agents = Agent.objects.only(
 | 
				
			||||||
 | 
					        "pk", "agent_id", "last_seen", "overdue_time", "offline_time"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    ids = [i.agent_id for i in agents if i.status != "online"]
 | 
				
			||||||
 | 
					    run_nats_api_cmd("monitor", ids)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.task
 | 
				
			||||||
 | 
					def get_wmi_task() -> None:
 | 
				
			||||||
 | 
					    agents = Agent.objects.only(
 | 
				
			||||||
 | 
					        "pk", "agent_id", "last_seen", "overdue_time", "offline_time"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    ids = [i.agent_id for i in agents if i.status == "online"]
 | 
				
			||||||
 | 
					    run_nats_api_cmd("wmi", ids)
 | 
				
			||||||
 
 | 
				
			|||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -1,12 +1,11 @@
 | 
				
			|||||||
from django.urls import path
 | 
					from django.urls import path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from . import views
 | 
					from . import views
 | 
				
			||||||
 | 
					
 | 
				
			||||||
urlpatterns = [
 | 
					urlpatterns = [
 | 
				
			||||||
    path("listagents/", views.AgentsTableList.as_view()),
 | 
					    path("listagents/", views.AgentsTableList.as_view()),
 | 
				
			||||||
    path("listagentsnodetail/", views.list_agents_no_detail),
 | 
					    path("listagentsnodetail/", views.list_agents_no_detail),
 | 
				
			||||||
    path("<int:pk>/agenteditdetails/", views.agent_edit_details),
 | 
					    path("<int:pk>/agenteditdetails/", views.agent_edit_details),
 | 
				
			||||||
    path("byclient/<int:clientpk>/", views.by_client),
 | 
					 | 
				
			||||||
    path("bysite/<int:sitepk>/", views.by_site),
 | 
					 | 
				
			||||||
    path("overdueaction/", views.overdue_action),
 | 
					    path("overdueaction/", views.overdue_action),
 | 
				
			||||||
    path("sendrawcmd/", views.send_raw_cmd),
 | 
					    path("sendrawcmd/", views.send_raw_cmd),
 | 
				
			||||||
    path("<pk>/agentdetail/", views.agent_detail),
 | 
					    path("<pk>/agentdetail/", views.agent_detail),
 | 
				
			||||||
@@ -28,7 +27,6 @@ urlpatterns = [
 | 
				
			|||||||
    path("<int:pk>/notes/", views.GetAddNotes.as_view()),
 | 
					    path("<int:pk>/notes/", views.GetAddNotes.as_view()),
 | 
				
			||||||
    path("<int:pk>/note/", views.GetEditDeleteNote.as_view()),
 | 
					    path("<int:pk>/note/", views.GetEditDeleteNote.as_view()),
 | 
				
			||||||
    path("bulk/", views.bulk),
 | 
					    path("bulk/", views.bulk),
 | 
				
			||||||
    path("agent_counts/", views.agent_counts),
 | 
					 | 
				
			||||||
    path("maintenance/", views.agent_maintenance),
 | 
					    path("maintenance/", views.agent_maintenance),
 | 
				
			||||||
    path("<int:pk>/wmi/", views.WMI.as_view()),
 | 
					    path("<int:pk>/wmi/", views.WMI.as_view()),
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										37
									
								
								api/tacticalrmm/agents/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								api/tacticalrmm/agents/utils.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
				
			|||||||
 | 
					import random
 | 
				
			||||||
 | 
					import urllib.parse
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import requests
 | 
				
			||||||
 | 
					from django.conf import settings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_exegen_url() -> str:
 | 
				
			||||||
 | 
					    urls: list[str] = settings.EXE_GEN_URLS
 | 
				
			||||||
 | 
					    for url in urls:
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            r = requests.get(url, timeout=10)
 | 
				
			||||||
 | 
					        except:
 | 
				
			||||||
 | 
					            continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if r.status_code == 200:
 | 
				
			||||||
 | 
					            return url
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return random.choice(urls)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_winagent_url(arch: str) -> str:
 | 
				
			||||||
 | 
					    from core.models import CodeSignToken
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        codetoken = CodeSignToken.objects.first().token
 | 
				
			||||||
 | 
					        base_url = get_exegen_url() + "/api/v1/winagents/?"
 | 
				
			||||||
 | 
					        params = {
 | 
				
			||||||
 | 
					            "version": settings.LATEST_AGENT_VER,
 | 
				
			||||||
 | 
					            "arch": arch,
 | 
				
			||||||
 | 
					            "token": codetoken,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        dl_url = base_url + urllib.parse.urlencode(params)
 | 
				
			||||||
 | 
					    except:
 | 
				
			||||||
 | 
					        dl_url = settings.DL_64 if arch == "64" else settings.DL_32
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return dl_url
 | 
				
			||||||
@@ -1,49 +1,62 @@
 | 
				
			|||||||
import asyncio
 | 
					import asyncio
 | 
				
			||||||
from loguru import logger
 | 
					import datetime as dt
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
import subprocess
 | 
					 | 
				
			||||||
import pytz
 | 
					 | 
				
			||||||
import random
 | 
					import random
 | 
				
			||||||
import string
 | 
					import string
 | 
				
			||||||
import datetime as dt
 | 
					import time
 | 
				
			||||||
from packaging import version as pyver
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.conf import settings
 | 
					from django.conf import settings
 | 
				
			||||||
from django.shortcuts import get_object_or_404
 | 
					 | 
				
			||||||
from django.http import HttpResponse
 | 
					from django.http import HttpResponse
 | 
				
			||||||
 | 
					from django.shortcuts import get_object_or_404
 | 
				
			||||||
from rest_framework.decorators import api_view
 | 
					from loguru import logger
 | 
				
			||||||
from rest_framework.views import APIView
 | 
					from packaging import version as pyver
 | 
				
			||||||
 | 
					from rest_framework import status
 | 
				
			||||||
 | 
					from rest_framework.decorators import api_view, permission_classes
 | 
				
			||||||
 | 
					from rest_framework.permissions import IsAuthenticated
 | 
				
			||||||
from rest_framework.response import Response
 | 
					from rest_framework.response import Response
 | 
				
			||||||
from rest_framework import status, generics
 | 
					from rest_framework.views import APIView
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .models import Agent, AgentOutage, RecoveryAction, Note
 | 
					 | 
				
			||||||
from core.models import CoreSettings
 | 
					from core.models import CoreSettings
 | 
				
			||||||
from scripts.models import Script
 | 
					 | 
				
			||||||
from logs.models import AuditLog, PendingAction
 | 
					from logs.models import AuditLog, PendingAction
 | 
				
			||||||
 | 
					from scripts.models import Script
 | 
				
			||||||
 | 
					from scripts.tasks import handle_bulk_command_task, handle_bulk_script_task
 | 
				
			||||||
 | 
					from tacticalrmm.utils import get_default_timezone, notify_error, reload_nats
 | 
				
			||||||
 | 
					from winupdate.serializers import WinUpdatePolicySerializer
 | 
				
			||||||
 | 
					from winupdate.tasks import bulk_check_for_updates_task, bulk_install_updates_task
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .models import Agent, AgentCustomField, Note, RecoveryAction
 | 
				
			||||||
 | 
					from .permissions import (
 | 
				
			||||||
 | 
					    EditAgentPerms,
 | 
				
			||||||
 | 
					    EvtLogPerms,
 | 
				
			||||||
 | 
					    InstallAgentPerms,
 | 
				
			||||||
 | 
					    ManageNotesPerms,
 | 
				
			||||||
 | 
					    ManageProcPerms,
 | 
				
			||||||
 | 
					    MeshPerms,
 | 
				
			||||||
 | 
					    RebootAgentPerms,
 | 
				
			||||||
 | 
					    RunBulkPerms,
 | 
				
			||||||
 | 
					    RunScriptPerms,
 | 
				
			||||||
 | 
					    SendCMDPerms,
 | 
				
			||||||
 | 
					    UninstallPerms,
 | 
				
			||||||
 | 
					    UpdateAgentPerms,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
from .serializers import (
 | 
					from .serializers import (
 | 
				
			||||||
    AgentSerializer,
 | 
					    AgentCustomFieldSerializer,
 | 
				
			||||||
    AgentHostnameSerializer,
 | 
					 | 
				
			||||||
    AgentTableSerializer,
 | 
					 | 
				
			||||||
    AgentEditSerializer,
 | 
					    AgentEditSerializer,
 | 
				
			||||||
 | 
					    AgentHostnameSerializer,
 | 
				
			||||||
 | 
					    AgentOverdueActionSerializer,
 | 
				
			||||||
 | 
					    AgentSerializer,
 | 
				
			||||||
 | 
					    AgentTableSerializer,
 | 
				
			||||||
    NoteSerializer,
 | 
					    NoteSerializer,
 | 
				
			||||||
    NotesSerializer,
 | 
					    NotesSerializer,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
from winupdate.serializers import WinUpdatePolicySerializer
 | 
					from .tasks import run_script_email_results_task, send_agent_update_task
 | 
				
			||||||
 | 
					 | 
				
			||||||
from .tasks import uninstall_agent_task, send_agent_update_task
 | 
					 | 
				
			||||||
from winupdate.tasks import bulk_check_for_updates_task
 | 
					 | 
				
			||||||
from scripts.tasks import handle_bulk_command_task, handle_bulk_script_task
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from tacticalrmm.utils import notify_error, reload_nats
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
logger.configure(**settings.LOG_CONFIG)
 | 
					logger.configure(**settings.LOG_CONFIG)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@api_view()
 | 
					@api_view()
 | 
				
			||||||
def get_agent_versions(request):
 | 
					def get_agent_versions(request):
 | 
				
			||||||
    agents = Agent.objects.only("pk")
 | 
					    agents = Agent.objects.prefetch_related("site").only("pk", "hostname")
 | 
				
			||||||
    return Response(
 | 
					    return Response(
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            "versions": [settings.LATEST_AGENT_VER],
 | 
					            "versions": [settings.LATEST_AGENT_VER],
 | 
				
			||||||
@@ -53,70 +66,95 @@ def get_agent_versions(request):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@api_view(["POST"])
 | 
					@api_view(["POST"])
 | 
				
			||||||
 | 
					@permission_classes([IsAuthenticated, UpdateAgentPerms])
 | 
				
			||||||
def update_agents(request):
 | 
					def update_agents(request):
 | 
				
			||||||
    pks = request.data["pks"]
 | 
					    q = Agent.objects.filter(pk__in=request.data["pks"]).only("pk", "version")
 | 
				
			||||||
    version = request.data["version"]
 | 
					    pks: list[int] = [
 | 
				
			||||||
    send_agent_update_task.delay(pks=pks, version=version)
 | 
					        i.pk
 | 
				
			||||||
 | 
					        for i in q
 | 
				
			||||||
 | 
					        if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					    send_agent_update_task.delay(pks=pks)
 | 
				
			||||||
    return Response("ok")
 | 
					    return Response("ok")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@api_view()
 | 
					@api_view()
 | 
				
			||||||
 | 
					@permission_classes([IsAuthenticated, UninstallPerms])
 | 
				
			||||||
def ping(request, pk):
 | 
					def ping(request, pk):
 | 
				
			||||||
    agent = get_object_or_404(Agent, pk=pk)
 | 
					    agent = get_object_or_404(Agent, pk=pk)
 | 
				
			||||||
    status = "offline"
 | 
					    status = "offline"
 | 
				
			||||||
    if agent.has_nats:
 | 
					    attempts = 0
 | 
				
			||||||
        r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=5))
 | 
					    while 1:
 | 
				
			||||||
 | 
					        r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=2))
 | 
				
			||||||
        if r == "pong":
 | 
					        if r == "pong":
 | 
				
			||||||
            status = "online"
 | 
					            status = "online"
 | 
				
			||||||
    else:
 | 
					            break
 | 
				
			||||||
        r = agent.salt_api_cmd(timeout=5, func="test.ping")
 | 
					        else:
 | 
				
			||||||
        if isinstance(r, bool) and r:
 | 
					            attempts += 1
 | 
				
			||||||
            status = "online"
 | 
					            time.sleep(1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if attempts >= 5:
 | 
				
			||||||
 | 
					            break
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Response({"name": agent.hostname, "status": status})
 | 
					    return Response({"name": agent.hostname, "status": status})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@api_view(["DELETE"])
 | 
					@api_view(["DELETE"])
 | 
				
			||||||
 | 
					@permission_classes([IsAuthenticated, UninstallPerms])
 | 
				
			||||||
def uninstall(request):
 | 
					def uninstall(request):
 | 
				
			||||||
    agent = get_object_or_404(Agent, pk=request.data["pk"])
 | 
					    agent = get_object_or_404(Agent, pk=request.data["pk"])
 | 
				
			||||||
    if agent.has_nats:
 | 
					    asyncio.run(agent.nats_cmd({"func": "uninstall"}, wait=False))
 | 
				
			||||||
        asyncio.run(agent.nats_cmd({"func": "uninstall"}, wait=False))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    salt_id = agent.salt_id
 | 
					 | 
				
			||||||
    name = agent.hostname
 | 
					    name = agent.hostname
 | 
				
			||||||
    has_nats = agent.has_nats
 | 
					 | 
				
			||||||
    agent.delete()
 | 
					    agent.delete()
 | 
				
			||||||
    reload_nats()
 | 
					    reload_nats()
 | 
				
			||||||
 | 
					 | 
				
			||||||
    uninstall_agent_task.delay(salt_id, has_nats)
 | 
					 | 
				
			||||||
    return Response(f"{name} will now be uninstalled.")
 | 
					    return Response(f"{name} will now be uninstalled.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@api_view(["PATCH"])
 | 
					@api_view(["PATCH", "PUT"])
 | 
				
			||||||
 | 
					@permission_classes([IsAuthenticated, EditAgentPerms])
 | 
				
			||||||
def edit_agent(request):
 | 
					def edit_agent(request):
 | 
				
			||||||
    agent = get_object_or_404(Agent, pk=request.data["id"])
 | 
					    agent = get_object_or_404(Agent, pk=request.data["id"])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    old_site = agent.site.pk
 | 
					 | 
				
			||||||
    a_serializer = AgentSerializer(instance=agent, data=request.data, partial=True)
 | 
					    a_serializer = AgentSerializer(instance=agent, data=request.data, partial=True)
 | 
				
			||||||
    a_serializer.is_valid(raise_exception=True)
 | 
					    a_serializer.is_valid(raise_exception=True)
 | 
				
			||||||
    a_serializer.save()
 | 
					    a_serializer.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    policy = agent.winupdatepolicy.get()
 | 
					    if "winupdatepolicy" in request.data.keys():
 | 
				
			||||||
    p_serializer = WinUpdatePolicySerializer(
 | 
					        policy = agent.winupdatepolicy.get()  # type: ignore
 | 
				
			||||||
        instance=policy, data=request.data["winupdatepolicy"][0]
 | 
					        p_serializer = WinUpdatePolicySerializer(
 | 
				
			||||||
    )
 | 
					            instance=policy, data=request.data["winupdatepolicy"][0]
 | 
				
			||||||
    p_serializer.is_valid(raise_exception=True)
 | 
					        )
 | 
				
			||||||
    p_serializer.save()
 | 
					        p_serializer.is_valid(raise_exception=True)
 | 
				
			||||||
 | 
					        p_serializer.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # check if site changed and initiate generating correct policies
 | 
					    if "custom_fields" in request.data.keys():
 | 
				
			||||||
    if old_site != request.data["site"]:
 | 
					
 | 
				
			||||||
        agent.generate_checks_from_policies(clear=True)
 | 
					        for field in request.data["custom_fields"]:
 | 
				
			||||||
        agent.generate_tasks_from_policies(clear=True)
 | 
					
 | 
				
			||||||
 | 
					            custom_field = field
 | 
				
			||||||
 | 
					            custom_field["agent"] = agent.id  # type: ignore
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if AgentCustomField.objects.filter(
 | 
				
			||||||
 | 
					                field=field["field"], agent=agent.id  # type: ignore
 | 
				
			||||||
 | 
					            ):
 | 
				
			||||||
 | 
					                value = AgentCustomField.objects.get(
 | 
				
			||||||
 | 
					                    field=field["field"], agent=agent.id  # type: ignore
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                serializer = AgentCustomFieldSerializer(
 | 
				
			||||||
 | 
					                    instance=value, data=custom_field
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                serializer.is_valid(raise_exception=True)
 | 
				
			||||||
 | 
					                serializer.save()
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                serializer = AgentCustomFieldSerializer(data=custom_field)
 | 
				
			||||||
 | 
					                serializer.is_valid(raise_exception=True)
 | 
				
			||||||
 | 
					                serializer.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Response("ok")
 | 
					    return Response("ok")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@api_view()
 | 
					@api_view()
 | 
				
			||||||
 | 
					@permission_classes([IsAuthenticated, MeshPerms])
 | 
				
			||||||
def meshcentral(request, pk):
 | 
					def meshcentral(request, pk):
 | 
				
			||||||
    agent = get_object_or_404(Agent, pk=pk)
 | 
					    agent = get_object_or_404(Agent, pk=pk)
 | 
				
			||||||
    core = CoreSettings.objects.first()
 | 
					    core = CoreSettings.objects.first()
 | 
				
			||||||
@@ -155,21 +193,16 @@ def agent_detail(request, pk):
 | 
				
			|||||||
@api_view()
 | 
					@api_view()
 | 
				
			||||||
def get_processes(request, pk):
 | 
					def get_processes(request, pk):
 | 
				
			||||||
    agent = get_object_or_404(Agent, pk=pk)
 | 
					    agent = get_object_or_404(Agent, pk=pk)
 | 
				
			||||||
    if not agent.has_nats:
 | 
					 | 
				
			||||||
        return notify_error("Requires agent version 1.1.0 or greater")
 | 
					 | 
				
			||||||
    r = asyncio.run(agent.nats_cmd(data={"func": "procs"}, timeout=5))
 | 
					    r = asyncio.run(agent.nats_cmd(data={"func": "procs"}, timeout=5))
 | 
				
			||||||
    if r == "timeout":
 | 
					    if r == "timeout":
 | 
				
			||||||
        return notify_error("Unable to contact the agent")
 | 
					        return notify_error("Unable to contact the agent")
 | 
				
			||||||
 | 
					 | 
				
			||||||
    return Response(r)
 | 
					    return Response(r)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@api_view()
 | 
					@api_view()
 | 
				
			||||||
 | 
					@permission_classes([IsAuthenticated, ManageProcPerms])
 | 
				
			||||||
def kill_proc(request, pk, pid):
 | 
					def kill_proc(request, pk, pid):
 | 
				
			||||||
    agent = get_object_or_404(Agent, pk=pk)
 | 
					    agent = get_object_or_404(Agent, pk=pk)
 | 
				
			||||||
    if not agent.has_nats:
 | 
					 | 
				
			||||||
        return notify_error("Requires agent version 1.1.0 or greater")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    r = asyncio.run(
 | 
					    r = asyncio.run(
 | 
				
			||||||
        agent.nats_cmd({"func": "killproc", "procpid": int(pid)}, timeout=15)
 | 
					        agent.nats_cmd({"func": "killproc", "procpid": int(pid)}, timeout=15)
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
@@ -183,19 +216,19 @@ def kill_proc(request, pk, pid):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@api_view()
 | 
					@api_view()
 | 
				
			||||||
 | 
					@permission_classes([IsAuthenticated, EvtLogPerms])
 | 
				
			||||||
def get_event_log(request, pk, logtype, days):
 | 
					def get_event_log(request, pk, logtype, days):
 | 
				
			||||||
    agent = get_object_or_404(Agent, pk=pk)
 | 
					    agent = get_object_or_404(Agent, pk=pk)
 | 
				
			||||||
    if not agent.has_nats:
 | 
					    timeout = 180 if logtype == "Security" else 30
 | 
				
			||||||
        return notify_error("Requires agent version 1.1.0 or greater")
 | 
					 | 
				
			||||||
    data = {
 | 
					    data = {
 | 
				
			||||||
        "func": "eventlog",
 | 
					        "func": "eventlog",
 | 
				
			||||||
        "timeout": 30,
 | 
					        "timeout": timeout,
 | 
				
			||||||
        "payload": {
 | 
					        "payload": {
 | 
				
			||||||
            "logname": logtype,
 | 
					            "logname": logtype,
 | 
				
			||||||
            "days": str(days),
 | 
					            "days": str(days),
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    r = asyncio.run(agent.nats_cmd(data, timeout=32))
 | 
					    r = asyncio.run(agent.nats_cmd(data, timeout=timeout + 2))
 | 
				
			||||||
    if r == "timeout":
 | 
					    if r == "timeout":
 | 
				
			||||||
        return notify_error("Unable to contact the agent")
 | 
					        return notify_error("Unable to contact the agent")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -203,10 +236,9 @@ def get_event_log(request, pk, logtype, days):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@api_view(["POST"])
 | 
					@api_view(["POST"])
 | 
				
			||||||
 | 
					@permission_classes([IsAuthenticated, SendCMDPerms])
 | 
				
			||||||
def send_raw_cmd(request):
 | 
					def send_raw_cmd(request):
 | 
				
			||||||
    agent = get_object_or_404(Agent, pk=request.data["pk"])
 | 
					    agent = get_object_or_404(Agent, pk=request.data["pk"])
 | 
				
			||||||
    if not agent.has_nats:
 | 
					 | 
				
			||||||
        return notify_error("Requires agent version 1.1.0 or greater")
 | 
					 | 
				
			||||||
    timeout = int(request.data["timeout"])
 | 
					    timeout = int(request.data["timeout"])
 | 
				
			||||||
    data = {
 | 
					    data = {
 | 
				
			||||||
        "func": "rawcmd",
 | 
					        "func": "rawcmd",
 | 
				
			||||||
@@ -231,21 +263,39 @@ def send_raw_cmd(request):
 | 
				
			|||||||
    return Response(r)
 | 
					    return Response(r)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AgentsTableList(generics.ListAPIView):
 | 
					class AgentsTableList(APIView):
 | 
				
			||||||
    queryset = (
 | 
					    def patch(self, request):
 | 
				
			||||||
        Agent.objects.select_related("site")
 | 
					        if "sitePK" in request.data.keys():
 | 
				
			||||||
        .prefetch_related("agentchecks")
 | 
					            queryset = (
 | 
				
			||||||
        .only(
 | 
					                Agent.objects.select_related("site", "policy", "alert_template")
 | 
				
			||||||
 | 
					                .prefetch_related("agentchecks")
 | 
				
			||||||
 | 
					                .filter(site_id=request.data["sitePK"])
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        elif "clientPK" in request.data.keys():
 | 
				
			||||||
 | 
					            queryset = (
 | 
				
			||||||
 | 
					                Agent.objects.select_related("site", "policy", "alert_template")
 | 
				
			||||||
 | 
					                .prefetch_related("agentchecks")
 | 
				
			||||||
 | 
					                .filter(site__client_id=request.data["clientPK"])
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            queryset = Agent.objects.select_related(
 | 
				
			||||||
 | 
					                "site", "policy", "alert_template"
 | 
				
			||||||
 | 
					            ).prefetch_related("agentchecks")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        queryset = queryset.only(
 | 
				
			||||||
            "pk",
 | 
					            "pk",
 | 
				
			||||||
            "hostname",
 | 
					            "hostname",
 | 
				
			||||||
            "agent_id",
 | 
					            "agent_id",
 | 
				
			||||||
            "site",
 | 
					            "site",
 | 
				
			||||||
 | 
					            "policy",
 | 
				
			||||||
 | 
					            "alert_template",
 | 
				
			||||||
            "monitoring_type",
 | 
					            "monitoring_type",
 | 
				
			||||||
            "description",
 | 
					            "description",
 | 
				
			||||||
            "needs_reboot",
 | 
					            "needs_reboot",
 | 
				
			||||||
            "overdue_text_alert",
 | 
					            "overdue_text_alert",
 | 
				
			||||||
            "overdue_email_alert",
 | 
					            "overdue_email_alert",
 | 
				
			||||||
            "overdue_time",
 | 
					            "overdue_time",
 | 
				
			||||||
 | 
					            "offline_time",
 | 
				
			||||||
            "last_seen",
 | 
					            "last_seen",
 | 
				
			||||||
            "boot_time",
 | 
					            "boot_time",
 | 
				
			||||||
            "logged_in_username",
 | 
					            "logged_in_username",
 | 
				
			||||||
@@ -253,14 +303,7 @@ class AgentsTableList(generics.ListAPIView):
 | 
				
			|||||||
            "time_zone",
 | 
					            "time_zone",
 | 
				
			||||||
            "maintenance_mode",
 | 
					            "maintenance_mode",
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
    )
 | 
					        ctx = {"default_tz": get_default_timezone()}
 | 
				
			||||||
    serializer_class = AgentTableSerializer
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def list(self, request):
 | 
					 | 
				
			||||||
        queryset = self.get_queryset()
 | 
					 | 
				
			||||||
        ctx = {
 | 
					 | 
				
			||||||
            "default_tz": pytz.timezone(CoreSettings.objects.first().default_time_zone)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        serializer = AgentTableSerializer(queryset, many=True, context=ctx)
 | 
					        serializer = AgentTableSerializer(queryset, many=True, context=ctx)
 | 
				
			||||||
        return Response(serializer.data)
 | 
					        return Response(serializer.data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -277,96 +320,22 @@ def agent_edit_details(request, pk):
 | 
				
			|||||||
    return Response(AgentEditSerializer(agent).data)
 | 
					    return Response(AgentEditSerializer(agent).data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@api_view()
 | 
					 | 
				
			||||||
def by_client(request, clientpk):
 | 
					 | 
				
			||||||
    agents = (
 | 
					 | 
				
			||||||
        Agent.objects.select_related("site")
 | 
					 | 
				
			||||||
        .filter(site__client_id=clientpk)
 | 
					 | 
				
			||||||
        .prefetch_related("agentchecks")
 | 
					 | 
				
			||||||
        .only(
 | 
					 | 
				
			||||||
            "pk",
 | 
					 | 
				
			||||||
            "hostname",
 | 
					 | 
				
			||||||
            "agent_id",
 | 
					 | 
				
			||||||
            "site",
 | 
					 | 
				
			||||||
            "monitoring_type",
 | 
					 | 
				
			||||||
            "description",
 | 
					 | 
				
			||||||
            "needs_reboot",
 | 
					 | 
				
			||||||
            "overdue_text_alert",
 | 
					 | 
				
			||||||
            "overdue_email_alert",
 | 
					 | 
				
			||||||
            "overdue_time",
 | 
					 | 
				
			||||||
            "last_seen",
 | 
					 | 
				
			||||||
            "boot_time",
 | 
					 | 
				
			||||||
            "logged_in_username",
 | 
					 | 
				
			||||||
            "last_logged_in_user",
 | 
					 | 
				
			||||||
            "time_zone",
 | 
					 | 
				
			||||||
            "maintenance_mode",
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    ctx = {"default_tz": pytz.timezone(CoreSettings.objects.first().default_time_zone)}
 | 
					 | 
				
			||||||
    return Response(AgentTableSerializer(agents, many=True, context=ctx).data)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@api_view()
 | 
					 | 
				
			||||||
def by_site(request, sitepk):
 | 
					 | 
				
			||||||
    agents = (
 | 
					 | 
				
			||||||
        Agent.objects.filter(site_id=sitepk)
 | 
					 | 
				
			||||||
        .select_related("site")
 | 
					 | 
				
			||||||
        .prefetch_related("agentchecks")
 | 
					 | 
				
			||||||
        .only(
 | 
					 | 
				
			||||||
            "pk",
 | 
					 | 
				
			||||||
            "hostname",
 | 
					 | 
				
			||||||
            "agent_id",
 | 
					 | 
				
			||||||
            "site",
 | 
					 | 
				
			||||||
            "monitoring_type",
 | 
					 | 
				
			||||||
            "description",
 | 
					 | 
				
			||||||
            "needs_reboot",
 | 
					 | 
				
			||||||
            "overdue_text_alert",
 | 
					 | 
				
			||||||
            "overdue_email_alert",
 | 
					 | 
				
			||||||
            "overdue_time",
 | 
					 | 
				
			||||||
            "last_seen",
 | 
					 | 
				
			||||||
            "boot_time",
 | 
					 | 
				
			||||||
            "logged_in_username",
 | 
					 | 
				
			||||||
            "last_logged_in_user",
 | 
					 | 
				
			||||||
            "time_zone",
 | 
					 | 
				
			||||||
            "maintenance_mode",
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    ctx = {"default_tz": pytz.timezone(CoreSettings.objects.first().default_time_zone)}
 | 
					 | 
				
			||||||
    return Response(AgentTableSerializer(agents, many=True, context=ctx).data)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@api_view(["POST"])
 | 
					@api_view(["POST"])
 | 
				
			||||||
def overdue_action(request):
 | 
					def overdue_action(request):
 | 
				
			||||||
    pk = request.data["pk"]
 | 
					    agent = get_object_or_404(Agent, pk=request.data["pk"])
 | 
				
			||||||
    alert_type = request.data["alertType"]
 | 
					    serializer = AgentOverdueActionSerializer(
 | 
				
			||||||
    action = request.data["action"]
 | 
					        instance=agent, data=request.data, partial=True
 | 
				
			||||||
    agent = get_object_or_404(Agent, pk=pk)
 | 
					    )
 | 
				
			||||||
    if alert_type == "email" and action == "enabled":
 | 
					    serializer.is_valid(raise_exception=True)
 | 
				
			||||||
        agent.overdue_email_alert = True
 | 
					    serializer.save()
 | 
				
			||||||
        agent.save(update_fields=["overdue_email_alert"])
 | 
					 | 
				
			||||||
    elif alert_type == "email" and action == "disabled":
 | 
					 | 
				
			||||||
        agent.overdue_email_alert = False
 | 
					 | 
				
			||||||
        agent.save(update_fields=["overdue_email_alert"])
 | 
					 | 
				
			||||||
    elif alert_type == "text" and action == "enabled":
 | 
					 | 
				
			||||||
        agent.overdue_text_alert = True
 | 
					 | 
				
			||||||
        agent.save(update_fields=["overdue_text_alert"])
 | 
					 | 
				
			||||||
    elif alert_type == "text" and action == "disabled":
 | 
					 | 
				
			||||||
        agent.overdue_text_alert = False
 | 
					 | 
				
			||||||
        agent.save(update_fields=["overdue_text_alert"])
 | 
					 | 
				
			||||||
    else:
 | 
					 | 
				
			||||||
        return Response(
 | 
					 | 
				
			||||||
            {"error": "Something went wrong"}, status=status.HTTP_400_BAD_REQUEST
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
    return Response(agent.hostname)
 | 
					    return Response(agent.hostname)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Reboot(APIView):
 | 
					class Reboot(APIView):
 | 
				
			||||||
 | 
					    permission_classes = [IsAuthenticated, RebootAgentPerms]
 | 
				
			||||||
    # reboot now
 | 
					    # reboot now
 | 
				
			||||||
    def post(self, request):
 | 
					    def post(self, request):
 | 
				
			||||||
        agent = get_object_or_404(Agent, pk=request.data["pk"])
 | 
					        agent = get_object_or_404(Agent, pk=request.data["pk"])
 | 
				
			||||||
        if not agent.has_nats:
 | 
					 | 
				
			||||||
            return notify_error("Requires agent version 1.1.0 or greater")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        r = asyncio.run(agent.nats_cmd({"func": "rebootnow"}, timeout=10))
 | 
					        r = asyncio.run(agent.nats_cmd({"func": "rebootnow"}, timeout=10))
 | 
				
			||||||
        if r != "ok":
 | 
					        if r != "ok":
 | 
				
			||||||
            return notify_error("Unable to contact the agent")
 | 
					            return notify_error("Unable to contact the agent")
 | 
				
			||||||
@@ -376,8 +345,6 @@ class Reboot(APIView):
 | 
				
			|||||||
    # reboot later
 | 
					    # reboot later
 | 
				
			||||||
    def patch(self, request):
 | 
					    def patch(self, request):
 | 
				
			||||||
        agent = get_object_or_404(Agent, pk=request.data["pk"])
 | 
					        agent = get_object_or_404(Agent, pk=request.data["pk"])
 | 
				
			||||||
        if not agent.has_gotasks:
 | 
					 | 
				
			||||||
            return notify_error("Requires agent version 1.1.1 or greater")
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            obj = dt.datetime.strptime(request.data["datetime"], "%Y-%m-%d %H:%M")
 | 
					            obj = dt.datetime.strptime(request.data["datetime"], "%Y-%m-%d %H:%M")
 | 
				
			||||||
@@ -392,6 +359,7 @@ class Reboot(APIView):
 | 
				
			|||||||
            "func": "schedtask",
 | 
					            "func": "schedtask",
 | 
				
			||||||
            "schedtaskpayload": {
 | 
					            "schedtaskpayload": {
 | 
				
			||||||
                "type": "schedreboot",
 | 
					                "type": "schedreboot",
 | 
				
			||||||
 | 
					                "deleteafter": True,
 | 
				
			||||||
                "trigger": "once",
 | 
					                "trigger": "once",
 | 
				
			||||||
                "name": task_name,
 | 
					                "name": task_name,
 | 
				
			||||||
                "year": int(dt.datetime.strftime(obj, "%Y")),
 | 
					                "year": int(dt.datetime.strftime(obj, "%Y")),
 | 
				
			||||||
@@ -402,9 +370,6 @@ class Reboot(APIView):
 | 
				
			|||||||
            },
 | 
					            },
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if pyver.parse(agent.version) >= pyver.parse("1.1.2"):
 | 
					 | 
				
			||||||
            nats_data["schedtaskpayload"]["deleteafter"] = True
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        r = asyncio.run(agent.nats_cmd(nats_data, timeout=10))
 | 
					        r = asyncio.run(agent.nats_cmd(nats_data, timeout=10))
 | 
				
			||||||
        if r != "ok":
 | 
					        if r != "ok":
 | 
				
			||||||
            return notify_error(r)
 | 
					            return notify_error(r)
 | 
				
			||||||
@@ -420,9 +385,12 @@ class Reboot(APIView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@api_view(["POST"])
 | 
					@api_view(["POST"])
 | 
				
			||||||
 | 
					@permission_classes([IsAuthenticated, InstallAgentPerms])
 | 
				
			||||||
def install_agent(request):
 | 
					def install_agent(request):
 | 
				
			||||||
    from knox.models import AuthToken
 | 
					    from knox.models import AuthToken
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    from agents.utils import get_winagent_url
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    client_id = request.data["client"]
 | 
					    client_id = request.data["client"]
 | 
				
			||||||
    site_id = request.data["site"]
 | 
					    site_id = request.data["site"]
 | 
				
			||||||
    version = settings.LATEST_AGENT_VER
 | 
					    version = settings.LATEST_AGENT_VER
 | 
				
			||||||
@@ -443,131 +411,27 @@ def install_agent(request):
 | 
				
			|||||||
    inno = (
 | 
					    inno = (
 | 
				
			||||||
        f"winagent-v{version}.exe" if arch == "64" else f"winagent-v{version}-x86.exe"
 | 
					        f"winagent-v{version}.exe" if arch == "64" else f"winagent-v{version}-x86.exe"
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    download_url = settings.DL_64 if arch == "64" else settings.DL_32
 | 
					    download_url = get_winagent_url(arch)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    _, token = AuthToken.objects.create(
 | 
					    _, token = AuthToken.objects.create(
 | 
				
			||||||
        user=request.user, expiry=dt.timedelta(hours=request.data["expires"])
 | 
					        user=request.user, expiry=dt.timedelta(hours=request.data["expires"])
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if request.data["installMethod"] == "exe":
 | 
					    if request.data["installMethod"] == "exe":
 | 
				
			||||||
        go_bin = "/usr/local/rmmgo/go/bin/go"
 | 
					        from tacticalrmm.utils import generate_winagent_exe
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if not os.path.exists(go_bin):
 | 
					        return generate_winagent_exe(
 | 
				
			||||||
            return Response("nogolang", status=status.HTTP_409_CONFLICT)
 | 
					            client=client_id,
 | 
				
			||||||
 | 
					            site=site_id,
 | 
				
			||||||
        api = request.data["api"]
 | 
					            agent_type=request.data["agenttype"],
 | 
				
			||||||
        atype = request.data["agenttype"]
 | 
					            rdp=request.data["rdp"],
 | 
				
			||||||
        rdp = request.data["rdp"]
 | 
					            ping=request.data["ping"],
 | 
				
			||||||
        ping = request.data["ping"]
 | 
					            power=request.data["power"],
 | 
				
			||||||
        power = request.data["power"]
 | 
					            arch=arch,
 | 
				
			||||||
 | 
					            token=token,
 | 
				
			||||||
        file_name = "rmm-installer.exe"
 | 
					            api=request.data["api"],
 | 
				
			||||||
        exe = os.path.join(settings.EXE_DIR, file_name)
 | 
					            file_name=request.data["fileName"],
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
        if os.path.exists(exe):
 | 
					 | 
				
			||||||
            try:
 | 
					 | 
				
			||||||
                os.remove(exe)
 | 
					 | 
				
			||||||
            except Exception as e:
 | 
					 | 
				
			||||||
                logger.error(str(e))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        goarch = "amd64" if arch == "64" else "386"
 | 
					 | 
				
			||||||
        cmd = [
 | 
					 | 
				
			||||||
            "env",
 | 
					 | 
				
			||||||
            "GOOS=windows",
 | 
					 | 
				
			||||||
            f"GOARCH={goarch}",
 | 
					 | 
				
			||||||
            go_bin,
 | 
					 | 
				
			||||||
            "build",
 | 
					 | 
				
			||||||
            f"-ldflags=\"-X 'main.Inno={inno}'",
 | 
					 | 
				
			||||||
            f"-X 'main.Api={api}'",
 | 
					 | 
				
			||||||
            f"-X 'main.Client={client_id}'",
 | 
					 | 
				
			||||||
            f"-X 'main.Site={site_id}'",
 | 
					 | 
				
			||||||
            f"-X 'main.Atype={atype}'",
 | 
					 | 
				
			||||||
            f"-X 'main.Rdp={rdp}'",
 | 
					 | 
				
			||||||
            f"-X 'main.Ping={ping}'",
 | 
					 | 
				
			||||||
            f"-X 'main.Power={power}'",
 | 
					 | 
				
			||||||
            f"-X 'main.DownloadUrl={download_url}'",
 | 
					 | 
				
			||||||
            f"-X 'main.Token={token}'\"",
 | 
					 | 
				
			||||||
            "-o",
 | 
					 | 
				
			||||||
            exe,
 | 
					 | 
				
			||||||
        ]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        build_error = False
 | 
					 | 
				
			||||||
        gen_error = False
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        gen = [
 | 
					 | 
				
			||||||
            "env",
 | 
					 | 
				
			||||||
            "GOOS=windows",
 | 
					 | 
				
			||||||
            f"GOARCH={goarch}",
 | 
					 | 
				
			||||||
            go_bin,
 | 
					 | 
				
			||||||
            "generate",
 | 
					 | 
				
			||||||
        ]
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            r1 = subprocess.run(
 | 
					 | 
				
			||||||
                " ".join(gen),
 | 
					 | 
				
			||||||
                capture_output=True,
 | 
					 | 
				
			||||||
                shell=True,
 | 
					 | 
				
			||||||
                cwd=os.path.join(settings.BASE_DIR, "core/goinstaller"),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        except Exception as e:
 | 
					 | 
				
			||||||
            gen_error = True
 | 
					 | 
				
			||||||
            logger.error(str(e))
 | 
					 | 
				
			||||||
            return Response(
 | 
					 | 
				
			||||||
                "genfailed", status=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if r1.returncode != 0:
 | 
					 | 
				
			||||||
            gen_error = True
 | 
					 | 
				
			||||||
            if r1.stdout:
 | 
					 | 
				
			||||||
                logger.error(r1.stdout.decode("utf-8", errors="ignore"))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if r1.stderr:
 | 
					 | 
				
			||||||
                logger.error(r1.stderr.decode("utf-8", errors="ignore"))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            logger.error(f"Go build failed with return code {r1.returncode}")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if gen_error:
 | 
					 | 
				
			||||||
            return Response(
 | 
					 | 
				
			||||||
                "genfailed", status=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            r = subprocess.run(
 | 
					 | 
				
			||||||
                " ".join(cmd),
 | 
					 | 
				
			||||||
                capture_output=True,
 | 
					 | 
				
			||||||
                shell=True,
 | 
					 | 
				
			||||||
                cwd=os.path.join(settings.BASE_DIR, "core/goinstaller"),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        except Exception as e:
 | 
					 | 
				
			||||||
            build_error = True
 | 
					 | 
				
			||||||
            logger.error(str(e))
 | 
					 | 
				
			||||||
            return Response("buildfailed", status=status.HTTP_412_PRECONDITION_FAILED)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if r.returncode != 0:
 | 
					 | 
				
			||||||
            build_error = True
 | 
					 | 
				
			||||||
            if r.stdout:
 | 
					 | 
				
			||||||
                logger.error(r.stdout.decode("utf-8", errors="ignore"))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if r.stderr:
 | 
					 | 
				
			||||||
                logger.error(r.stderr.decode("utf-8", errors="ignore"))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            logger.error(f"Go build failed with return code {r.returncode}")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if build_error:
 | 
					 | 
				
			||||||
            return Response("buildfailed", status=status.HTTP_412_PRECONDITION_FAILED)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if settings.DEBUG:
 | 
					 | 
				
			||||||
            with open(exe, "rb") as f:
 | 
					 | 
				
			||||||
                response = HttpResponse(
 | 
					 | 
				
			||||||
                    f.read(),
 | 
					 | 
				
			||||||
                    content_type="application/vnd.microsoft.portable-executable",
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                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}"
 | 
					 | 
				
			||||||
            return response
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    elif request.data["installMethod"] == "manual":
 | 
					    elif request.data["installMethod"] == "manual":
 | 
				
			||||||
        cmd = [
 | 
					        cmd = [
 | 
				
			||||||
@@ -575,12 +439,10 @@ def install_agent(request):
 | 
				
			|||||||
            "/VERYSILENT",
 | 
					            "/VERYSILENT",
 | 
				
			||||||
            "/SUPPRESSMSGBOXES",
 | 
					            "/SUPPRESSMSGBOXES",
 | 
				
			||||||
            "&&",
 | 
					            "&&",
 | 
				
			||||||
            "timeout",
 | 
					            "ping",
 | 
				
			||||||
            "/t",
 | 
					            "127.0.0.1",
 | 
				
			||||||
            "10",
 | 
					            "-n",
 | 
				
			||||||
            "/nobreak",
 | 
					            "5",
 | 
				
			||||||
            ">",
 | 
					 | 
				
			||||||
            "NUL",
 | 
					 | 
				
			||||||
            "&&",
 | 
					            "&&",
 | 
				
			||||||
            r'"C:\Program Files\TacticalAgent\tacticalrmm.exe"',
 | 
					            r'"C:\Program Files\TacticalAgent\tacticalrmm.exe"',
 | 
				
			||||||
            "-m",
 | 
					            "-m",
 | 
				
			||||||
@@ -607,8 +469,6 @@ def install_agent(request):
 | 
				
			|||||||
        resp = {
 | 
					        resp = {
 | 
				
			||||||
            "cmd": " ".join(str(i) for i in cmd),
 | 
					            "cmd": " ".join(str(i) for i in cmd),
 | 
				
			||||||
            "url": download_url,
 | 
					            "url": download_url,
 | 
				
			||||||
            "salt64": settings.SALT_64,
 | 
					 | 
				
			||||||
            "salt32": settings.SALT_32,
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return Response(resp)
 | 
					        return Response(resp)
 | 
				
			||||||
@@ -665,27 +525,14 @@ def recover(request):
 | 
				
			|||||||
    agent = get_object_or_404(Agent, pk=request.data["pk"])
 | 
					    agent = get_object_or_404(Agent, pk=request.data["pk"])
 | 
				
			||||||
    mode = request.data["mode"]
 | 
					    mode = request.data["mode"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if pyver.parse(agent.version) <= pyver.parse("0.9.5"):
 | 
					    # attempt a realtime recovery, otherwise fall back to old recovery method
 | 
				
			||||||
        return notify_error("Only available in agent version greater than 0.9.5")
 | 
					    if mode == "tacagent" or mode == "mesh":
 | 
				
			||||||
 | 
					        data = {"func": "recover", "payload": {"mode": mode}}
 | 
				
			||||||
 | 
					        r = asyncio.run(agent.nats_cmd(data, timeout=10))
 | 
				
			||||||
 | 
					        if r == "ok":
 | 
				
			||||||
 | 
					            return Response("Successfully completed recovery")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if not agent.has_nats:
 | 
					    if agent.recoveryactions.filter(last_run=None).exists():  # type: ignore
 | 
				
			||||||
        if mode == "tacagent" or mode == "checkrunner" or mode == "rpc":
 | 
					 | 
				
			||||||
            return notify_error("Requires agent version 1.1.0 or greater")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # attempt a realtime recovery if supported, otherwise fall back to old recovery method
 | 
					 | 
				
			||||||
    if agent.has_nats:
 | 
					 | 
				
			||||||
        if (
 | 
					 | 
				
			||||||
            mode == "tacagent"
 | 
					 | 
				
			||||||
            or mode == "checkrunner"
 | 
					 | 
				
			||||||
            or mode == "salt"
 | 
					 | 
				
			||||||
            or mode == "mesh"
 | 
					 | 
				
			||||||
        ):
 | 
					 | 
				
			||||||
            data = {"func": "recover", "payload": {"mode": mode}}
 | 
					 | 
				
			||||||
            r = asyncio.run(agent.nats_cmd(data, timeout=10))
 | 
					 | 
				
			||||||
            if r == "ok":
 | 
					 | 
				
			||||||
                return Response("Successfully completed recovery")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if agent.recoveryactions.filter(last_run=None).exists():
 | 
					 | 
				
			||||||
        return notify_error(
 | 
					        return notify_error(
 | 
				
			||||||
            "A recovery action is currently pending. Please wait for the next agent check-in."
 | 
					            "A recovery action is currently pending. Please wait for the next agent check-in."
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
@@ -711,12 +558,12 @@ def recover(request):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@api_view(["POST"])
 | 
					@api_view(["POST"])
 | 
				
			||||||
 | 
					@permission_classes([IsAuthenticated, RunScriptPerms])
 | 
				
			||||||
def run_script(request):
 | 
					def run_script(request):
 | 
				
			||||||
    agent = get_object_or_404(Agent, pk=request.data["pk"])
 | 
					    agent = get_object_or_404(Agent, pk=request.data["pk"])
 | 
				
			||||||
    if not agent.has_nats:
 | 
					 | 
				
			||||||
        return notify_error("Requires agent version 1.1.0 or greater")
 | 
					 | 
				
			||||||
    script = get_object_or_404(Script, pk=request.data["scriptPK"])
 | 
					    script = get_object_or_404(Script, pk=request.data["scriptPK"])
 | 
				
			||||||
    output = request.data["output"]
 | 
					    output = request.data["output"]
 | 
				
			||||||
 | 
					    args = request.data["args"]
 | 
				
			||||||
    req_timeout = int(request.data["timeout"]) + 3
 | 
					    req_timeout = int(request.data["timeout"]) + 3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    AuditLog.audit_script_run(
 | 
					    AuditLog.audit_script_run(
 | 
				
			||||||
@@ -725,32 +572,34 @@ def run_script(request):
 | 
				
			|||||||
        script=script.name,
 | 
					        script=script.name,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    data = {
 | 
					 | 
				
			||||||
        "func": "runscript",
 | 
					 | 
				
			||||||
        "timeout": request.data["timeout"],
 | 
					 | 
				
			||||||
        "script_args": request.data["args"],
 | 
					 | 
				
			||||||
        "payload": {
 | 
					 | 
				
			||||||
            "code": script.code,
 | 
					 | 
				
			||||||
            "shell": script.shell,
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if output == "wait":
 | 
					    if output == "wait":
 | 
				
			||||||
        r = asyncio.run(agent.nats_cmd(data, timeout=req_timeout))
 | 
					        r = agent.run_script(
 | 
				
			||||||
 | 
					            scriptpk=script.pk, args=args, timeout=req_timeout, wait=True
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
        return Response(r)
 | 
					        return Response(r)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    elif output == "email":
 | 
				
			||||||
 | 
					        emails = (
 | 
				
			||||||
 | 
					            [] if request.data["emailmode"] == "default" else request.data["emails"]
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        run_script_email_results_task.delay(
 | 
				
			||||||
 | 
					            agentpk=agent.pk,
 | 
				
			||||||
 | 
					            scriptpk=script.pk,
 | 
				
			||||||
 | 
					            nats_timeout=req_timeout,
 | 
				
			||||||
 | 
					            emails=emails,
 | 
				
			||||||
 | 
					            args=args,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
        asyncio.run(agent.nats_cmd(data, wait=False))
 | 
					        agent.run_script(scriptpk=script.pk, args=args, timeout=req_timeout)
 | 
				
			||||||
        return Response(f"{script.name} will now be run on {agent.hostname}")
 | 
					
 | 
				
			||||||
 | 
					    return Response(f"{script.name} will now be run on {agent.hostname}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@api_view()
 | 
					@api_view()
 | 
				
			||||||
def recover_mesh(request, pk):
 | 
					def recover_mesh(request, pk):
 | 
				
			||||||
    agent = get_object_or_404(Agent, pk=pk)
 | 
					    agent = get_object_or_404(Agent, pk=pk)
 | 
				
			||||||
    if not agent.has_nats:
 | 
					 | 
				
			||||||
        return notify_error("Requires agent version 1.1.0 or greater")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    data = {"func": "recover", "payload": {"mode": "mesh"}}
 | 
					    data = {"func": "recover", "payload": {"mode": "mesh"}}
 | 
				
			||||||
    r = asyncio.run(agent.nats_cmd(data, timeout=45))
 | 
					    r = asyncio.run(agent.nats_cmd(data, timeout=90))
 | 
				
			||||||
    if r != "ok":
 | 
					    if r != "ok":
 | 
				
			||||||
        return notify_error("Unable to contact the agent")
 | 
					        return notify_error("Unable to contact the agent")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -792,6 +641,8 @@ class GetAddNotes(APIView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class GetEditDeleteNote(APIView):
 | 
					class GetEditDeleteNote(APIView):
 | 
				
			||||||
 | 
					    permission_classes = [IsAuthenticated, ManageNotesPerms]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get(self, request, pk):
 | 
					    def get(self, request, pk):
 | 
				
			||||||
        note = get_object_or_404(Note, pk=pk)
 | 
					        note = get_object_or_404(Note, pk=pk)
 | 
				
			||||||
        return Response(NoteSerializer(note).data)
 | 
					        return Response(NoteSerializer(note).data)
 | 
				
			||||||
@@ -810,6 +661,7 @@ class GetEditDeleteNote(APIView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@api_view(["POST"])
 | 
					@api_view(["POST"])
 | 
				
			||||||
 | 
					@permission_classes([IsAuthenticated, RunBulkPerms])
 | 
				
			||||||
def bulk(request):
 | 
					def bulk(request):
 | 
				
			||||||
    if request.data["target"] == "agents" and not request.data["agentPKs"]:
 | 
					    if request.data["target"] == "agents" and not request.data["agentPKs"]:
 | 
				
			||||||
        return notify_error("Must select at least 1 agent")
 | 
					        return notify_error("Must select at least 1 agent")
 | 
				
			||||||
@@ -821,12 +673,16 @@ def bulk(request):
 | 
				
			|||||||
    elif request.data["target"] == "agents":
 | 
					    elif request.data["target"] == "agents":
 | 
				
			||||||
        q = Agent.objects.filter(pk__in=request.data["agentPKs"])
 | 
					        q = Agent.objects.filter(pk__in=request.data["agentPKs"])
 | 
				
			||||||
    elif request.data["target"] == "all":
 | 
					    elif request.data["target"] == "all":
 | 
				
			||||||
        q = Agent.objects.all()
 | 
					        q = Agent.objects.only("pk", "monitoring_type")
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
        return notify_error("Something went wrong")
 | 
					        return notify_error("Something went wrong")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    minions = [agent.salt_id for agent in q]
 | 
					    if request.data["monType"] == "servers":
 | 
				
			||||||
    agents = [agent.pk for agent in q]
 | 
					        q = q.filter(monitoring_type="server")
 | 
				
			||||||
 | 
					    elif request.data["monType"] == "workstations":
 | 
				
			||||||
 | 
					        q = q.filter(monitoring_type="workstation")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    agents: list[int] = [agent.pk for agent in q]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    AuditLog.audit_bulk_action(request.user, request.data["mode"], request.data)
 | 
					    AuditLog.audit_bulk_action(request.user, request.data["mode"], request.data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -844,39 +700,17 @@ def bulk(request):
 | 
				
			|||||||
        return Response(f"{script.name} will now be run on {len(agents)} agents")
 | 
					        return Response(f"{script.name} will now be run on {len(agents)} agents")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    elif request.data["mode"] == "install":
 | 
					    elif request.data["mode"] == "install":
 | 
				
			||||||
        r = Agent.salt_batch_async(minions=minions, func="win_agent.install_updates")
 | 
					        bulk_install_updates_task.delay(agents)
 | 
				
			||||||
        if r == "timeout":
 | 
					 | 
				
			||||||
            return notify_error("Salt API not running")
 | 
					 | 
				
			||||||
        return Response(
 | 
					        return Response(
 | 
				
			||||||
            f"Pending updates will now be installed on {len(agents)} agents"
 | 
					            f"Pending updates will now be installed on {len(agents)} agents"
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
    elif request.data["mode"] == "scan":
 | 
					    elif request.data["mode"] == "scan":
 | 
				
			||||||
        bulk_check_for_updates_task.delay(minions=minions)
 | 
					        bulk_check_for_updates_task.delay(agents)
 | 
				
			||||||
        return Response(f"Patch status scan will now run on {len(agents)} agents")
 | 
					        return Response(f"Patch status scan will now run on {len(agents)} agents")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return notify_error("Something went wrong")
 | 
					    return notify_error("Something went wrong")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@api_view(["POST"])
 | 
					 | 
				
			||||||
def agent_counts(request):
 | 
					 | 
				
			||||||
    return Response(
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            "total_server_count": Agent.objects.filter(
 | 
					 | 
				
			||||||
                monitoring_type="server"
 | 
					 | 
				
			||||||
            ).count(),
 | 
					 | 
				
			||||||
            "total_server_offline_count": AgentOutage.objects.filter(
 | 
					 | 
				
			||||||
                recovery_time=None, agent__monitoring_type="server"
 | 
					 | 
				
			||||||
            ).count(),
 | 
					 | 
				
			||||||
            "total_workstation_count": Agent.objects.filter(
 | 
					 | 
				
			||||||
                monitoring_type="workstation"
 | 
					 | 
				
			||||||
            ).count(),
 | 
					 | 
				
			||||||
            "total_workstation_offline_count": AgentOutage.objects.filter(
 | 
					 | 
				
			||||||
                recovery_time=None, agent__monitoring_type="workstation"
 | 
					 | 
				
			||||||
            ).count(),
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@api_view(["POST"])
 | 
					@api_view(["POST"])
 | 
				
			||||||
def agent_maintenance(request):
 | 
					def agent_maintenance(request):
 | 
				
			||||||
    if request.data["type"] == "Client":
 | 
					    if request.data["type"] == "Client":
 | 
				
			||||||
@@ -903,10 +737,7 @@ def agent_maintenance(request):
 | 
				
			|||||||
class WMI(APIView):
 | 
					class WMI(APIView):
 | 
				
			||||||
    def get(self, request, pk):
 | 
					    def get(self, request, pk):
 | 
				
			||||||
        agent = get_object_or_404(Agent, pk=pk)
 | 
					        agent = get_object_or_404(Agent, pk=pk)
 | 
				
			||||||
        if pyver.parse(agent.version) < pyver.parse("1.1.2"):
 | 
					 | 
				
			||||||
            return notify_error("Requires agent version 1.1.2 or greater")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        r = asyncio.run(agent.nats_cmd({"func": "sysinfo"}, timeout=20))
 | 
					        r = asyncio.run(agent.nats_cmd({"func": "sysinfo"}, timeout=20))
 | 
				
			||||||
        if r != "ok":
 | 
					        if r != "ok":
 | 
				
			||||||
            return notify_error("Unable to contact the agent")
 | 
					            return notify_error("Unable to contact the agent")
 | 
				
			||||||
        return Response("ok")
 | 
					        return Response("ok")
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
from django.contrib import admin
 | 
					from django.contrib import admin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .models import Alert
 | 
					from .models import Alert, AlertTemplate
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
admin.site.register(Alert)
 | 
					admin.site.register(Alert)
 | 
				
			||||||
 | 
					admin.site.register(AlertTemplate)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
# Generated by Django 3.1 on 2020-08-15 15:31
 | 
					# Generated by Django 3.1 on 2020-08-15 15:31
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.db import migrations, models
 | 
					 | 
				
			||||||
import django.db.models.deletion
 | 
					import django.db.models.deletion
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Migration(migrations.Migration):
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
@@ -42,4 +42,4 @@ class Migration(migrations.Migration):
 | 
				
			|||||||
                ),
 | 
					                ),
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
@@ -27,4 +27,4 @@ class Migration(migrations.Migration):
 | 
				
			|||||||
                max_length=100,
 | 
					                max_length=100,
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
@@ -1,25 +1,31 @@
 | 
				
			|||||||
# Generated by Django 3.1.2 on 2020-10-21 18:15
 | 
					# Generated by Django 3.1.2 on 2020-10-21 18:15
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.db import migrations, models
 | 
					 | 
				
			||||||
import django.db.models.deletion
 | 
					import django.db.models.deletion
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Migration(migrations.Migration):
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    dependencies = [
 | 
					    dependencies = [
 | 
				
			||||||
        ('checks', '0010_auto_20200922_1344'),
 | 
					        ("checks", "0010_auto_20200922_1344"),
 | 
				
			||||||
        ('alerts', '0002_auto_20200815_1618'),
 | 
					        ("alerts", "0002_auto_20200815_1618"),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    operations = [
 | 
					    operations = [
 | 
				
			||||||
        migrations.AddField(
 | 
					        migrations.AddField(
 | 
				
			||||||
            model_name='alert',
 | 
					            model_name="alert",
 | 
				
			||||||
            name='assigned_check',
 | 
					            name="assigned_check",
 | 
				
			||||||
            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='alert', to='checks.check'),
 | 
					            field=models.ForeignKey(
 | 
				
			||||||
 | 
					                blank=True,
 | 
				
			||||||
 | 
					                null=True,
 | 
				
			||||||
 | 
					                on_delete=django.db.models.deletion.CASCADE,
 | 
				
			||||||
 | 
					                related_name="alert",
 | 
				
			||||||
 | 
					                to="checks.check",
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        migrations.AlterField(
 | 
					        migrations.AlterField(
 | 
				
			||||||
            model_name='alert',
 | 
					            model_name="alert",
 | 
				
			||||||
            name='alert_time',
 | 
					            name="alert_time",
 | 
				
			||||||
            field=models.DateTimeField(auto_now_add=True, null=True),
 | 
					            field=models.DateTimeField(auto_now_add=True, null=True),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
							
								
								
									
										172
									
								
								api/tacticalrmm/alerts/migrations/0004_auto_20210212_1408.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								api/tacticalrmm/alerts/migrations/0004_auto_20210212_1408.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,172 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.1.4 on 2021-02-12 14:08
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import django.contrib.postgres.fields
 | 
				
			||||||
 | 
					import django.db.models.deletion
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('agents', '0029_delete_agentoutage'),
 | 
				
			||||||
 | 
					        ('clients', '0008_auto_20201103_1430'),
 | 
				
			||||||
 | 
					        ('autotasks', '0017_auto_20210210_1512'),
 | 
				
			||||||
 | 
					        ('scripts', '0005_auto_20201207_1606'),
 | 
				
			||||||
 | 
					        ('alerts', '0003_auto_20201021_1815'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='alert',
 | 
				
			||||||
 | 
					            name='action_execution_time',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, max_length=100, null=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='alert',
 | 
				
			||||||
 | 
					            name='action_retcode',
 | 
				
			||||||
 | 
					            field=models.IntegerField(blank=True, null=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='alert',
 | 
				
			||||||
 | 
					            name='action_run',
 | 
				
			||||||
 | 
					            field=models.DateTimeField(blank=True, null=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='alert',
 | 
				
			||||||
 | 
					            name='action_stderr',
 | 
				
			||||||
 | 
					            field=models.TextField(blank=True, null=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='alert',
 | 
				
			||||||
 | 
					            name='action_stdout',
 | 
				
			||||||
 | 
					            field=models.TextField(blank=True, null=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='alert',
 | 
				
			||||||
 | 
					            name='action_timeout',
 | 
				
			||||||
 | 
					            field=models.PositiveIntegerField(blank=True, null=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='alert',
 | 
				
			||||||
 | 
					            name='alert_type',
 | 
				
			||||||
 | 
					            field=models.CharField(choices=[('availability', 'Availability'), ('check', 'Check'), ('task', 'Task'), ('custom', 'Custom')], default='availability', max_length=20),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='alert',
 | 
				
			||||||
 | 
					            name='assigned_task',
 | 
				
			||||||
 | 
					            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='alert', to='autotasks.automatedtask'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='alert',
 | 
				
			||||||
 | 
					            name='email_sent',
 | 
				
			||||||
 | 
					            field=models.DateTimeField(blank=True, null=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='alert',
 | 
				
			||||||
 | 
					            name='hidden',
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='alert',
 | 
				
			||||||
 | 
					            name='resolved_action_execution_time',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, max_length=100, null=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='alert',
 | 
				
			||||||
 | 
					            name='resolved_action_retcode',
 | 
				
			||||||
 | 
					            field=models.IntegerField(blank=True, null=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='alert',
 | 
				
			||||||
 | 
					            name='resolved_action_run',
 | 
				
			||||||
 | 
					            field=models.DateTimeField(blank=True, null=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='alert',
 | 
				
			||||||
 | 
					            name='resolved_action_stderr',
 | 
				
			||||||
 | 
					            field=models.TextField(blank=True, null=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='alert',
 | 
				
			||||||
 | 
					            name='resolved_action_stdout',
 | 
				
			||||||
 | 
					            field=models.TextField(blank=True, null=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='alert',
 | 
				
			||||||
 | 
					            name='resolved_action_timeout',
 | 
				
			||||||
 | 
					            field=models.PositiveIntegerField(blank=True, null=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='alert',
 | 
				
			||||||
 | 
					            name='resolved_email_sent',
 | 
				
			||||||
 | 
					            field=models.DateTimeField(blank=True, null=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='alert',
 | 
				
			||||||
 | 
					            name='resolved_on',
 | 
				
			||||||
 | 
					            field=models.DateTimeField(blank=True, null=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='alert',
 | 
				
			||||||
 | 
					            name='resolved_sms_sent',
 | 
				
			||||||
 | 
					            field=models.DateTimeField(blank=True, null=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='alert',
 | 
				
			||||||
 | 
					            name='sms_sent',
 | 
				
			||||||
 | 
					            field=models.DateTimeField(blank=True, null=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='alert',
 | 
				
			||||||
 | 
					            name='snoozed',
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='alert',
 | 
				
			||||||
 | 
					            name='severity',
 | 
				
			||||||
 | 
					            field=models.CharField(choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], default='info', max_length=30),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name='AlertTemplate',
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
				
			||||||
 | 
					                ('name', models.CharField(max_length=100)),
 | 
				
			||||||
 | 
					                ('is_active', models.BooleanField(default=True)),
 | 
				
			||||||
 | 
					                ('action_args', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=255, null=True), blank=True, default=list, null=True, size=None)),
 | 
				
			||||||
 | 
					                ('resolved_action_args', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=255, null=True), blank=True, default=list, null=True, size=None)),
 | 
				
			||||||
 | 
					                ('email_recipients', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=100), blank=True, default=list, null=True, size=None)),
 | 
				
			||||||
 | 
					                ('text_recipients', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=100), blank=True, default=list, null=True, size=None)),
 | 
				
			||||||
 | 
					                ('email_from', models.EmailField(blank=True, max_length=254, null=True)),
 | 
				
			||||||
 | 
					                ('agent_email_on_resolved', models.BooleanField(blank=True, default=False, null=True)),
 | 
				
			||||||
 | 
					                ('agent_text_on_resolved', models.BooleanField(blank=True, default=False, null=True)),
 | 
				
			||||||
 | 
					                ('agent_include_desktops', models.BooleanField(blank=True, default=False, null=True)),
 | 
				
			||||||
 | 
					                ('agent_always_email', models.BooleanField(blank=True, default=False, null=True)),
 | 
				
			||||||
 | 
					                ('agent_always_text', models.BooleanField(blank=True, default=False, null=True)),
 | 
				
			||||||
 | 
					                ('agent_always_alert', models.BooleanField(blank=True, default=False, null=True)),
 | 
				
			||||||
 | 
					                ('agent_periodic_alert_days', models.PositiveIntegerField(blank=True, default=0, null=True)),
 | 
				
			||||||
 | 
					                ('check_email_alert_severity', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], max_length=25), blank=True, default=list, size=None)),
 | 
				
			||||||
 | 
					                ('check_text_alert_severity', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], max_length=25), blank=True, default=list, size=None)),
 | 
				
			||||||
 | 
					                ('check_dashboard_alert_severity', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], max_length=25), blank=True, default=list, size=None)),
 | 
				
			||||||
 | 
					                ('check_email_on_resolved', models.BooleanField(blank=True, default=False, null=True)),
 | 
				
			||||||
 | 
					                ('check_text_on_resolved', models.BooleanField(blank=True, default=False, null=True)),
 | 
				
			||||||
 | 
					                ('check_always_email', models.BooleanField(blank=True, default=False, null=True)),
 | 
				
			||||||
 | 
					                ('check_always_text', models.BooleanField(blank=True, default=False, null=True)),
 | 
				
			||||||
 | 
					                ('check_always_alert', models.BooleanField(blank=True, default=False, null=True)),
 | 
				
			||||||
 | 
					                ('check_periodic_alert_days', models.PositiveIntegerField(blank=True, default=0, null=True)),
 | 
				
			||||||
 | 
					                ('task_email_alert_severity', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], max_length=25), blank=True, default=list, size=None)),
 | 
				
			||||||
 | 
					                ('task_text_alert_severity', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], max_length=25), blank=True, default=list, size=None)),
 | 
				
			||||||
 | 
					                ('task_dashboard_alert_severity', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], max_length=25), blank=True, default=list, size=None)),
 | 
				
			||||||
 | 
					                ('task_email_on_resolved', models.BooleanField(blank=True, default=False, null=True)),
 | 
				
			||||||
 | 
					                ('task_text_on_resolved', models.BooleanField(blank=True, default=False, null=True)),
 | 
				
			||||||
 | 
					                ('task_always_email', models.BooleanField(blank=True, default=False, null=True)),
 | 
				
			||||||
 | 
					                ('task_always_text', models.BooleanField(blank=True, default=False, null=True)),
 | 
				
			||||||
 | 
					                ('task_always_alert', models.BooleanField(blank=True, default=False, null=True)),
 | 
				
			||||||
 | 
					                ('task_periodic_alert_days', models.PositiveIntegerField(blank=True, default=0, null=True)),
 | 
				
			||||||
 | 
					                ('action', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='alert_template', to='scripts.script')),
 | 
				
			||||||
 | 
					                ('excluded_agents', models.ManyToManyField(blank=True, related_name='alert_exclusions', to='agents.Agent')),
 | 
				
			||||||
 | 
					                ('excluded_clients', models.ManyToManyField(blank=True, related_name='alert_exclusions', to='clients.Client')),
 | 
				
			||||||
 | 
					                ('excluded_sites', models.ManyToManyField(blank=True, related_name='alert_exclusions', to='clients.Site')),
 | 
				
			||||||
 | 
					                ('resolved_action', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='resolved_alert_template', to='scripts.script')),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
							
								
								
									
										31
									
								
								api/tacticalrmm/alerts/migrations/0005_auto_20210212_1745.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								api/tacticalrmm/alerts/migrations/0005_auto_20210212_1745.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.1.4 on 2021-02-12 17:45
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('alerts', '0004_auto_20210212_1408'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='alert',
 | 
				
			||||||
 | 
					            name='action_timeout',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='alert',
 | 
				
			||||||
 | 
					            name='resolved_action_timeout',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='alerttemplate',
 | 
				
			||||||
 | 
					            name='action_timeout',
 | 
				
			||||||
 | 
					            field=models.PositiveIntegerField(default=15),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='alerttemplate',
 | 
				
			||||||
 | 
					            name='resolved_action_timeout',
 | 
				
			||||||
 | 
					            field=models.PositiveIntegerField(default=15),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
							
								
								
									
										72
									
								
								api/tacticalrmm/alerts/migrations/0006_auto_20210217_1736.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								api/tacticalrmm/alerts/migrations/0006_auto_20210217_1736.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,72 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.1.6 on 2021-02-17 17:36
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('alerts', '0005_auto_20210212_1745'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='alerttemplate',
 | 
				
			||||||
 | 
					            name='agent_include_desktops',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='alerttemplate',
 | 
				
			||||||
 | 
					            name='exclude_servers',
 | 
				
			||||||
 | 
					            field=models.BooleanField(blank=True, default=False, null=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='alerttemplate',
 | 
				
			||||||
 | 
					            name='exclude_workstations',
 | 
				
			||||||
 | 
					            field=models.BooleanField(blank=True, default=False, null=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='alerttemplate',
 | 
				
			||||||
 | 
					            name='agent_always_alert',
 | 
				
			||||||
 | 
					            field=models.BooleanField(blank=True, default=None, null=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='alerttemplate',
 | 
				
			||||||
 | 
					            name='agent_always_email',
 | 
				
			||||||
 | 
					            field=models.BooleanField(blank=True, default=None, null=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='alerttemplate',
 | 
				
			||||||
 | 
					            name='agent_always_text',
 | 
				
			||||||
 | 
					            field=models.BooleanField(blank=True, default=None, null=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='alerttemplate',
 | 
				
			||||||
 | 
					            name='check_always_alert',
 | 
				
			||||||
 | 
					            field=models.BooleanField(blank=True, default=None, null=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='alerttemplate',
 | 
				
			||||||
 | 
					            name='check_always_email',
 | 
				
			||||||
 | 
					            field=models.BooleanField(blank=True, default=None, null=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='alerttemplate',
 | 
				
			||||||
 | 
					            name='check_always_text',
 | 
				
			||||||
 | 
					            field=models.BooleanField(blank=True, default=None, null=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='alerttemplate',
 | 
				
			||||||
 | 
					            name='task_always_alert',
 | 
				
			||||||
 | 
					            field=models.BooleanField(blank=True, default=None, null=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='alerttemplate',
 | 
				
			||||||
 | 
					            name='task_always_email',
 | 
				
			||||||
 | 
					            field=models.BooleanField(blank=True, default=None, null=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='alerttemplate',
 | 
				
			||||||
 | 
					            name='task_always_text',
 | 
				
			||||||
 | 
					            field=models.BooleanField(blank=True, default=None, null=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -1,5 +1,21 @@
 | 
				
			|||||||
from django.db import models
 | 
					from __future__ import annotations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import re
 | 
				
			||||||
 | 
					from typing import TYPE_CHECKING, Union
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.conf import settings
 | 
				
			||||||
 | 
					from django.contrib.postgres.fields import ArrayField
 | 
				
			||||||
 | 
					from django.db import models
 | 
				
			||||||
 | 
					from django.db.models.fields import BooleanField, PositiveIntegerField
 | 
				
			||||||
 | 
					from django.utils import timezone as djangotime
 | 
				
			||||||
 | 
					from loguru import logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if TYPE_CHECKING:
 | 
				
			||||||
 | 
					    from agents.models import Agent
 | 
				
			||||||
 | 
					    from autotasks.models import AutomatedTask
 | 
				
			||||||
 | 
					    from checks.models import Check
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					logger.configure(**settings.LOG_CONFIG)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
SEVERITY_CHOICES = [
 | 
					SEVERITY_CHOICES = [
 | 
				
			||||||
    ("info", "Informational"),
 | 
					    ("info", "Informational"),
 | 
				
			||||||
@@ -7,6 +23,13 @@ SEVERITY_CHOICES = [
 | 
				
			|||||||
    ("error", "Error"),
 | 
					    ("error", "Error"),
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ALERT_TYPE_CHOICES = [
 | 
				
			||||||
 | 
					    ("availability", "Availability"),
 | 
				
			||||||
 | 
					    ("check", "Check"),
 | 
				
			||||||
 | 
					    ("task", "Task"),
 | 
				
			||||||
 | 
					    ("custom", "Custom"),
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Alert(models.Model):
 | 
					class Alert(models.Model):
 | 
				
			||||||
    agent = models.ForeignKey(
 | 
					    agent = models.ForeignKey(
 | 
				
			||||||
@@ -23,21 +46,584 @@ class Alert(models.Model):
 | 
				
			|||||||
        null=True,
 | 
					        null=True,
 | 
				
			||||||
        blank=True,
 | 
					        blank=True,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					    assigned_task = models.ForeignKey(
 | 
				
			||||||
 | 
					        "autotasks.AutomatedTask",
 | 
				
			||||||
 | 
					        related_name="alert",
 | 
				
			||||||
 | 
					        on_delete=models.CASCADE,
 | 
				
			||||||
 | 
					        null=True,
 | 
				
			||||||
 | 
					        blank=True,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    alert_type = models.CharField(
 | 
				
			||||||
 | 
					        max_length=20, choices=ALERT_TYPE_CHOICES, default="availability"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
    message = models.TextField(null=True, blank=True)
 | 
					    message = models.TextField(null=True, blank=True)
 | 
				
			||||||
    alert_time = models.DateTimeField(auto_now_add=True, null=True)
 | 
					    alert_time = models.DateTimeField(auto_now_add=True, null=True, blank=True)
 | 
				
			||||||
 | 
					    snoozed = models.BooleanField(default=False)
 | 
				
			||||||
    snooze_until = models.DateTimeField(null=True, blank=True)
 | 
					    snooze_until = models.DateTimeField(null=True, blank=True)
 | 
				
			||||||
    resolved = models.BooleanField(default=False)
 | 
					    resolved = models.BooleanField(default=False)
 | 
				
			||||||
    severity = models.CharField(
 | 
					    resolved_on = models.DateTimeField(null=True, blank=True)
 | 
				
			||||||
        max_length=100, choices=SEVERITY_CHOICES, default="info"
 | 
					    severity = models.CharField(max_length=30, choices=SEVERITY_CHOICES, default="info")
 | 
				
			||||||
 | 
					    email_sent = models.DateTimeField(null=True, blank=True)
 | 
				
			||||||
 | 
					    resolved_email_sent = models.DateTimeField(null=True, blank=True)
 | 
				
			||||||
 | 
					    sms_sent = models.DateTimeField(null=True, blank=True)
 | 
				
			||||||
 | 
					    resolved_sms_sent = models.DateTimeField(null=True, blank=True)
 | 
				
			||||||
 | 
					    hidden = models.BooleanField(default=False)
 | 
				
			||||||
 | 
					    action_run = models.DateTimeField(null=True, blank=True)
 | 
				
			||||||
 | 
					    action_stdout = models.TextField(null=True, blank=True)
 | 
				
			||||||
 | 
					    action_stderr = models.TextField(null=True, blank=True)
 | 
				
			||||||
 | 
					    action_retcode = models.IntegerField(null=True, blank=True)
 | 
				
			||||||
 | 
					    action_execution_time = models.CharField(max_length=100, null=True, blank=True)
 | 
				
			||||||
 | 
					    resolved_action_run = models.DateTimeField(null=True, blank=True)
 | 
				
			||||||
 | 
					    resolved_action_stdout = models.TextField(null=True, blank=True)
 | 
				
			||||||
 | 
					    resolved_action_stderr = models.TextField(null=True, blank=True)
 | 
				
			||||||
 | 
					    resolved_action_retcode = models.IntegerField(null=True, blank=True)
 | 
				
			||||||
 | 
					    resolved_action_execution_time = models.CharField(
 | 
				
			||||||
 | 
					        max_length=100, null=True, blank=True
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __str__(self):
 | 
					    def __str__(self):
 | 
				
			||||||
        return self.message
 | 
					        return self.message
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def resolve(self):
 | 
				
			||||||
 | 
					        self.resolved = True
 | 
				
			||||||
 | 
					        self.resolved_on = djangotime.now()
 | 
				
			||||||
 | 
					        self.snoozed = False
 | 
				
			||||||
 | 
					        self.snooze_until = None
 | 
				
			||||||
 | 
					        self.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    def create_availability_alert(cls, agent):
 | 
					    def create_or_return_availability_alert(cls, agent):
 | 
				
			||||||
        pass
 | 
					        if not cls.objects.filter(agent=agent, resolved=False).exists():
 | 
				
			||||||
    
 | 
					            return cls.objects.create(
 | 
				
			||||||
 | 
					                agent=agent,
 | 
				
			||||||
 | 
					                alert_type="availability",
 | 
				
			||||||
 | 
					                severity="error",
 | 
				
			||||||
 | 
					                message=f"{agent.hostname} in {agent.client.name}\\{agent.site.name} is overdue.",
 | 
				
			||||||
 | 
					                hidden=True,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            return cls.objects.get(agent=agent, resolved=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    def create_check_alert(cls, check):
 | 
					    def create_or_return_check_alert(cls, check):
 | 
				
			||||||
        pass
 | 
					
 | 
				
			||||||
 | 
					        if not cls.objects.filter(assigned_check=check, resolved=False).exists():
 | 
				
			||||||
 | 
					            return cls.objects.create(
 | 
				
			||||||
 | 
					                assigned_check=check,
 | 
				
			||||||
 | 
					                alert_type="check",
 | 
				
			||||||
 | 
					                severity=check.alert_severity,
 | 
				
			||||||
 | 
					                message=f"{check.agent.hostname} has a {check.check_type} check: {check.readable_desc} that failed.",
 | 
				
			||||||
 | 
					                hidden=True,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            return cls.objects.get(assigned_check=check, resolved=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def create_or_return_task_alert(cls, task):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if not cls.objects.filter(assigned_task=task, resolved=False).exists():
 | 
				
			||||||
 | 
					            return cls.objects.create(
 | 
				
			||||||
 | 
					                assigned_task=task,
 | 
				
			||||||
 | 
					                alert_type="task",
 | 
				
			||||||
 | 
					                severity=task.alert_severity,
 | 
				
			||||||
 | 
					                message=f"{task.agent.hostname} has task: {task.name} that failed.",
 | 
				
			||||||
 | 
					                hidden=True,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            return cls.objects.get(assigned_task=task, resolved=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def handle_alert_failure(cls, instance: Union[Agent, AutomatedTask, Check]) -> None:
 | 
				
			||||||
 | 
					        from agents.models import Agent
 | 
				
			||||||
 | 
					        from autotasks.models import AutomatedTask
 | 
				
			||||||
 | 
					        from checks.models import Check
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # set variables
 | 
				
			||||||
 | 
					        dashboard_severities = None
 | 
				
			||||||
 | 
					        email_severities = None
 | 
				
			||||||
 | 
					        text_severities = None
 | 
				
			||||||
 | 
					        always_dashboard = None
 | 
				
			||||||
 | 
					        always_email = None
 | 
				
			||||||
 | 
					        always_text = None
 | 
				
			||||||
 | 
					        alert_interval = None
 | 
				
			||||||
 | 
					        email_task = None
 | 
				
			||||||
 | 
					        text_task = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # check what the instance passed is
 | 
				
			||||||
 | 
					        if isinstance(instance, Agent):
 | 
				
			||||||
 | 
					            from agents.tasks import agent_outage_email_task, agent_outage_sms_task
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            email_task = agent_outage_email_task
 | 
				
			||||||
 | 
					            text_task = agent_outage_sms_task
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            email_alert = instance.overdue_email_alert
 | 
				
			||||||
 | 
					            text_alert = instance.overdue_text_alert
 | 
				
			||||||
 | 
					            dashboard_alert = instance.overdue_dashboard_alert
 | 
				
			||||||
 | 
					            alert_template = instance.alert_template
 | 
				
			||||||
 | 
					            maintenance_mode = instance.maintenance_mode
 | 
				
			||||||
 | 
					            alert_severity = "error"
 | 
				
			||||||
 | 
					            agent = instance
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # set alert_template settings
 | 
				
			||||||
 | 
					            if alert_template:
 | 
				
			||||||
 | 
					                dashboard_severities = ["error"]
 | 
				
			||||||
 | 
					                email_severities = ["error"]
 | 
				
			||||||
 | 
					                text_severities = ["error"]
 | 
				
			||||||
 | 
					                always_dashboard = alert_template.agent_always_alert
 | 
				
			||||||
 | 
					                always_email = alert_template.agent_always_email
 | 
				
			||||||
 | 
					                always_text = alert_template.agent_always_text
 | 
				
			||||||
 | 
					                alert_interval = alert_template.agent_periodic_alert_days
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if instance.should_create_alert(alert_template):
 | 
				
			||||||
 | 
					                alert = cls.create_or_return_availability_alert(instance)
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                # check if there is an alert that exists
 | 
				
			||||||
 | 
					                if cls.objects.filter(agent=instance, resolved=False).exists():
 | 
				
			||||||
 | 
					                    alert = cls.objects.get(agent=instance, resolved=False)
 | 
				
			||||||
 | 
					                else:
 | 
				
			||||||
 | 
					                    alert = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        elif isinstance(instance, Check):
 | 
				
			||||||
 | 
					            from checks.tasks import (
 | 
				
			||||||
 | 
					                handle_check_email_alert_task,
 | 
				
			||||||
 | 
					                handle_check_sms_alert_task,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            email_task = handle_check_email_alert_task
 | 
				
			||||||
 | 
					            text_task = handle_check_sms_alert_task
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            email_alert = instance.email_alert
 | 
				
			||||||
 | 
					            text_alert = instance.text_alert
 | 
				
			||||||
 | 
					            dashboard_alert = instance.dashboard_alert
 | 
				
			||||||
 | 
					            alert_template = instance.agent.alert_template
 | 
				
			||||||
 | 
					            maintenance_mode = instance.agent.maintenance_mode
 | 
				
			||||||
 | 
					            alert_severity = instance.alert_severity
 | 
				
			||||||
 | 
					            agent = instance.agent
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # set alert_template settings
 | 
				
			||||||
 | 
					            if alert_template:
 | 
				
			||||||
 | 
					                dashboard_severities = alert_template.check_dashboard_alert_severity
 | 
				
			||||||
 | 
					                email_severities = alert_template.check_email_alert_severity
 | 
				
			||||||
 | 
					                text_severities = alert_template.check_text_alert_severity
 | 
				
			||||||
 | 
					                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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if instance.should_create_alert(alert_template):
 | 
				
			||||||
 | 
					                alert = cls.create_or_return_check_alert(instance)
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                # check if there is an alert that exists
 | 
				
			||||||
 | 
					                if cls.objects.filter(assigned_check=instance, resolved=False).exists():
 | 
				
			||||||
 | 
					                    alert = cls.objects.get(assigned_check=instance, resolved=False)
 | 
				
			||||||
 | 
					                else:
 | 
				
			||||||
 | 
					                    alert = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        elif isinstance(instance, AutomatedTask):
 | 
				
			||||||
 | 
					            from autotasks.tasks import handle_task_email_alert, handle_task_sms_alert
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            email_task = handle_task_email_alert
 | 
				
			||||||
 | 
					            text_task = handle_task_sms_alert
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            email_alert = instance.email_alert
 | 
				
			||||||
 | 
					            text_alert = instance.text_alert
 | 
				
			||||||
 | 
					            dashboard_alert = instance.dashboard_alert
 | 
				
			||||||
 | 
					            alert_template = instance.agent.alert_template
 | 
				
			||||||
 | 
					            maintenance_mode = instance.agent.maintenance_mode
 | 
				
			||||||
 | 
					            alert_severity = instance.alert_severity
 | 
				
			||||||
 | 
					            agent = instance.agent
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # set alert_template settings
 | 
				
			||||||
 | 
					            if alert_template:
 | 
				
			||||||
 | 
					                dashboard_severities = alert_template.task_dashboard_alert_severity
 | 
				
			||||||
 | 
					                email_severities = alert_template.task_email_alert_severity
 | 
				
			||||||
 | 
					                text_severities = alert_template.task_text_alert_severity
 | 
				
			||||||
 | 
					                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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if instance.should_create_alert(alert_template):
 | 
				
			||||||
 | 
					                alert = cls.create_or_return_task_alert(instance)
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                # check if there is an alert that exists
 | 
				
			||||||
 | 
					                if cls.objects.filter(assigned_task=instance, resolved=False).exists():
 | 
				
			||||||
 | 
					                    alert = cls.objects.get(assigned_task=instance, resolved=False)
 | 
				
			||||||
 | 
					                else:
 | 
				
			||||||
 | 
					                    alert = None
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # return if agent is in maintenance mode
 | 
				
			||||||
 | 
					        if maintenance_mode or not alert:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # check if alert severity changed on check and update the alert
 | 
				
			||||||
 | 
					        if alert_severity != alert.severity:
 | 
				
			||||||
 | 
					            alert.severity = alert_severity
 | 
				
			||||||
 | 
					            alert.save(update_fields=["severity"])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # create alert in dashboard if enabled
 | 
				
			||||||
 | 
					        if dashboard_alert or always_dashboard:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # check if alert template is set and specific severities are configured
 | 
				
			||||||
 | 
					            if alert_template and alert.severity not in dashboard_severities:  # type: ignore
 | 
				
			||||||
 | 
					                pass
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                alert.hidden = False
 | 
				
			||||||
 | 
					                alert.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # send email if enabled
 | 
				
			||||||
 | 
					        if email_alert or always_email:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # check if alert template is set and specific severities are configured
 | 
				
			||||||
 | 
					            if alert_template and alert.severity not in email_severities:  # type: ignore
 | 
				
			||||||
 | 
					                pass
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                email_task.delay(
 | 
				
			||||||
 | 
					                    pk=alert.pk,
 | 
				
			||||||
 | 
					                    alert_interval=alert_interval,
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # send text if enabled
 | 
				
			||||||
 | 
					        if text_alert or always_text:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # check if alert template is set and specific severities are configured
 | 
				
			||||||
 | 
					            if alert_template and alert.severity not in text_severities:  # type: ignore
 | 
				
			||||||
 | 
					                pass
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                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 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,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # command was successful
 | 
				
			||||||
 | 
					            if type(r) == dict:
 | 
				
			||||||
 | 
					                alert.action_retcode = r["retcode"]
 | 
				
			||||||
 | 
					                alert.action_stdout = r["stdout"]
 | 
				
			||||||
 | 
					                alert.action_stderr = r["stderr"]
 | 
				
			||||||
 | 
					                alert.action_execution_time = "{:.4f}".format(r["execution_time"])
 | 
				
			||||||
 | 
					                alert.action_run = djangotime.now()
 | 
				
			||||||
 | 
					                alert.save()
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                logger.error(
 | 
				
			||||||
 | 
					                    f"Failure action: {alert_template.action.name} failed to run on any agent for {agent.hostname} failure alert"
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def handle_alert_resolve(cls, instance: Union[Agent, AutomatedTask, Check]) -> None:
 | 
				
			||||||
 | 
					        from agents.models import Agent
 | 
				
			||||||
 | 
					        from autotasks.models import AutomatedTask
 | 
				
			||||||
 | 
					        from checks.models import Check
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # set variables
 | 
				
			||||||
 | 
					        email_on_resolved = False
 | 
				
			||||||
 | 
					        text_on_resolved = False
 | 
				
			||||||
 | 
					        resolved_email_task = None
 | 
				
			||||||
 | 
					        resolved_text_task = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # check what the instance passed is
 | 
				
			||||||
 | 
					        if isinstance(instance, Agent):
 | 
				
			||||||
 | 
					            from agents.tasks import agent_recovery_email_task, agent_recovery_sms_task
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            resolved_email_task = agent_recovery_email_task
 | 
				
			||||||
 | 
					            resolved_text_task = agent_recovery_sms_task
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            alert_template = instance.alert_template
 | 
				
			||||||
 | 
					            alert = cls.objects.get(agent=instance, resolved=False)
 | 
				
			||||||
 | 
					            maintenance_mode = instance.maintenance_mode
 | 
				
			||||||
 | 
					            agent = instance
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if alert_template:
 | 
				
			||||||
 | 
					                email_on_resolved = alert_template.agent_email_on_resolved
 | 
				
			||||||
 | 
					                text_on_resolved = alert_template.agent_text_on_resolved
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        elif isinstance(instance, Check):
 | 
				
			||||||
 | 
					            from checks.tasks import (
 | 
				
			||||||
 | 
					                handle_resolved_check_email_alert_task,
 | 
				
			||||||
 | 
					                handle_resolved_check_sms_alert_task,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            resolved_email_task = handle_resolved_check_email_alert_task
 | 
				
			||||||
 | 
					            resolved_text_task = handle_resolved_check_sms_alert_task
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            alert_template = instance.agent.alert_template
 | 
				
			||||||
 | 
					            alert = cls.objects.get(assigned_check=instance, resolved=False)
 | 
				
			||||||
 | 
					            maintenance_mode = instance.agent.maintenance_mode
 | 
				
			||||||
 | 
					            agent = instance.agent
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if alert_template:
 | 
				
			||||||
 | 
					                email_on_resolved = alert_template.check_email_on_resolved
 | 
				
			||||||
 | 
					                text_on_resolved = alert_template.check_text_on_resolved
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        elif isinstance(instance, AutomatedTask):
 | 
				
			||||||
 | 
					            from autotasks.tasks import (
 | 
				
			||||||
 | 
					                handle_resolved_task_email_alert,
 | 
				
			||||||
 | 
					                handle_resolved_task_sms_alert,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            resolved_email_task = handle_resolved_task_email_alert
 | 
				
			||||||
 | 
					            resolved_text_task = handle_resolved_task_sms_alert
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            alert_template = instance.agent.alert_template
 | 
				
			||||||
 | 
					            alert = cls.objects.get(assigned_task=instance, resolved=False)
 | 
				
			||||||
 | 
					            maintenance_mode = instance.agent.maintenance_mode
 | 
				
			||||||
 | 
					            agent = instance.agent
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if alert_template:
 | 
				
			||||||
 | 
					                email_on_resolved = alert_template.task_email_on_resolved
 | 
				
			||||||
 | 
					                text_on_resolved = alert_template.task_text_on_resolved
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # return if agent is in maintenance mode
 | 
				
			||||||
 | 
					        if maintenance_mode:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        alert.resolve()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # 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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # check if resolved text should be sent
 | 
				
			||||||
 | 
					        if text_on_resolved and not alert.resolved_sms_sent:
 | 
				
			||||||
 | 
					            resolved_text_task.delay(pk=alert.pk)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # check if resolved script should be run
 | 
				
			||||||
 | 
					        if (
 | 
				
			||||||
 | 
					            alert_template
 | 
				
			||||||
 | 
					            and alert_template.resolved_action
 | 
				
			||||||
 | 
					            and not alert.resolved_action_run
 | 
				
			||||||
 | 
					        ):
 | 
				
			||||||
 | 
					            r = 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,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # command was successful
 | 
				
			||||||
 | 
					            if type(r) == dict:
 | 
				
			||||||
 | 
					                alert.resolved_action_retcode = r["retcode"]
 | 
				
			||||||
 | 
					                alert.resolved_action_stdout = r["stdout"]
 | 
				
			||||||
 | 
					                alert.resolved_action_stderr = r["stderr"]
 | 
				
			||||||
 | 
					                alert.resolved_action_execution_time = "{:.4f}".format(
 | 
				
			||||||
 | 
					                    r["execution_time"]
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                alert.resolved_action_run = djangotime.now()
 | 
				
			||||||
 | 
					                alert.save()
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                logger.error(
 | 
				
			||||||
 | 
					                    f"Resolved action: {alert_template.action.name} failed to run on any agent for {agent.hostname} resolved alert"
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def parse_script_args(self, args: list[str]):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if not args:
 | 
				
			||||||
 | 
					            return []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        temp_args = list()
 | 
				
			||||||
 | 
					        # pattern to match for injection
 | 
				
			||||||
 | 
					        pattern = re.compile(".*\\{\\{alert\\.(.*)\\}\\}.*")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for arg in args:
 | 
				
			||||||
 | 
					            match = pattern.match(arg)
 | 
				
			||||||
 | 
					            if match:
 | 
				
			||||||
 | 
					                name = match.group(1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if hasattr(self, name):
 | 
				
			||||||
 | 
					                    value = f"'{getattr(self, name)}'"
 | 
				
			||||||
 | 
					                else:
 | 
				
			||||||
 | 
					                    continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                try:
 | 
				
			||||||
 | 
					                    temp_args.append(re.sub("\\{\\{.*\\}\\}", value, arg))  # type: ignore
 | 
				
			||||||
 | 
					                except Exception as e:
 | 
				
			||||||
 | 
					                    logger.error(e)
 | 
				
			||||||
 | 
					                    continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                temp_args.append(arg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return temp_args
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AlertTemplate(models.Model):
 | 
				
			||||||
 | 
					    name = models.CharField(max_length=100)
 | 
				
			||||||
 | 
					    is_active = models.BooleanField(default=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    action = models.ForeignKey(
 | 
				
			||||||
 | 
					        "scripts.Script",
 | 
				
			||||||
 | 
					        related_name="alert_template",
 | 
				
			||||||
 | 
					        blank=True,
 | 
				
			||||||
 | 
					        null=True,
 | 
				
			||||||
 | 
					        on_delete=models.SET_NULL,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    action_args = ArrayField(
 | 
				
			||||||
 | 
					        models.CharField(max_length=255, null=True, blank=True),
 | 
				
			||||||
 | 
					        null=True,
 | 
				
			||||||
 | 
					        blank=True,
 | 
				
			||||||
 | 
					        default=list,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    action_timeout = models.PositiveIntegerField(default=15)
 | 
				
			||||||
 | 
					    resolved_action = models.ForeignKey(
 | 
				
			||||||
 | 
					        "scripts.Script",
 | 
				
			||||||
 | 
					        related_name="resolved_alert_template",
 | 
				
			||||||
 | 
					        blank=True,
 | 
				
			||||||
 | 
					        null=True,
 | 
				
			||||||
 | 
					        on_delete=models.SET_NULL,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    resolved_action_args = ArrayField(
 | 
				
			||||||
 | 
					        models.CharField(max_length=255, null=True, blank=True),
 | 
				
			||||||
 | 
					        null=True,
 | 
				
			||||||
 | 
					        blank=True,
 | 
				
			||||||
 | 
					        default=list,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    resolved_action_timeout = models.PositiveIntegerField(default=15)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # overrides the global recipients
 | 
				
			||||||
 | 
					    email_recipients = ArrayField(
 | 
				
			||||||
 | 
					        models.CharField(max_length=100, blank=True),
 | 
				
			||||||
 | 
					        null=True,
 | 
				
			||||||
 | 
					        blank=True,
 | 
				
			||||||
 | 
					        default=list,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    text_recipients = ArrayField(
 | 
				
			||||||
 | 
					        models.CharField(max_length=100, blank=True),
 | 
				
			||||||
 | 
					        null=True,
 | 
				
			||||||
 | 
					        blank=True,
 | 
				
			||||||
 | 
					        default=list,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # overrides the from address
 | 
				
			||||||
 | 
					    email_from = models.EmailField(blank=True, null=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # agent alert settings
 | 
				
			||||||
 | 
					    agent_email_on_resolved = BooleanField(null=True, blank=True, default=False)
 | 
				
			||||||
 | 
					    agent_text_on_resolved = BooleanField(null=True, blank=True, default=False)
 | 
				
			||||||
 | 
					    agent_always_email = BooleanField(null=True, blank=True, default=None)
 | 
				
			||||||
 | 
					    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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # check alert settings
 | 
				
			||||||
 | 
					    check_email_alert_severity = ArrayField(
 | 
				
			||||||
 | 
					        models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
 | 
				
			||||||
 | 
					        blank=True,
 | 
				
			||||||
 | 
					        default=list,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    check_text_alert_severity = ArrayField(
 | 
				
			||||||
 | 
					        models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
 | 
				
			||||||
 | 
					        blank=True,
 | 
				
			||||||
 | 
					        default=list,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    check_dashboard_alert_severity = ArrayField(
 | 
				
			||||||
 | 
					        models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
 | 
				
			||||||
 | 
					        blank=True,
 | 
				
			||||||
 | 
					        default=list,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    check_email_on_resolved = BooleanField(null=True, blank=True, default=False)
 | 
				
			||||||
 | 
					    check_text_on_resolved = BooleanField(null=True, blank=True, default=False)
 | 
				
			||||||
 | 
					    check_always_email = BooleanField(null=True, blank=True, default=None)
 | 
				
			||||||
 | 
					    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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # task alert settings
 | 
				
			||||||
 | 
					    task_email_alert_severity = ArrayField(
 | 
				
			||||||
 | 
					        models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
 | 
				
			||||||
 | 
					        blank=True,
 | 
				
			||||||
 | 
					        default=list,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    task_text_alert_severity = ArrayField(
 | 
				
			||||||
 | 
					        models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
 | 
				
			||||||
 | 
					        blank=True,
 | 
				
			||||||
 | 
					        default=list,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    task_dashboard_alert_severity = ArrayField(
 | 
				
			||||||
 | 
					        models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
 | 
				
			||||||
 | 
					        blank=True,
 | 
				
			||||||
 | 
					        default=list,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    task_email_on_resolved = BooleanField(null=True, blank=True, default=False)
 | 
				
			||||||
 | 
					    task_text_on_resolved = BooleanField(null=True, blank=True, default=False)
 | 
				
			||||||
 | 
					    task_always_email = BooleanField(null=True, blank=True, default=None)
 | 
				
			||||||
 | 
					    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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # exclusion settings
 | 
				
			||||||
 | 
					    exclude_workstations = BooleanField(null=True, blank=True, default=False)
 | 
				
			||||||
 | 
					    exclude_servers = BooleanField(null=True, blank=True, default=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    excluded_sites = models.ManyToManyField(
 | 
				
			||||||
 | 
					        "clients.Site", related_name="alert_exclusions", blank=True
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    excluded_clients = models.ManyToManyField(
 | 
				
			||||||
 | 
					        "clients.Client", related_name="alert_exclusions", blank=True
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    excluded_agents = models.ManyToManyField(
 | 
				
			||||||
 | 
					        "agents.Agent", related_name="alert_exclusions", blank=True
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __str__(self):
 | 
				
			||||||
 | 
					        return self.name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def has_agent_settings(self) -> bool:
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					            self.agent_email_on_resolved
 | 
				
			||||||
 | 
					            or self.agent_text_on_resolved
 | 
				
			||||||
 | 
					            or self.agent_always_email
 | 
				
			||||||
 | 
					            or self.agent_always_text
 | 
				
			||||||
 | 
					            or self.agent_always_alert
 | 
				
			||||||
 | 
					            or bool(self.agent_periodic_alert_days)
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def has_check_settings(self) -> bool:
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					            bool(self.check_email_alert_severity)
 | 
				
			||||||
 | 
					            or bool(self.check_text_alert_severity)
 | 
				
			||||||
 | 
					            or bool(self.check_dashboard_alert_severity)
 | 
				
			||||||
 | 
					            or self.check_email_on_resolved
 | 
				
			||||||
 | 
					            or self.check_text_on_resolved
 | 
				
			||||||
 | 
					            or self.check_always_email
 | 
				
			||||||
 | 
					            or self.check_always_text
 | 
				
			||||||
 | 
					            or self.check_always_alert
 | 
				
			||||||
 | 
					            or bool(self.check_periodic_alert_days)
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def has_task_settings(self) -> bool:
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					            bool(self.task_email_alert_severity)
 | 
				
			||||||
 | 
					            or bool(self.task_text_alert_severity)
 | 
				
			||||||
 | 
					            or bool(self.task_dashboard_alert_severity)
 | 
				
			||||||
 | 
					            or self.task_email_on_resolved
 | 
				
			||||||
 | 
					            or self.task_text_on_resolved
 | 
				
			||||||
 | 
					            or self.task_always_email
 | 
				
			||||||
 | 
					            or self.task_always_text
 | 
				
			||||||
 | 
					            or self.task_always_alert
 | 
				
			||||||
 | 
					            or bool(self.task_periodic_alert_days)
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def has_core_settings(self) -> bool:
 | 
				
			||||||
 | 
					        return bool(self.email_from) or self.email_recipients or self.text_recipients
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def is_default_template(self) -> bool:
 | 
				
			||||||
 | 
					        return self.default_alert_template.exists()  # type: ignore
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										11
									
								
								api/tacticalrmm/alerts/permissions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								api/tacticalrmm/alerts/permissions.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					from rest_framework import permissions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from tacticalrmm.permissions import _has_perm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ManageAlertsPerms(permissions.BasePermission):
 | 
				
			||||||
 | 
					    def has_permission(self, r, view):
 | 
				
			||||||
 | 
					        if r.method == "GET" or r.method == "PATCH":
 | 
				
			||||||
 | 
					            return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return _has_perm(r, "can_manage_alerts")
 | 
				
			||||||
@@ -1,19 +1,121 @@
 | 
				
			|||||||
from rest_framework.serializers import (
 | 
					from rest_framework.fields import SerializerMethodField
 | 
				
			||||||
    ModelSerializer,
 | 
					from rest_framework.serializers import ModelSerializer, ReadOnlyField
 | 
				
			||||||
    ReadOnlyField,
 | 
					 | 
				
			||||||
    DateTimeField,
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .models import Alert
 | 
					from automation.serializers import PolicySerializer
 | 
				
			||||||
 | 
					from clients.serializers import ClientSerializer, SiteSerializer
 | 
				
			||||||
 | 
					from tacticalrmm.utils import get_default_timezone
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .models import Alert, AlertTemplate
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AlertSerializer(ModelSerializer):
 | 
					class AlertSerializer(ModelSerializer):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    hostname = ReadOnlyField(source="agent.hostname")
 | 
					    hostname = SerializerMethodField(read_only=True)
 | 
				
			||||||
    client = ReadOnlyField(source="agent.client")
 | 
					    client = SerializerMethodField(read_only=True)
 | 
				
			||||||
    site = ReadOnlyField(source="agent.site")
 | 
					    site = SerializerMethodField(read_only=True)
 | 
				
			||||||
    alert_time = DateTimeField(format="iso-8601")
 | 
					    alert_time = SerializerMethodField(read_only=True)
 | 
				
			||||||
 | 
					    resolve_on = SerializerMethodField(read_only=True)
 | 
				
			||||||
 | 
					    snoozed_until = SerializerMethodField(read_only=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_hostname(self, instance):
 | 
				
			||||||
 | 
					        if instance.alert_type == "availability":
 | 
				
			||||||
 | 
					            return instance.agent.hostname if instance.agent else ""
 | 
				
			||||||
 | 
					        elif instance.alert_type == "check":
 | 
				
			||||||
 | 
					            return (
 | 
				
			||||||
 | 
					                instance.assigned_check.agent.hostname
 | 
				
			||||||
 | 
					                if instance.assigned_check
 | 
				
			||||||
 | 
					                else ""
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        elif instance.alert_type == "task":
 | 
				
			||||||
 | 
					            return (
 | 
				
			||||||
 | 
					                instance.assigned_task.agent.hostname if instance.assigned_task else ""
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            return ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_client(self, instance):
 | 
				
			||||||
 | 
					        if instance.alert_type == "availability":
 | 
				
			||||||
 | 
					            return instance.agent.client.name if instance.agent else ""
 | 
				
			||||||
 | 
					        elif instance.alert_type == "check":
 | 
				
			||||||
 | 
					            return (
 | 
				
			||||||
 | 
					                instance.assigned_check.agent.client.name
 | 
				
			||||||
 | 
					                if instance.assigned_check
 | 
				
			||||||
 | 
					                else ""
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        elif instance.alert_type == "task":
 | 
				
			||||||
 | 
					            return (
 | 
				
			||||||
 | 
					                instance.assigned_task.agent.client.name
 | 
				
			||||||
 | 
					                if instance.assigned_task
 | 
				
			||||||
 | 
					                else ""
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            return ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_site(self, instance):
 | 
				
			||||||
 | 
					        if instance.alert_type == "availability":
 | 
				
			||||||
 | 
					            return instance.agent.site.name if instance.agent else ""
 | 
				
			||||||
 | 
					        elif instance.alert_type == "check":
 | 
				
			||||||
 | 
					            return (
 | 
				
			||||||
 | 
					                instance.assigned_check.agent.site.name
 | 
				
			||||||
 | 
					                if instance.assigned_check
 | 
				
			||||||
 | 
					                else ""
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        elif instance.alert_type == "task":
 | 
				
			||||||
 | 
					            return (
 | 
				
			||||||
 | 
					                instance.assigned_task.agent.site.name if instance.assigned_task else ""
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            return ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_alert_time(self, instance):
 | 
				
			||||||
 | 
					        if instance.alert_time:
 | 
				
			||||||
 | 
					            return instance.alert_time.astimezone(get_default_timezone()).timestamp()
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_resolve_on(self, instance):
 | 
				
			||||||
 | 
					        if instance.resolved_on:
 | 
				
			||||||
 | 
					            return instance.resolved_on.astimezone(get_default_timezone()).timestamp()
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_snoozed_until(self, instance):
 | 
				
			||||||
 | 
					        if instance.snooze_until:
 | 
				
			||||||
 | 
					            return instance.snooze_until.astimezone(get_default_timezone()).timestamp()
 | 
				
			||||||
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = Alert
 | 
					        model = Alert
 | 
				
			||||||
        fields = "__all__"
 | 
					        fields = "__all__"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AlertTemplateSerializer(ModelSerializer):
 | 
				
			||||||
 | 
					    agent_settings = ReadOnlyField(source="has_agent_settings")
 | 
				
			||||||
 | 
					    check_settings = ReadOnlyField(source="has_check_settings")
 | 
				
			||||||
 | 
					    task_settings = ReadOnlyField(source="has_task_settings")
 | 
				
			||||||
 | 
					    core_settings = ReadOnlyField(source="has_core_settings")
 | 
				
			||||||
 | 
					    default_template = ReadOnlyField(source="is_default_template")
 | 
				
			||||||
 | 
					    action_name = ReadOnlyField(source="action.name")
 | 
				
			||||||
 | 
					    resolved_action_name = ReadOnlyField(source="resolved_action.name")
 | 
				
			||||||
 | 
					    applied_count = SerializerMethodField()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        model = AlertTemplate
 | 
				
			||||||
 | 
					        fields = "__all__"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_applied_count(self, instance):
 | 
				
			||||||
 | 
					        count = 0
 | 
				
			||||||
 | 
					        count += instance.policies.count()
 | 
				
			||||||
 | 
					        count += instance.clients.count()
 | 
				
			||||||
 | 
					        count += instance.sites.count()
 | 
				
			||||||
 | 
					        return count
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AlertTemplateRelationSerializer(ModelSerializer):
 | 
				
			||||||
 | 
					    policies = PolicySerializer(read_only=True, many=True)
 | 
				
			||||||
 | 
					    clients = ClientSerializer(read_only=True, many=True)
 | 
				
			||||||
 | 
					    sites = SiteSerializer(read_only=True, many=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        model = AlertTemplate
 | 
				
			||||||
 | 
					        fields = "__all__"
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										24
									
								
								api/tacticalrmm/alerts/tasks.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								api/tacticalrmm/alerts/tasks.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					from django.utils import timezone as djangotime
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from alerts.models import Alert
 | 
				
			||||||
 | 
					from tacticalrmm.celery import app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.task
 | 
				
			||||||
 | 
					def unsnooze_alerts() -> str:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Alert.objects.filter(snoozed=True, snooze_until__lte=djangotime.now()).update(
 | 
				
			||||||
 | 
					        snoozed=False, snooze_until=None
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return "ok"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.task
 | 
				
			||||||
 | 
					def cache_agents_alert_template():
 | 
				
			||||||
 | 
					    from agents.models import Agent
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for agent in Agent.objects.only("pk"):
 | 
				
			||||||
 | 
					        agent.set_alert_template()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return "ok"
 | 
				
			||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -1,7 +1,12 @@
 | 
				
			|||||||
from django.urls import path
 | 
					from django.urls import path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from . import views
 | 
					from . import views
 | 
				
			||||||
 | 
					
 | 
				
			||||||
urlpatterns = [
 | 
					urlpatterns = [
 | 
				
			||||||
    path("alerts/", views.GetAddAlerts.as_view()),
 | 
					    path("alerts/", views.GetAddAlerts.as_view()),
 | 
				
			||||||
 | 
					    path("bulk/", views.BulkAlerts.as_view()),
 | 
				
			||||||
    path("alerts/<int:pk>/", views.GetUpdateDeleteAlert.as_view()),
 | 
					    path("alerts/<int:pk>/", views.GetUpdateDeleteAlert.as_view()),
 | 
				
			||||||
 | 
					    path("alerttemplates/", views.GetAddAlertTemplates.as_view()),
 | 
				
			||||||
 | 
					    path("alerttemplates/<int:pk>/", views.GetUpdateDeleteAlertTemplate.as_view()),
 | 
				
			||||||
 | 
					    path("alerttemplates/<int:pk>/related/", views.RelatedAlertTemplate.as_view()),
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,19 +1,108 @@
 | 
				
			|||||||
 | 
					from datetime import datetime as dt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db.models import Q
 | 
				
			||||||
from django.shortcuts import get_object_or_404
 | 
					from django.shortcuts import get_object_or_404
 | 
				
			||||||
 | 
					from django.utils import timezone as djangotime
 | 
				
			||||||
from rest_framework.views import APIView
 | 
					from rest_framework.permissions import IsAuthenticated
 | 
				
			||||||
from rest_framework.response import Response
 | 
					from rest_framework.response import Response
 | 
				
			||||||
from rest_framework import status
 | 
					from rest_framework.views import APIView
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .models import Alert
 | 
					from tacticalrmm.utils import notify_error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .serializers import AlertSerializer
 | 
					from .models import Alert, AlertTemplate
 | 
				
			||||||
 | 
					from .permissions import ManageAlertsPerms
 | 
				
			||||||
 | 
					from .serializers import (
 | 
				
			||||||
 | 
					    AlertSerializer,
 | 
				
			||||||
 | 
					    AlertTemplateRelationSerializer,
 | 
				
			||||||
 | 
					    AlertTemplateSerializer,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					from .tasks import cache_agents_alert_template
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class GetAddAlerts(APIView):
 | 
					class GetAddAlerts(APIView):
 | 
				
			||||||
    def get(self, request):
 | 
					    permission_classes = [IsAuthenticated, ManageAlertsPerms]
 | 
				
			||||||
        alerts = Alert.objects.all()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return Response(AlertSerializer(alerts, many=True).data)
 | 
					    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()
 | 
				
			||||||
 | 
					            return Response(
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    "alerts_count": count,
 | 
				
			||||||
 | 
					                    "alerts": AlertSerializer(alerts, many=True).data,
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        elif any(
 | 
				
			||||||
 | 
					            key
 | 
				
			||||||
 | 
					            in [
 | 
				
			||||||
 | 
					                "timeFilter",
 | 
				
			||||||
 | 
					                "clientFilter",
 | 
				
			||||||
 | 
					                "severityFilter",
 | 
				
			||||||
 | 
					                "resolvedFilter",
 | 
				
			||||||
 | 
					                "snoozedFilter",
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					            for key in request.data.keys()
 | 
				
			||||||
 | 
					        ):
 | 
				
			||||||
 | 
					            clientFilter = Q()
 | 
				
			||||||
 | 
					            severityFilter = Q()
 | 
				
			||||||
 | 
					            timeFilter = Q()
 | 
				
			||||||
 | 
					            resolvedFilter = Q()
 | 
				
			||||||
 | 
					            snoozedFilter = Q()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (
 | 
				
			||||||
 | 
					                "snoozedFilter" in request.data.keys()
 | 
				
			||||||
 | 
					                and not request.data["snoozedFilter"]
 | 
				
			||||||
 | 
					            ):
 | 
				
			||||||
 | 
					                snoozedFilter = Q(snoozed=request.data["snoozedFilter"])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (
 | 
				
			||||||
 | 
					                "resolvedFilter" in request.data.keys()
 | 
				
			||||||
 | 
					                and not request.data["resolvedFilter"]
 | 
				
			||||||
 | 
					            ):
 | 
				
			||||||
 | 
					                resolvedFilter = Q(resolved=request.data["resolvedFilter"])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if "clientFilter" in request.data.keys():
 | 
				
			||||||
 | 
					                from agents.models import Agent
 | 
				
			||||||
 | 
					                from clients.models import Client
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                clients = Client.objects.filter(
 | 
				
			||||||
 | 
					                    pk__in=request.data["clientFilter"]
 | 
				
			||||||
 | 
					                ).values_list("id")
 | 
				
			||||||
 | 
					                agents = Agent.objects.filter(site__client_id__in=clients).values_list(
 | 
				
			||||||
 | 
					                    "id"
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                clientFilter = Q(agent__in=agents)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if "severityFilter" in request.data.keys():
 | 
				
			||||||
 | 
					                severityFilter = Q(severity__in=request.data["severityFilter"])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if "timeFilter" in request.data.keys():
 | 
				
			||||||
 | 
					                timeFilter = Q(
 | 
				
			||||||
 | 
					                    alert_time__lte=djangotime.make_aware(dt.today()),
 | 
				
			||||||
 | 
					                    alert_time__gt=djangotime.make_aware(dt.today())
 | 
				
			||||||
 | 
					                    - djangotime.timedelta(days=int(request.data["timeFilter"])),
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            alerts = (
 | 
				
			||||||
 | 
					                Alert.objects.filter(clientFilter)
 | 
				
			||||||
 | 
					                .filter(severityFilter)
 | 
				
			||||||
 | 
					                .filter(resolvedFilter)
 | 
				
			||||||
 | 
					                .filter(snoozedFilter)
 | 
				
			||||||
 | 
					                .filter(timeFilter)
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            return Response(AlertSerializer(alerts, many=True).data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            alerts = Alert.objects.all()
 | 
				
			||||||
 | 
					            return Response(AlertSerializer(alerts, many=True).data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def post(self, request):
 | 
					    def post(self, request):
 | 
				
			||||||
        serializer = AlertSerializer(data=request.data, partial=True)
 | 
					        serializer = AlertSerializer(data=request.data, partial=True)
 | 
				
			||||||
@@ -24,6 +113,8 @@ class GetAddAlerts(APIView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class GetUpdateDeleteAlert(APIView):
 | 
					class GetUpdateDeleteAlert(APIView):
 | 
				
			||||||
 | 
					    permission_classes = [IsAuthenticated, ManageAlertsPerms]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get(self, request, pk):
 | 
					    def get(self, request, pk):
 | 
				
			||||||
        alert = get_object_or_404(Alert, pk=pk)
 | 
					        alert = get_object_or_404(Alert, pk=pk)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -32,7 +123,40 @@ class GetUpdateDeleteAlert(APIView):
 | 
				
			|||||||
    def put(self, request, pk):
 | 
					    def put(self, request, pk):
 | 
				
			||||||
        alert = get_object_or_404(Alert, pk=pk)
 | 
					        alert = get_object_or_404(Alert, pk=pk)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        serializer = AlertSerializer(instance=alert, data=request.data, partial=True)
 | 
					        data = request.data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if "type" in data.keys():
 | 
				
			||||||
 | 
					            if data["type"] == "resolve":
 | 
				
			||||||
 | 
					                data = {
 | 
				
			||||||
 | 
					                    "resolved": True,
 | 
				
			||||||
 | 
					                    "resolved_on": djangotime.now(),
 | 
				
			||||||
 | 
					                    "snoozed": False,
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                # unable to set snooze_until to none in serialzier
 | 
				
			||||||
 | 
					                alert.snooze_until = None
 | 
				
			||||||
 | 
					                alert.save()
 | 
				
			||||||
 | 
					            elif data["type"] == "snooze":
 | 
				
			||||||
 | 
					                if "snooze_days" in data.keys():
 | 
				
			||||||
 | 
					                    data = {
 | 
				
			||||||
 | 
					                        "snoozed": True,
 | 
				
			||||||
 | 
					                        "snooze_until": djangotime.now()
 | 
				
			||||||
 | 
					                        + djangotime.timedelta(days=int(data["snooze_days"])),
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                else:
 | 
				
			||||||
 | 
					                    return notify_error(
 | 
				
			||||||
 | 
					                        "Missing 'snoozed_days' when trying to snooze alert"
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					            elif data["type"] == "unsnooze":
 | 
				
			||||||
 | 
					                data = {"snoozed": False}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                # unable to set snooze_until to none in serialzier
 | 
				
			||||||
 | 
					                alert.snooze_until = None
 | 
				
			||||||
 | 
					                alert.save()
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                return notify_error("There was an error in the request data")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        serializer = AlertSerializer(instance=alert, data=data, partial=True)
 | 
				
			||||||
        serializer.is_valid(raise_exception=True)
 | 
					        serializer.is_valid(raise_exception=True)
 | 
				
			||||||
        serializer.save()
 | 
					        serializer.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -42,3 +166,83 @@ class GetUpdateDeleteAlert(APIView):
 | 
				
			|||||||
        Alert.objects.get(pk=pk).delete()
 | 
					        Alert.objects.get(pk=pk).delete()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return Response("ok")
 | 
					        return Response("ok")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class BulkAlerts(APIView):
 | 
				
			||||||
 | 
					    permission_classes = [IsAuthenticated, ManageAlertsPerms]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def post(self, request):
 | 
				
			||||||
 | 
					        if request.data["bulk_action"] == "resolve":
 | 
				
			||||||
 | 
					            Alert.objects.filter(id__in=request.data["alerts"]).update(
 | 
				
			||||||
 | 
					                resolved=True,
 | 
				
			||||||
 | 
					                resolved_on=djangotime.now(),
 | 
				
			||||||
 | 
					                snoozed=False,
 | 
				
			||||||
 | 
					                snooze_until=None,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            return Response("ok")
 | 
				
			||||||
 | 
					        elif request.data["bulk_action"] == "snooze":
 | 
				
			||||||
 | 
					            if "snooze_days" in request.data.keys():
 | 
				
			||||||
 | 
					                Alert.objects.filter(id__in=request.data["alerts"]).update(
 | 
				
			||||||
 | 
					                    snoozed=True,
 | 
				
			||||||
 | 
					                    snooze_until=djangotime.now()
 | 
				
			||||||
 | 
					                    + djangotime.timedelta(days=int(request.data["snooze_days"])),
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                return Response("ok")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return notify_error("The request was invalid")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class GetAddAlertTemplates(APIView):
 | 
				
			||||||
 | 
					    permission_classes = [IsAuthenticated, ManageAlertsPerms]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get(self, request):
 | 
				
			||||||
 | 
					        alert_templates = AlertTemplate.objects.all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Response(AlertTemplateSerializer(alert_templates, many=True).data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def post(self, request):
 | 
				
			||||||
 | 
					        serializer = AlertTemplateSerializer(data=request.data, partial=True)
 | 
				
			||||||
 | 
					        serializer.is_valid(raise_exception=True)
 | 
				
			||||||
 | 
					        serializer.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # cache alert_template value on agents
 | 
				
			||||||
 | 
					        cache_agents_alert_template.delay()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Response("ok")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class GetUpdateDeleteAlertTemplate(APIView):
 | 
				
			||||||
 | 
					    permission_classes = [IsAuthenticated, ManageAlertsPerms]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get(self, request, pk):
 | 
				
			||||||
 | 
					        alert_template = get_object_or_404(AlertTemplate, pk=pk)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Response(AlertTemplateSerializer(alert_template).data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def put(self, request, pk):
 | 
				
			||||||
 | 
					        alert_template = get_object_or_404(AlertTemplate, pk=pk)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        serializer = AlertTemplateSerializer(
 | 
				
			||||||
 | 
					            instance=alert_template, data=request.data, partial=True
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        serializer.is_valid(raise_exception=True)
 | 
				
			||||||
 | 
					        serializer.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # cache alert_template value on agents
 | 
				
			||||||
 | 
					        cache_agents_alert_template.delay()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Response("ok")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def delete(self, request, pk):
 | 
				
			||||||
 | 
					        get_object_or_404(AlertTemplate, pk=pk).delete()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # cache alert_template value on agents
 | 
				
			||||||
 | 
					        cache_agents_alert_template.delay()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Response("ok")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class RelatedAlertTemplate(APIView):
 | 
				
			||||||
 | 
					    def get(self, request, pk):
 | 
				
			||||||
 | 
					        alert_template = get_object_or_404(AlertTemplate, pk=pk)
 | 
				
			||||||
 | 
					        return Response(AlertTemplateRelationSerializer(alert_template).data)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +0,0 @@
 | 
				
			|||||||
from django.apps import AppConfig
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Apiv2Config(AppConfig):
 | 
					 | 
				
			||||||
    name = 'apiv2'
 | 
					 | 
				
			||||||
@@ -1,38 +0,0 @@
 | 
				
			|||||||
from tacticalrmm.test import TacticalTestCase
 | 
					 | 
				
			||||||
from unittest.mock import patch
 | 
					 | 
				
			||||||
from model_bakery import baker
 | 
					 | 
				
			||||||
from itertools import cycle
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class TestAPIv2(TacticalTestCase):
 | 
					 | 
				
			||||||
    def setUp(self):
 | 
					 | 
				
			||||||
        self.authenticate()
 | 
					 | 
				
			||||||
        self.setup_coresettings()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @patch("agents.models.Agent.salt_api_cmd")
 | 
					 | 
				
			||||||
    def test_sync_modules(self, mock_ret):
 | 
					 | 
				
			||||||
        # setup data
 | 
					 | 
				
			||||||
        agent = baker.make_recipe("agents.agent")
 | 
					 | 
				
			||||||
        url = "/api/v2/saltminion/"
 | 
					 | 
				
			||||||
        payload = {"agent_id": agent.agent_id}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        mock_ret.return_value = "error"
 | 
					 | 
				
			||||||
        r = self.client.patch(url, payload, format="json")
 | 
					 | 
				
			||||||
        self.assertEqual(r.status_code, 400)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        mock_ret.return_value = []
 | 
					 | 
				
			||||||
        r = self.client.patch(url, payload, format="json")
 | 
					 | 
				
			||||||
        self.assertEqual(r.status_code, 200)
 | 
					 | 
				
			||||||
        self.assertEqual(r.data, "Modules are already in sync")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        mock_ret.return_value = ["modules.win_agent"]
 | 
					 | 
				
			||||||
        r = self.client.patch(url, payload, format="json")
 | 
					 | 
				
			||||||
        self.assertEqual(r.status_code, 200)
 | 
					 | 
				
			||||||
        self.assertEqual(r.data, "Successfully synced salt modules")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        mock_ret.return_value = ["askdjaskdjasd", "modules.win_agent"]
 | 
					 | 
				
			||||||
        r = self.client.patch(url, payload, format="json")
 | 
					 | 
				
			||||||
        self.assertEqual(r.status_code, 200)
 | 
					 | 
				
			||||||
        self.assertEqual(r.data, "Successfully synced salt modules")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.check_not_authenticated("patch", url)
 | 
					 | 
				
			||||||
@@ -1,14 +0,0 @@
 | 
				
			|||||||
from django.urls import path
 | 
					 | 
				
			||||||
from . import views
 | 
					 | 
				
			||||||
from apiv3 import views as v3_views
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
urlpatterns = [
 | 
					 | 
				
			||||||
    path("newagent/", v3_views.NewAgent.as_view()),
 | 
					 | 
				
			||||||
    path("meshexe/", v3_views.MeshExe.as_view()),
 | 
					 | 
				
			||||||
    path("saltminion/", v3_views.SaltMinion.as_view()),
 | 
					 | 
				
			||||||
    path("<str:agentid>/saltminion/", v3_views.SaltMinion.as_view()),
 | 
					 | 
				
			||||||
    path("sysinfo/", v3_views.SysInfo.as_view()),
 | 
					 | 
				
			||||||
    path("hello/", v3_views.Hello.as_view()),
 | 
					 | 
				
			||||||
    path("checkrunner/", views.CheckRunner.as_view()),
 | 
					 | 
				
			||||||
    path("<str:agentid>/checkrunner/", views.CheckRunner.as_view()),
 | 
					 | 
				
			||||||
]
 | 
					 | 
				
			||||||
@@ -1,41 +0,0 @@
 | 
				
			|||||||
from django.shortcuts import get_object_or_404
 | 
					 | 
				
			||||||
from django.utils import timezone as djangotime
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from rest_framework.authentication import TokenAuthentication
 | 
					 | 
				
			||||||
from rest_framework.permissions import IsAuthenticated
 | 
					 | 
				
			||||||
from rest_framework.response import Response
 | 
					 | 
				
			||||||
from rest_framework.views import APIView
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from agents.models import Agent
 | 
					 | 
				
			||||||
from checks.models import Check
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from checks.serializers import CheckRunnerGetSerializerV2
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class CheckRunner(APIView):
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    For the windows python agent
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    authentication_classes = [TokenAuthentication]
 | 
					 | 
				
			||||||
    permission_classes = [IsAuthenticated]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get(self, request, agentid):
 | 
					 | 
				
			||||||
        agent = get_object_or_404(Agent, agent_id=agentid)
 | 
					 | 
				
			||||||
        agent.last_seen = djangotime.now()
 | 
					 | 
				
			||||||
        agent.save(update_fields=["last_seen"])
 | 
					 | 
				
			||||||
        checks = Check.objects.filter(agent__pk=agent.pk, overriden_by_policy=False)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        ret = {
 | 
					 | 
				
			||||||
            "agent": agent.pk,
 | 
					 | 
				
			||||||
            "check_interval": agent.check_interval,
 | 
					 | 
				
			||||||
            "checks": CheckRunnerGetSerializerV2(checks, many=True).data,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return Response(ret)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def patch(self, request):
 | 
					 | 
				
			||||||
        check = get_object_or_404(Check, pk=request.data["id"])
 | 
					 | 
				
			||||||
        check.last_run = djangotime.now()
 | 
					 | 
				
			||||||
        check.save(update_fields=["last_run"])
 | 
					 | 
				
			||||||
        status = check.handle_checkv2(request.data)
 | 
					 | 
				
			||||||
        return Response(status)
 | 
					 | 
				
			||||||
@@ -1,11 +1,13 @@
 | 
				
			|||||||
import os
 | 
					 | 
				
			||||||
import json
 | 
					import json
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					from unittest.mock import patch
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.conf import settings
 | 
					from django.conf import settings
 | 
				
			||||||
from tacticalrmm.test import TacticalTestCase
 | 
					from django.utils import timezone as djangotime
 | 
				
			||||||
from unittest.mock import patch
 | 
					 | 
				
			||||||
from model_bakery import baker
 | 
					from model_bakery import baker
 | 
				
			||||||
from itertools import cycle
 | 
					
 | 
				
			||||||
 | 
					from autotasks.models import AutomatedTask
 | 
				
			||||||
 | 
					from tacticalrmm.test import TacticalTestCase
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TestAPIv3(TacticalTestCase):
 | 
					class TestAPIv3(TacticalTestCase):
 | 
				
			||||||
@@ -17,8 +19,44 @@ class TestAPIv3(TacticalTestCase):
 | 
				
			|||||||
    def test_get_checks(self):
 | 
					    def test_get_checks(self):
 | 
				
			||||||
        url = f"/api/v3/{self.agent.agent_id}/checkrunner/"
 | 
					        url = f"/api/v3/{self.agent.agent_id}/checkrunner/"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # add a check
 | 
				
			||||||
 | 
					        check1 = baker.make_recipe("checks.ping_check", agent=self.agent)
 | 
				
			||||||
        r = self.client.get(url)
 | 
					        r = self.client.get(url)
 | 
				
			||||||
        self.assertEqual(r.status_code, 200)
 | 
					        self.assertEqual(r.status_code, 200)
 | 
				
			||||||
 | 
					        self.assertEqual(r.data["check_interval"], self.agent.check_interval)  # type: ignore
 | 
				
			||||||
 | 
					        self.assertEqual(len(r.data["checks"]), 1)  # type: ignore
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # override check run interval
 | 
				
			||||||
 | 
					        check2 = baker.make_recipe(
 | 
				
			||||||
 | 
					            "checks.ping_check", agent=self.agent, run_interval=20
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        r = self.client.get(url)
 | 
				
			||||||
 | 
					        self.assertEqual(r.status_code, 200)
 | 
				
			||||||
 | 
					        self.assertEqual(r.data["check_interval"], 20)  # type: ignore
 | 
				
			||||||
 | 
					        self.assertEqual(len(r.data["checks"]), 2)  # type: ignore
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Set last_run on both checks and should return an empty list
 | 
				
			||||||
 | 
					        check1.last_run = djangotime.now()
 | 
				
			||||||
 | 
					        check1.save()
 | 
				
			||||||
 | 
					        check2.last_run = djangotime.now()
 | 
				
			||||||
 | 
					        check2.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        r = self.client.get(url)
 | 
				
			||||||
 | 
					        self.assertEqual(r.status_code, 200)
 | 
				
			||||||
 | 
					        self.assertEqual(r.data["check_interval"], 20)  # type: ignore
 | 
				
			||||||
 | 
					        self.assertFalse(r.data["checks"])  # type: ignore
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # set last_run greater than interval
 | 
				
			||||||
 | 
					        check1.last_run = djangotime.now() - djangotime.timedelta(seconds=200)
 | 
				
			||||||
 | 
					        check1.save()
 | 
				
			||||||
 | 
					        check2.last_run = djangotime.now() - djangotime.timedelta(seconds=200)
 | 
				
			||||||
 | 
					        check2.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        r = self.client.get(url)
 | 
				
			||||||
 | 
					        self.assertEqual(r.status_code, 200)
 | 
				
			||||||
 | 
					        self.assertEqual(r.data["check_interval"], 20)  # type: ignore
 | 
				
			||||||
 | 
					        self.assertEquals(len(r.data["checks"]), 2)  # type: ignore
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        url = "/api/v3/Maj34ACb324j234asdj2n34kASDjh34-DESKTOPTEST123/checkrunner/"
 | 
					        url = "/api/v3/Maj34ACb324j234asdj2n34kASDjh34-DESKTOPTEST123/checkrunner/"
 | 
				
			||||||
        r = self.client.get(url)
 | 
					        r = self.client.get(url)
 | 
				
			||||||
@@ -26,42 +64,10 @@ class TestAPIv3(TacticalTestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        self.check_not_authenticated("get", url)
 | 
					        self.check_not_authenticated("get", url)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_get_salt_minion(self):
 | 
					 | 
				
			||||||
        url = f"/api/v3/{self.agent.agent_id}/saltminion/"
 | 
					 | 
				
			||||||
        url2 = f"/api/v2/{self.agent.agent_id}/saltminion/"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        r = self.client.get(url)
 | 
					 | 
				
			||||||
        self.assertEqual(r.status_code, 200)
 | 
					 | 
				
			||||||
        self.assertIn("latestVer", r.json().keys())
 | 
					 | 
				
			||||||
        self.assertIn("currentVer", r.json().keys())
 | 
					 | 
				
			||||||
        self.assertIn("salt_id", r.json().keys())
 | 
					 | 
				
			||||||
        self.assertIn("downloadURL", r.json().keys())
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        r2 = self.client.get(url2)
 | 
					 | 
				
			||||||
        self.assertEqual(r2.status_code, 200)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.check_not_authenticated("get", url)
 | 
					 | 
				
			||||||
        self.check_not_authenticated("get", url2)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def test_get_mesh_info(self):
 | 
					 | 
				
			||||||
        url = f"/api/v3/{self.agent.pk}/meshinfo/"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        r = self.client.get(url)
 | 
					 | 
				
			||||||
        self.assertEqual(r.status_code, 200)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.check_not_authenticated("get", url)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def test_get_winupdater(self):
 | 
					 | 
				
			||||||
        url = f"/api/v3/{self.agent.agent_id}/winupdater/"
 | 
					 | 
				
			||||||
        r = self.client.get(url)
 | 
					 | 
				
			||||||
        self.assertEqual(r.status_code, 200)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.check_not_authenticated("get", url)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def test_sysinfo(self):
 | 
					    def test_sysinfo(self):
 | 
				
			||||||
        # TODO replace this with golang wmi sample data
 | 
					        # TODO replace this with golang wmi sample data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        url = f"/api/v3/sysinfo/"
 | 
					        url = "/api/v3/sysinfo/"
 | 
				
			||||||
        with open(
 | 
					        with open(
 | 
				
			||||||
            os.path.join(
 | 
					            os.path.join(
 | 
				
			||||||
                settings.BASE_DIR, "tacticalrmm/test_data/wmi_python_agent.json"
 | 
					                settings.BASE_DIR, "tacticalrmm/test_data/wmi_python_agent.json"
 | 
				
			||||||
@@ -76,19 +82,261 @@ class TestAPIv3(TacticalTestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        self.check_not_authenticated("patch", url)
 | 
					        self.check_not_authenticated("patch", url)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_hello_patch(self):
 | 
					    def test_checkrunner_interval(self):
 | 
				
			||||||
        url = f"/api/v3/hello/"
 | 
					        url = f"/api/v3/{self.agent.agent_id}/checkinterval/"
 | 
				
			||||||
 | 
					        r = self.client.get(url, format="json")
 | 
				
			||||||
 | 
					        self.assertEqual(r.status_code, 200)
 | 
				
			||||||
 | 
					        self.assertEqual(
 | 
				
			||||||
 | 
					            r.json(),
 | 
				
			||||||
 | 
					            {"agent": self.agent.pk, "check_interval": self.agent.check_interval},
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # add check to agent with check interval set
 | 
				
			||||||
 | 
					        check = 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)
 | 
				
			||||||
 | 
					        self.assertEqual(
 | 
				
			||||||
 | 
					            r.json(),
 | 
				
			||||||
 | 
					            {"agent": self.agent.pk, "check_interval": 30},
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # minimum check run interval is 15 seconds
 | 
				
			||||||
 | 
					        check = 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)
 | 
				
			||||||
 | 
					        self.assertEqual(
 | 
				
			||||||
 | 
					            r.json(),
 | 
				
			||||||
 | 
					            {"agent": self.agent.pk, "check_interval": 15},
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_run_checks(self):
 | 
				
			||||||
 | 
					        # force run all checks regardless of interval
 | 
				
			||||||
 | 
					        agent = baker.make_recipe("agents.online_agent")
 | 
				
			||||||
 | 
					        baker.make_recipe("checks.ping_check", agent=agent)
 | 
				
			||||||
 | 
					        baker.make_recipe("checks.diskspace_check", agent=agent)
 | 
				
			||||||
 | 
					        baker.make_recipe("checks.cpuload_check", agent=agent)
 | 
				
			||||||
 | 
					        baker.make_recipe("checks.memory_check", agent=agent)
 | 
				
			||||||
 | 
					        baker.make_recipe("checks.eventlog_check", agent=agent)
 | 
				
			||||||
 | 
					        for _ in range(10):
 | 
				
			||||||
 | 
					            baker.make_recipe("checks.script_check", agent=agent)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        url = f"/api/v3/{agent.agent_id}/runchecks/"
 | 
				
			||||||
 | 
					        r = self.client.get(url)
 | 
				
			||||||
 | 
					        self.assertEqual(r.json()["agent"], agent.pk)
 | 
				
			||||||
 | 
					        self.assertIsInstance(r.json()["check_interval"], int)
 | 
				
			||||||
 | 
					        self.assertEqual(len(r.json()["checks"]), 15)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_checkin_patch(self):
 | 
				
			||||||
 | 
					        from logs.models import PendingAction
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        url = "/api/v3/checkin/"
 | 
				
			||||||
 | 
					        agent_updated = baker.make_recipe("agents.agent", version="1.3.0")
 | 
				
			||||||
 | 
					        PendingAction.objects.create(
 | 
				
			||||||
 | 
					            agent=agent_updated,
 | 
				
			||||||
 | 
					            action_type="agentupdate",
 | 
				
			||||||
 | 
					            details={
 | 
				
			||||||
 | 
					                "url": agent_updated.winagent_dl,
 | 
				
			||||||
 | 
					                "version": agent_updated.version,
 | 
				
			||||||
 | 
					                "inno": agent_updated.win_inno_exe,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        action = agent_updated.pendingactions.filter(action_type="agentupdate").first()
 | 
				
			||||||
 | 
					        self.assertEqual(action.status, "pending")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # test agent failed to update and still on same version
 | 
				
			||||||
        payload = {
 | 
					        payload = {
 | 
				
			||||||
            "agent_id": self.agent.agent_id,
 | 
					            "func": "hello",
 | 
				
			||||||
            "logged_in_username": "None",
 | 
					            "agent_id": agent_updated.agent_id,
 | 
				
			||||||
            "disks": [],
 | 
					            "version": "1.3.0",
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        r = self.client.patch(url, payload, format="json")
 | 
				
			||||||
 | 
					        self.assertEqual(r.status_code, 200)
 | 
				
			||||||
 | 
					        action = agent_updated.pendingactions.filter(action_type="agentupdate").first()
 | 
				
			||||||
 | 
					        self.assertEqual(action.status, "pending")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # test agent successful update
 | 
				
			||||||
 | 
					        payload["version"] = settings.LATEST_AGENT_VER
 | 
				
			||||||
 | 
					        r = self.client.patch(url, payload, format="json")
 | 
				
			||||||
 | 
					        self.assertEqual(r.status_code, 200)
 | 
				
			||||||
 | 
					        action = agent_updated.pendingactions.filter(action_type="agentupdate").first()
 | 
				
			||||||
 | 
					        self.assertEqual(action.status, "completed")
 | 
				
			||||||
 | 
					        action.delete()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @patch("apiv3.views.reload_nats")
 | 
				
			||||||
 | 
					    def test_agent_recovery(self, reload_nats):
 | 
				
			||||||
 | 
					        reload_nats.return_value = "ok"
 | 
				
			||||||
 | 
					        r = self.client.get("/api/v3/34jahsdkjasncASDjhg2b3j4r/recover/")
 | 
				
			||||||
 | 
					        self.assertEqual(r.status_code, 404)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        agent = baker.make_recipe("agents.online_agent")
 | 
				
			||||||
 | 
					        url = f"/api/v3/{agent.agent_id}/recovery/"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        r = self.client.get(url)
 | 
				
			||||||
 | 
					        self.assertEqual(r.status_code, 200)
 | 
				
			||||||
 | 
					        self.assertEqual(r.json(), {"mode": "pass", "shellcmd": ""})
 | 
				
			||||||
 | 
					        reload_nats.assert_not_called()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        baker.make("agents.RecoveryAction", agent=agent, mode="mesh")
 | 
				
			||||||
 | 
					        r = self.client.get(url)
 | 
				
			||||||
 | 
					        self.assertEqual(r.status_code, 200)
 | 
				
			||||||
 | 
					        self.assertEqual(r.json(), {"mode": "mesh", "shellcmd": ""})
 | 
				
			||||||
 | 
					        reload_nats.assert_not_called()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        baker.make(
 | 
				
			||||||
 | 
					            "agents.RecoveryAction",
 | 
				
			||||||
 | 
					            agent=agent,
 | 
				
			||||||
 | 
					            mode="command",
 | 
				
			||||||
 | 
					            command="shutdown /r /t 5 /f",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        r = self.client.get(url)
 | 
				
			||||||
 | 
					        self.assertEqual(r.status_code, 200)
 | 
				
			||||||
 | 
					        self.assertEqual(
 | 
				
			||||||
 | 
					            r.json(), {"mode": "command", "shellcmd": "shutdown /r /t 5 /f"}
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        reload_nats.assert_not_called()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        baker.make("agents.RecoveryAction", agent=agent, mode="rpc")
 | 
				
			||||||
 | 
					        r = self.client.get(url)
 | 
				
			||||||
 | 
					        self.assertEqual(r.status_code, 200)
 | 
				
			||||||
 | 
					        self.assertEqual(r.json(), {"mode": "rpc", "shellcmd": ""})
 | 
				
			||||||
 | 
					        reload_nats.assert_called_once()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_task_runner_get(self):
 | 
				
			||||||
 | 
					        from autotasks.serializers import TaskGOGetSerializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        r = self.client.get("/api/v3/500/asdf9df9dfdf/taskrunner/")
 | 
				
			||||||
 | 
					        self.assertEqual(r.status_code, 404)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # setup data
 | 
				
			||||||
 | 
					        agent = baker.make_recipe("agents.agent")
 | 
				
			||||||
 | 
					        script = baker.make_recipe("scripts.script")
 | 
				
			||||||
 | 
					        task = baker.make("autotasks.AutomatedTask", agent=agent, script=script)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        url = f"/api/v3/{task.pk}/{agent.agent_id}/taskrunner/"  # type: ignore
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        r = self.client.get(url)
 | 
				
			||||||
 | 
					        self.assertEqual(r.status_code, 200)
 | 
				
			||||||
 | 
					        self.assertEqual(TaskGOGetSerializer(task).data, r.data)  # type: ignore
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_task_runner_results(self):
 | 
				
			||||||
 | 
					        from agents.models import AgentCustomField
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        r = self.client.patch("/api/v3/500/asdf9df9dfdf/taskrunner/")
 | 
				
			||||||
 | 
					        self.assertEqual(r.status_code, 404)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # setup data
 | 
				
			||||||
 | 
					        agent = baker.make_recipe("agents.agent")
 | 
				
			||||||
 | 
					        task = baker.make("autotasks.AutomatedTask", agent=agent)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        url = f"/api/v3/{task.pk}/{agent.agent_id}/taskrunner/"  # type: ignore
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # test passing task
 | 
				
			||||||
 | 
					        data = {
 | 
				
			||||||
 | 
					            "stdout": "test test \ntestest stdgsd\n",
 | 
				
			||||||
 | 
					            "stderr": "",
 | 
				
			||||||
 | 
					            "retcode": 0,
 | 
				
			||||||
 | 
					            "execution_time": 3.560,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        r = self.client.patch(url, payload, format="json")
 | 
					        r = self.client.patch(url, data)
 | 
				
			||||||
        self.assertEqual(r.status_code, 200)
 | 
					        self.assertEqual(r.status_code, 200)
 | 
				
			||||||
 | 
					        self.assertTrue(AutomatedTask.objects.get(pk=task.pk).status == "passing")  # type: ignore
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        payload["logged_in_username"] = "Bob"
 | 
					        # test failing task
 | 
				
			||||||
        r = self.client.patch(url, payload, format="json")
 | 
					        data = {
 | 
				
			||||||
 | 
					            "stdout": "test test \ntestest stdgsd\n",
 | 
				
			||||||
 | 
					            "stderr": "",
 | 
				
			||||||
 | 
					            "retcode": 1,
 | 
				
			||||||
 | 
					            "execution_time": 3.560,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        r = self.client.patch(url, data)
 | 
				
			||||||
        self.assertEqual(r.status_code, 200)
 | 
					        self.assertEqual(r.status_code, 200)
 | 
				
			||||||
 | 
					        self.assertTrue(AutomatedTask.objects.get(pk=task.pk).status == "failing")  # type: ignore
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.check_not_authenticated("patch", url)
 | 
					        # test collector task
 | 
				
			||||||
 | 
					        text = baker.make("core.CustomField", model="agent", type="text", name="Test")
 | 
				
			||||||
 | 
					        boolean = baker.make(
 | 
				
			||||||
 | 
					            "core.CustomField", model="agent", type="checkbox", name="Test1"
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        multiple = baker.make(
 | 
				
			||||||
 | 
					            "core.CustomField", model="agent", type="multiple", name="Test2"
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # test text fields
 | 
				
			||||||
 | 
					        task.custom_field = text  # type: ignore
 | 
				
			||||||
 | 
					        task.save()  # type: ignore
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # test failing failing with stderr
 | 
				
			||||||
 | 
					        data = {
 | 
				
			||||||
 | 
					            "stdout": "test test \nthe last line",
 | 
				
			||||||
 | 
					            "stderr": "This is an error",
 | 
				
			||||||
 | 
					            "retcode": 1,
 | 
				
			||||||
 | 
					            "execution_time": 3.560,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        r = self.client.patch(url, data)
 | 
				
			||||||
 | 
					        self.assertEqual(r.status_code, 200)
 | 
				
			||||||
 | 
					        self.assertTrue(AutomatedTask.objects.get(pk=task.pk).status == "failing")  # type: ignore
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # test saving to text field
 | 
				
			||||||
 | 
					        data = {
 | 
				
			||||||
 | 
					            "stdout": "test test \nthe last line",
 | 
				
			||||||
 | 
					            "stderr": "",
 | 
				
			||||||
 | 
					            "retcode": 0,
 | 
				
			||||||
 | 
					            "execution_time": 3.560,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        r = self.client.patch(url, data)
 | 
				
			||||||
 | 
					        self.assertEqual(r.status_code, 200)
 | 
				
			||||||
 | 
					        self.assertEqual(AutomatedTask.objects.get(pk=task.pk).status, "passing")  # type: ignore
 | 
				
			||||||
 | 
					        self.assertEqual(AgentCustomField.objects.get(field=text, agent=task.agent).value, "the last line")  # type: ignore
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # test saving to checkbox field
 | 
				
			||||||
 | 
					        task.custom_field = boolean  # type: ignore
 | 
				
			||||||
 | 
					        task.save()  # type: ignore
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        data = {
 | 
				
			||||||
 | 
					            "stdout": "1",
 | 
				
			||||||
 | 
					            "stderr": "",
 | 
				
			||||||
 | 
					            "retcode": 0,
 | 
				
			||||||
 | 
					            "execution_time": 3.560,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        r = self.client.patch(url, data)
 | 
				
			||||||
 | 
					        self.assertEqual(r.status_code, 200)
 | 
				
			||||||
 | 
					        self.assertEqual(AutomatedTask.objects.get(pk=task.pk).status, "passing")  # type: ignore
 | 
				
			||||||
 | 
					        self.assertTrue(AgentCustomField.objects.get(field=boolean, agent=task.agent).value)  # type: ignore
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # test saving to multiple field with commas
 | 
				
			||||||
 | 
					        task.custom_field = multiple  # type: ignore
 | 
				
			||||||
 | 
					        task.save()  # type: ignore
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        data = {
 | 
				
			||||||
 | 
					            "stdout": "this,is,an,array",
 | 
				
			||||||
 | 
					            "stderr": "",
 | 
				
			||||||
 | 
					            "retcode": 0,
 | 
				
			||||||
 | 
					            "execution_time": 3.560,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        r = self.client.patch(url, data)
 | 
				
			||||||
 | 
					        self.assertEqual(r.status_code, 200)
 | 
				
			||||||
 | 
					        self.assertEqual(AutomatedTask.objects.get(pk=task.pk).status, "passing")  # type: ignore
 | 
				
			||||||
 | 
					        self.assertEqual(AgentCustomField.objects.get(field=multiple, agent=task.agent).value, ["this", "is", "an", "array"])  # type: ignore
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # test mutiple with a single value
 | 
				
			||||||
 | 
					        data = {
 | 
				
			||||||
 | 
					            "stdout": "this",
 | 
				
			||||||
 | 
					            "stderr": "",
 | 
				
			||||||
 | 
					            "retcode": 0,
 | 
				
			||||||
 | 
					            "execution_time": 3.560,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        r = self.client.patch(url, data)
 | 
				
			||||||
 | 
					        self.assertEqual(r.status_code, 200)
 | 
				
			||||||
 | 
					        self.assertEqual(AutomatedTask.objects.get(pk=task.pk).status, "passing")  # type: ignore
 | 
				
			||||||
 | 
					        self.assertEqual(AgentCustomField.objects.get(field=multiple, agent=task.agent).value, ["this"])  # type: ignore
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,19 +1,23 @@
 | 
				
			|||||||
from django.urls import path
 | 
					from django.urls import path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from . import views
 | 
					from . import views
 | 
				
			||||||
 | 
					
 | 
				
			||||||
urlpatterns = [
 | 
					urlpatterns = [
 | 
				
			||||||
    path("hello/", views.Hello.as_view()),
 | 
					 | 
				
			||||||
    path("checkrunner/", views.CheckRunner.as_view()),
 | 
					    path("checkrunner/", views.CheckRunner.as_view()),
 | 
				
			||||||
    path("<str:agentid>/checkrunner/", views.CheckRunner.as_view()),
 | 
					    path("<str:agentid>/checkrunner/", views.CheckRunner.as_view()),
 | 
				
			||||||
 | 
					    path("<str:agentid>/runchecks/", views.RunChecks.as_view()),
 | 
				
			||||||
 | 
					    path("<str:agentid>/checkinterval/", views.CheckRunnerInterval.as_view()),
 | 
				
			||||||
    path("<int:pk>/<str:agentid>/taskrunner/", views.TaskRunner.as_view()),
 | 
					    path("<int:pk>/<str:agentid>/taskrunner/", views.TaskRunner.as_view()),
 | 
				
			||||||
    path("saltminion/", views.SaltMinion.as_view()),
 | 
					 | 
				
			||||||
    path("<str:agentid>/saltminion/", views.SaltMinion.as_view()),
 | 
					 | 
				
			||||||
    path("<int:pk>/meshinfo/", views.MeshInfo.as_view()),
 | 
					 | 
				
			||||||
    path("meshexe/", views.MeshExe.as_view()),
 | 
					    path("meshexe/", views.MeshExe.as_view()),
 | 
				
			||||||
    path("sysinfo/", views.SysInfo.as_view()),
 | 
					    path("sysinfo/", views.SysInfo.as_view()),
 | 
				
			||||||
    path("newagent/", views.NewAgent.as_view()),
 | 
					    path("newagent/", views.NewAgent.as_view()),
 | 
				
			||||||
    path("winupdater/", views.WinUpdater.as_view()),
 | 
					 | 
				
			||||||
    path("<str:agentid>/winupdater/", views.WinUpdater.as_view()),
 | 
					 | 
				
			||||||
    path("software/", views.Software.as_view()),
 | 
					    path("software/", views.Software.as_view()),
 | 
				
			||||||
    path("installer/", views.Installer.as_view()),
 | 
					    path("installer/", views.Installer.as_view()),
 | 
				
			||||||
 | 
					    path("checkin/", views.CheckIn.as_view()),
 | 
				
			||||||
 | 
					    path("syncmesh/", views.SyncMeshNodeID.as_view()),
 | 
				
			||||||
 | 
					    path("choco/", views.Choco.as_view()),
 | 
				
			||||||
 | 
					    path("winupdates/", views.WinUpdates.as_view()),
 | 
				
			||||||
 | 
					    path("superseded/", views.SupersededWinUpdate.as_view()),
 | 
				
			||||||
 | 
					    path("<int:pk>/chocoresult/", views.ChocoResult.as_view()),
 | 
				
			||||||
 | 
					    path("<str:agentid>/recovery/", views.AgentRecovery.as_view()),
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 
 | 
				
			|||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user