Compare commits
	
		
			913 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					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 | ||
| 
						 | 
					0d2b4af986 | ||
| 
						 | 
					dc2b2eeb9f | ||
| 
						 | 
					e5dbb66d53 | ||
| 
						 | 
					3474b1c471 | ||
| 
						 | 
					3886de5b7c | ||
| 
						 | 
					2b3cec06b3 | ||
| 
						 | 
					8536754d14 | ||
| 
						 | 
					1f36235801 | ||
| 
						 | 
					a4194b14f9 | ||
| 
						 | 
					2dcc629d9d | ||
| 
						 | 
					98ddadc6bc | ||
| 
						 | 
					f6e47b7383 | ||
| 
						 | 
					f073ddc906 | ||
| 
						 | 
					3e00631925 | 
							
								
								
									
										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
 | 
				
			||||||
							
								
								
									
										257
									
								
								.devcontainer/docker-compose.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										257
									
								
								.devcontainer/docker-compose.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,257 @@
 | 
				
			|||||||
 | 
					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
 | 
				
			||||||
 | 
					    image: redis:6.0-alpine
 | 
				
			||||||
 | 
					    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:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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
 | 
				
			||||||
							
								
								
									
										35
									
								
								.devcontainer/requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								.devcontainer/requirements.txt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
				
			|||||||
 | 
					# 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
 | 
				
			||||||
 | 
					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:
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -45,3 +45,5 @@ 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/
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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,5 +1,4 @@
 | 
				
			|||||||
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
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -1,13 +1,43 @@
 | 
				
			|||||||
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"),
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    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",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,13 +1,23 @@
 | 
				
			|||||||
import pyotp
 | 
					import pyotp
 | 
				
			||||||
 | 
					from rest_framework.serializers import ModelSerializer, SerializerMethodField
 | 
				
			||||||
from rest_framework.serializers import (
 | 
					 | 
				
			||||||
    ModelSerializer,
 | 
					 | 
				
			||||||
    SerializerMethodField,
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .models import User
 | 
					from .models import User
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class UserUISerializer(ModelSerializer):
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        model = User
 | 
				
			||||||
 | 
					        fields = [
 | 
				
			||||||
 | 
					            "dark_mode",
 | 
				
			||||||
 | 
					            "show_community_scripts",
 | 
				
			||||||
 | 
					            "agent_dblclick_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
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 = [
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,23 +1,28 @@
 | 
				
			|||||||
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 import status
 | 
				
			||||||
 | 
					from rest_framework.authtoken.serializers import AuthTokenSerializer
 | 
				
			||||||
from rest_framework.permissions import AllowAny
 | 
					from rest_framework.permissions import AllowAny
 | 
				
			||||||
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 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
 | 
				
			||||||
 | 
					from .serializers import TOTPSetupSerializer, UserSerializer, UserUISerializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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 +65,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:
 | 
				
			||||||
@@ -81,7 +86,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"],
 | 
				
			||||||
@@ -108,6 +113,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,7 +123,11 @@ 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")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -124,8 +136,10 @@ class UserActions(APIView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    # 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 +147,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 +176,9 @@ 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"])
 | 
					        )
 | 
				
			||||||
 | 
					        serializer.is_valid(raise_exception=True)
 | 
				
			||||||
 | 
					        serializer.save()
 | 
				
			||||||
        return Response("ok")
 | 
					        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,27 +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
 | 
				
			||||||
        has_failing_checks = True if failing > 0 else False
 | 
					                    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": has_failing_checks,
 | 
					            "warning": warning,
 | 
				
			||||||
 | 
					            "info": info,
 | 
				
			||||||
 | 
					            "has_failing_checks": failing > 0 or warning > 0,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        return ret
 | 
					        return ret
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -185,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 = []
 | 
				
			||||||
@@ -229,6 +268,7 @@ class Agent(BaseAuditModel):
 | 
				
			|||||||
            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
 | 
				
			||||||
@@ -258,33 +298,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"
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -299,7 +413,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
 | 
				
			||||||
@@ -308,21 +422,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
 | 
				
			||||||
@@ -333,21 +460,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:
 | 
				
			||||||
@@ -384,32 +526,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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -437,7 +709,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",
 | 
				
			||||||
@@ -459,7 +731,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
 | 
				
			||||||
@@ -468,77 +744,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
 | 
				
			||||||
@@ -549,41 +754,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:
 | 
				
			||||||
@@ -596,69 +775,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):
 | 
				
			||||||
@@ -666,13 +818,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):
 | 
				
			||||||
@@ -680,7 +833,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):
 | 
				
			||||||
@@ -688,12 +842,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"),
 | 
				
			||||||
@@ -717,12 +869,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(
 | 
				
			||||||
@@ -742,3 +888,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
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,293 +1,280 @@
 | 
				
			|||||||
from loguru import logger
 | 
					import asyncio
 | 
				
			||||||
from time import sleep
 | 
					import datetime as dt
 | 
				
			||||||
import random
 | 
					import random
 | 
				
			||||||
import requests
 | 
					import urllib.parse
 | 
				
			||||||
from packaging import version as pyver
 | 
					from time import sleep
 | 
				
			||||||
 | 
					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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
OLD_64_PY_AGENT = "https://github.com/wh1te909/winagent/releases/download/v0.11.2/winagent-v0.11.2.exe"
 | 
					
 | 
				
			||||||
OLD_32_PY_AGENT = "https://github.com/wh1te909/winagent/releases/download/v0.11.2/winagent-v0.11.2-x86.exe"
 | 
					def agent_update(pk: int, codesigntoken: str = None) -> str:
 | 
				
			||||||
 | 
					    from agents.utils import get_exegen_url
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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
 | 
				
			||||||
 | 
					    if agent.arch is None:
 | 
				
			||||||
 | 
					        logger.warning(
 | 
				
			||||||
 | 
					            f"Unable to determine arch on {agent.hostname}. Skipping agent update."
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        return "noarch"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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 agent.pendingactions.filter(
 | 
				
			||||||
 | 
					        action_type="agentupdate", status="pending"
 | 
				
			||||||
 | 
					    ).exists():
 | 
				
			||||||
 | 
					        agent.pendingactions.filter(
 | 
				
			||||||
 | 
					            action_type="agentupdate", status="pending"
 | 
				
			||||||
 | 
					        ).delete()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    PendingAction.objects.create(
 | 
				
			||||||
 | 
					        agent=agent,
 | 
				
			||||||
 | 
					        action_type="agentupdate",
 | 
				
			||||||
 | 
					        details={
 | 
				
			||||||
 | 
					            "url": url,
 | 
				
			||||||
 | 
					            "version": version,
 | 
				
			||||||
 | 
					            "inno": inno,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    nats_data = {
 | 
				
			||||||
 | 
					        "func": "agentupdate",
 | 
				
			||||||
 | 
					        "payload": {
 | 
				
			||||||
 | 
					            "url": url,
 | 
				
			||||||
 | 
					            "version": version,
 | 
				
			||||||
 | 
					            "inno": inno,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    asyncio.run(agent.nats_cmd(nats_data, wait=False))
 | 
				
			||||||
 | 
					    return "created"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@app.task
 | 
					@app.task
 | 
				
			||||||
def send_agent_update_task(pks, version):
 | 
					def send_agent_update_task(pks: list[int]) -> None:
 | 
				
			||||||
    assert isinstance(pks, list)
 | 
					    try:
 | 
				
			||||||
 | 
					        codesigntoken = CodeSignToken.objects.first().token
 | 
				
			||||||
    q = Agent.objects.filter(pk__in=pks)
 | 
					    except:
 | 
				
			||||||
    agents = [i.pk for i in q if pyver.parse(i.version) < pyver.parse(version)]
 | 
					        codesigntoken = None
 | 
				
			||||||
 | 
					 | 
				
			||||||
    chunks = (agents[i : i + 30] for i in range(0, len(agents), 30))
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    chunks = (pks[i : i + 30] for i in range(0, len(pks), 30))
 | 
				
			||||||
    for chunk in chunks:
 | 
					    for chunk in chunks:
 | 
				
			||||||
        for pk in chunk:
 | 
					        for pk in chunk:
 | 
				
			||||||
            agent = Agent.objects.get(pk=pk)
 | 
					            agent_update(pk, codesigntoken)
 | 
				
			||||||
 | 
					            sleep(0.05)
 | 
				
			||||||
            # skip if we can't determine the arch
 | 
					        sleep(4)
 | 
				
			||||||
            if agent.arch is None:
 | 
					 | 
				
			||||||
                logger.warning(
 | 
					 | 
				
			||||||
                    f"Unable to determine arch on {agent.salt_id}. Skipping."
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                continue
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            # golang agent only backwards compatible with py agent 0.11.2
 | 
					 | 
				
			||||||
            # force an upgrade to the latest python agent if version < 0.11.2
 | 
					 | 
				
			||||||
            if pyver.parse(agent.version) < pyver.parse("0.11.2"):
 | 
					 | 
				
			||||||
                url = OLD_64_PY_AGENT if agent.arch == "64" else OLD_32_PY_AGENT
 | 
					 | 
				
			||||||
                inno = (
 | 
					 | 
				
			||||||
                    "winagent-v0.11.2.exe"
 | 
					 | 
				
			||||||
                    if agent.arch == "64"
 | 
					 | 
				
			||||||
                    else "winagent-v0.11.2-x86.exe"
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
            else:
 | 
					 | 
				
			||||||
                url = agent.winagent_dl
 | 
					 | 
				
			||||||
                inno = agent.win_inno_exe
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if agent.has_nats:
 | 
					 | 
				
			||||||
                if agent.pendingactions.filter(
 | 
					 | 
				
			||||||
                    action_type="agentupdate", status="pending"
 | 
					 | 
				
			||||||
                ).exists():
 | 
					 | 
				
			||||||
                    continue
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                PendingAction.objects.create(
 | 
					 | 
				
			||||||
                    agent=agent,
 | 
					 | 
				
			||||||
                    action_type="agentupdate",
 | 
					 | 
				
			||||||
                    details={
 | 
					 | 
				
			||||||
                        "url": agent.winagent_dl,
 | 
					 | 
				
			||||||
                        "version": settings.LATEST_AGENT_VER,
 | 
					 | 
				
			||||||
                        "inno": agent.win_inno_exe,
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
            # TODO
 | 
					 | 
				
			||||||
            # Salt is deprecated, remove this once salt is gone
 | 
					 | 
				
			||||||
            else:
 | 
					 | 
				
			||||||
                r = agent.salt_api_async(
 | 
					 | 
				
			||||||
                    func="win_agent.do_agent_update_v2",
 | 
					 | 
				
			||||||
                    kwargs={
 | 
					 | 
				
			||||||
                        "inno": inno,
 | 
					 | 
				
			||||||
                        "url": url,
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
        sleep(5)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@app.task
 | 
					@app.task
 | 
				
			||||||
def auto_self_agent_update_task():
 | 
					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")
 | 
				
			||||||
    agents = [
 | 
					    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)
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    chunks = (agents[i : i + 30] for i in range(0, len(agents), 30))
 | 
					    chunks = (pks[i : i + 30] for i in range(0, len(pks), 30))
 | 
				
			||||||
 | 
					 | 
				
			||||||
    for chunk in chunks:
 | 
					    for chunk in chunks:
 | 
				
			||||||
        for pk in chunk:
 | 
					        for pk in chunk:
 | 
				
			||||||
            agent = Agent.objects.get(pk=pk)
 | 
					            agent_update(pk, codesigntoken)
 | 
				
			||||||
 | 
					            sleep(0.05)
 | 
				
			||||||
            # skip if we can't determine the arch
 | 
					        sleep(4)
 | 
				
			||||||
            if agent.arch is None:
 | 
					 | 
				
			||||||
                logger.warning(
 | 
					 | 
				
			||||||
                    f"Unable to determine arch on {agent.salt_id}. Skipping."
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                continue
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            # golang agent only backwards compatible with py agent 0.11.2
 | 
					 | 
				
			||||||
            # force an upgrade to the latest python agent if version < 0.11.2
 | 
					 | 
				
			||||||
            if pyver.parse(agent.version) < pyver.parse("0.11.2"):
 | 
					 | 
				
			||||||
                url = OLD_64_PY_AGENT if agent.arch == "64" else OLD_32_PY_AGENT
 | 
					 | 
				
			||||||
                inno = (
 | 
					 | 
				
			||||||
                    "winagent-v0.11.2.exe"
 | 
					 | 
				
			||||||
                    if agent.arch == "64"
 | 
					 | 
				
			||||||
                    else "winagent-v0.11.2-x86.exe"
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
            else:
 | 
					 | 
				
			||||||
                url = agent.winagent_dl
 | 
					 | 
				
			||||||
                inno = agent.win_inno_exe
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if agent.has_nats:
 | 
					 | 
				
			||||||
                if agent.pendingactions.filter(
 | 
					 | 
				
			||||||
                    action_type="agentupdate", status="pending"
 | 
					 | 
				
			||||||
                ).exists():
 | 
					 | 
				
			||||||
                    continue
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                PendingAction.objects.create(
 | 
					 | 
				
			||||||
                    agent=agent,
 | 
					 | 
				
			||||||
                    action_type="agentupdate",
 | 
					 | 
				
			||||||
                    details={
 | 
					 | 
				
			||||||
                        "url": agent.winagent_dl,
 | 
					 | 
				
			||||||
                        "version": settings.LATEST_AGENT_VER,
 | 
					 | 
				
			||||||
                        "inno": agent.win_inno_exe,
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
            # TODO
 | 
					 | 
				
			||||||
            # Salt is deprecated, remove this once salt is gone
 | 
					 | 
				
			||||||
            else:
 | 
					 | 
				
			||||||
                r = agent.salt_api_async(
 | 
					 | 
				
			||||||
                    func="win_agent.do_agent_update_v2",
 | 
					 | 
				
			||||||
                    kwargs={
 | 
					 | 
				
			||||||
                        "inno": inno,
 | 
					 | 
				
			||||||
                        "url": url,
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
        sleep(5)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@app.task
 | 
					@app.task
 | 
				
			||||||
def sync_salt_modules_task(pk):
 | 
					def agent_outage_email_task(pk: int, alert_interval: Union[float, None] = None) -> str:
 | 
				
			||||||
    agent = Agent.objects.get(pk=pk)
 | 
					    from alerts.models import Alert
 | 
				
			||||||
    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}"
 | 
					    alert = Alert.objects.get(pk=pk)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not alert.email_sent:
 | 
				
			||||||
@app.task
 | 
					        sleep(random.randint(1, 15))
 | 
				
			||||||
def batch_sync_modules_task():
 | 
					        alert.agent.send_outage_email()
 | 
				
			||||||
    # sync modules, split into chunks of 50 agents to not overload salt
 | 
					        alert.email_sent = djangotime.now()
 | 
				
			||||||
    agents = Agent.objects.all()
 | 
					        alert.save(update_fields=["email_sent"])
 | 
				
			||||||
    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:
 | 
					 | 
				
			||||||
        Agent.salt_batch_async(minions=chunk, func="saltutil.sync_modules")
 | 
					 | 
				
			||||||
        sleep(10)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@app.task
 | 
					 | 
				
			||||||
def uninstall_agent_task(salt_id, has_nats):
 | 
					 | 
				
			||||||
    attempts = 0
 | 
					 | 
				
			||||||
    error = False
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if not has_nats:
 | 
					 | 
				
			||||||
        while 1:
 | 
					 | 
				
			||||||
            try:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                r = requests.post(
 | 
					 | 
				
			||||||
                    f"http://{settings.SALT_HOST}:8123/run",
 | 
					 | 
				
			||||||
                    json=[
 | 
					 | 
				
			||||||
                        {
 | 
					 | 
				
			||||||
                            "client": "local",
 | 
					 | 
				
			||||||
                            "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",
 | 
				
			||||||
 | 
					        "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 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,46 @@
 | 
				
			|||||||
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
 | 
					 | 
				
			||||||
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 loguru import logger
 | 
				
			||||||
 | 
					from packaging import version as pyver
 | 
				
			||||||
 | 
					from rest_framework import status
 | 
				
			||||||
from rest_framework.decorators import api_view
 | 
					from rest_framework.decorators import api_view
 | 
				
			||||||
from rest_framework.views import APIView
 | 
					 | 
				
			||||||
from rest_framework.response import Response
 | 
					from rest_framework.response import Response
 | 
				
			||||||
from rest_framework import status, generics
 | 
					from rest_framework.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 .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],
 | 
				
			||||||
@@ -54,9 +51,13 @@ def get_agent_versions(request):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
@api_view(["POST"])
 | 
					@api_view(["POST"])
 | 
				
			||||||
def update_agents(request):
 | 
					def update_agents(request):
 | 
				
			||||||
    pks = request.data["pks"]
 | 
					    q = Agent.objects.filter(pk__in=request.data["pks"]).only("pk", "version")
 | 
				
			||||||
    version = request.data["version"]
 | 
					    pks: list[int] = [
 | 
				
			||||||
    send_agent_update_task.delay(pks=pks, version=version)
 | 
					        i.pk
 | 
				
			||||||
 | 
					        for i in q
 | 
				
			||||||
 | 
					        if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					    send_agent_update_task.delay(pks=pks)
 | 
				
			||||||
    return Response("ok")
 | 
					    return Response("ok")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -64,14 +65,9 @@ def update_agents(request):
 | 
				
			|||||||
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:
 | 
					    r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=5))
 | 
				
			||||||
        r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=5))
 | 
					    if r == "pong":
 | 
				
			||||||
        if r == "pong":
 | 
					        status = "online"
 | 
				
			||||||
            status = "online"
 | 
					 | 
				
			||||||
    else:
 | 
					 | 
				
			||||||
        r = agent.salt_api_cmd(timeout=5, func="test.ping")
 | 
					 | 
				
			||||||
        if isinstance(r, bool) and r:
 | 
					 | 
				
			||||||
            status = "online"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Response({"name": agent.hostname, "status": status})
 | 
					    return Response({"name": agent.hostname, "status": status})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -79,39 +75,52 @@ def ping(request, pk):
 | 
				
			|||||||
@api_view(["DELETE"])
 | 
					@api_view(["DELETE"])
 | 
				
			||||||
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"])
 | 
				
			||||||
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")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -155,21 +164,15 @@ 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()
 | 
				
			||||||
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)
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
@@ -185,17 +188,16 @@ def kill_proc(request, pk, pid):
 | 
				
			|||||||
@api_view()
 | 
					@api_view()
 | 
				
			||||||
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")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -205,8 +207,6 @@ def get_event_log(request, pk, logtype, days):
 | 
				
			|||||||
@api_view(["POST"])
 | 
					@api_view(["POST"])
 | 
				
			||||||
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 +231,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 +271,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,86 +288,14 @@ 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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -364,9 +303,6 @@ class Reboot(APIView):
 | 
				
			|||||||
    # 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 +312,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 +326,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 +337,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)
 | 
				
			||||||
@@ -423,6 +355,8 @@ class Reboot(APIView):
 | 
				
			|||||||
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 +377,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 +405,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 +435,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 +491,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."
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
@@ -713,10 +526,9 @@ def recover(request):
 | 
				
			|||||||
@api_view(["POST"])
 | 
					@api_view(["POST"])
 | 
				
			||||||
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,30 +537,32 @@ 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=45))
 | 
				
			||||||
    if r != "ok":
 | 
					    if r != "ok":
 | 
				
			||||||
@@ -821,12 +635,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 +662,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,9 +699,6 @@ 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")
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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):
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    def resolve(self):
 | 
				
			||||||
    def create_availability_alert(cls, agent):
 | 
					        self.resolved = True
 | 
				
			||||||
        pass
 | 
					        self.resolved_on = djangotime.now()
 | 
				
			||||||
 | 
					        self.snoozed = False
 | 
				
			||||||
 | 
					        self.snooze_until = None
 | 
				
			||||||
 | 
					        self.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    def create_check_alert(cls, check):
 | 
					    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
 | 
				
			||||||
 | 
					    def create_or_return_check_alert(cls, check):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if not cls.objects.filter(assigned_check=check, resolved=False).exists():
 | 
				
			||||||
 | 
					            return cls.objects.create(
 | 
				
			||||||
 | 
					                assigned_check=check,
 | 
				
			||||||
 | 
					                alert_type="check",
 | 
				
			||||||
 | 
					                severity=check.alert_severity,
 | 
				
			||||||
 | 
					                message=f"{check.agent.hostname} has a {check.check_type} check: {check.readable_desc} that failed.",
 | 
				
			||||||
 | 
					                hidden=True,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        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 = 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
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,104 @@
 | 
				
			|||||||
 | 
					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.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 .serializers import (
 | 
				
			||||||
 | 
					    AlertSerializer,
 | 
				
			||||||
 | 
					    AlertTemplateRelationSerializer,
 | 
				
			||||||
 | 
					    AlertTemplateSerializer,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					from .tasks import cache_agents_alert_template
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class GetAddAlerts(APIView):
 | 
					class GetAddAlerts(APIView):
 | 
				
			||||||
    def get(self, request):
 | 
					    def patch(self, request):
 | 
				
			||||||
        alerts = Alert.objects.all()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return Response(AlertSerializer(alerts, many=True).data)
 | 
					        # 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)
 | 
				
			||||||
@@ -32,7 +117,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 +160,77 @@ class GetUpdateDeleteAlert(APIView):
 | 
				
			|||||||
        Alert.objects.get(pk=pk).delete()
 | 
					        Alert.objects.get(pk=pk).delete()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return Response("ok")
 | 
					        return Response("ok")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class BulkAlerts(APIView):
 | 
				
			||||||
 | 
					    def post(self, request):
 | 
				
			||||||
 | 
					        if request.data["bulk_action"] == "resolve":
 | 
				
			||||||
 | 
					            Alert.objects.filter(id__in=request.data["alerts"]).update(
 | 
				
			||||||
 | 
					                resolved=True,
 | 
				
			||||||
 | 
					                resolved_on=djangotime.now(),
 | 
				
			||||||
 | 
					                snoozed=False,
 | 
				
			||||||
 | 
					                snooze_until=None,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            return Response("ok")
 | 
				
			||||||
 | 
					        elif request.data["bulk_action"] == "snooze":
 | 
				
			||||||
 | 
					            if "snooze_days" in request.data.keys():
 | 
				
			||||||
 | 
					                Alert.objects.filter(id__in=request.data["alerts"]).update(
 | 
				
			||||||
 | 
					                    snoozed=True,
 | 
				
			||||||
 | 
					                    snooze_until=djangotime.now()
 | 
				
			||||||
 | 
					                    + djangotime.timedelta(days=int(request.data["snooze_days"])),
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                return Response("ok")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return notify_error("The request was invalid")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class GetAddAlertTemplates(APIView):
 | 
				
			||||||
 | 
					    def get(self, request):
 | 
				
			||||||
 | 
					        alert_templates = AlertTemplate.objects.all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Response(AlertTemplateSerializer(alert_templates, many=True).data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def post(self, request):
 | 
				
			||||||
 | 
					        serializer = AlertTemplateSerializer(data=request.data, partial=True)
 | 
				
			||||||
 | 
					        serializer.is_valid(raise_exception=True)
 | 
				
			||||||
 | 
					        serializer.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # cache alert_template value on agents
 | 
				
			||||||
 | 
					        cache_agents_alert_template.delay()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Response("ok")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class GetUpdateDeleteAlertTemplate(APIView):
 | 
				
			||||||
 | 
					    def get(self, request, pk):
 | 
				
			||||||
 | 
					        alert_template = get_object_or_404(AlertTemplate, pk=pk)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Response(AlertTemplateSerializer(alert_template).data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def put(self, request, pk):
 | 
				
			||||||
 | 
					        alert_template = get_object_or_404(AlertTemplate, pk=pk)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        serializer = AlertTemplateSerializer(
 | 
				
			||||||
 | 
					            instance=alert_template, data=request.data, partial=True
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        serializer.is_valid(raise_exception=True)
 | 
				
			||||||
 | 
					        serializer.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # 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,260 @@ 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")
 | 
				
			||||||
 | 
					        task = baker.make("autotasks.AutomatedTask", agent=agent)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        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()),
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,70 +1,94 @@
 | 
				
			|||||||
import asyncio
 | 
					import asyncio
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
import requests
 | 
					import time
 | 
				
			||||||
from loguru import logger
 | 
					 | 
				
			||||||
from packaging import version as pyver
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.conf import settings
 | 
					from django.conf import settings
 | 
				
			||||||
 | 
					from django.http import HttpResponse
 | 
				
			||||||
from django.shortcuts import get_object_or_404
 | 
					from django.shortcuts import get_object_or_404
 | 
				
			||||||
from django.utils import timezone as djangotime
 | 
					from django.utils import timezone as djangotime
 | 
				
			||||||
from django.http import HttpResponse
 | 
					from loguru import logger
 | 
				
			||||||
 | 
					from packaging import version as pyver
 | 
				
			||||||
 | 
					from rest_framework.authentication import TokenAuthentication
 | 
				
			||||||
 | 
					from rest_framework.authtoken.models import Token
 | 
				
			||||||
 | 
					from rest_framework.permissions import IsAuthenticated
 | 
				
			||||||
from rest_framework.response import Response
 | 
					from rest_framework.response import Response
 | 
				
			||||||
from rest_framework.views import APIView
 | 
					from rest_framework.views import APIView
 | 
				
			||||||
from rest_framework.authentication import TokenAuthentication
 | 
					 | 
				
			||||||
from rest_framework.permissions import IsAuthenticated
 | 
					 | 
				
			||||||
from rest_framework.authtoken.models import Token
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
from agents.models import Agent
 | 
					 | 
				
			||||||
from checks.models import Check
 | 
					 | 
				
			||||||
from autotasks.models import AutomatedTask
 | 
					 | 
				
			||||||
from accounts.models import User
 | 
					from accounts.models import User
 | 
				
			||||||
from winupdate.models import WinUpdatePolicy
 | 
					from agents.models import Agent, AgentCustomField
 | 
				
			||||||
from software.models import InstalledSoftware
 | 
					 | 
				
			||||||
from checks.serializers import CheckRunnerGetSerializerV3
 | 
					 | 
				
			||||||
from agents.serializers import WinAgentSerializer
 | 
					from agents.serializers import WinAgentSerializer
 | 
				
			||||||
 | 
					from autotasks.models import AutomatedTask
 | 
				
			||||||
from autotasks.serializers import TaskGOGetSerializer, TaskRunnerPatchSerializer
 | 
					from autotasks.serializers import TaskGOGetSerializer, TaskRunnerPatchSerializer
 | 
				
			||||||
from winupdate.serializers import ApprovedUpdateSerializer
 | 
					from checks.models import Check
 | 
				
			||||||
 | 
					from checks.serializers import CheckRunnerGetSerializer
 | 
				
			||||||
from agents.tasks import (
 | 
					 | 
				
			||||||
    agent_recovery_email_task,
 | 
					 | 
				
			||||||
    agent_recovery_sms_task,
 | 
					 | 
				
			||||||
    sync_salt_modules_task,
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
from winupdate.tasks import check_for_updates_task
 | 
					 | 
				
			||||||
from software.tasks import install_chocolatey
 | 
					 | 
				
			||||||
from checks.utils import bytes2human
 | 
					from checks.utils import bytes2human
 | 
				
			||||||
from tacticalrmm.utils import notify_error, reload_nats, filter_software, SoftwareList
 | 
					from logs.models import PendingAction
 | 
				
			||||||
 | 
					from software.models import InstalledSoftware
 | 
				
			||||||
 | 
					from tacticalrmm.utils import SoftwareList, filter_software, notify_error, reload_nats
 | 
				
			||||||
 | 
					from winupdate.models import WinUpdate, WinUpdatePolicy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
logger.configure(**settings.LOG_CONFIG)
 | 
					logger.configure(**settings.LOG_CONFIG)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Hello(APIView):
 | 
					class CheckIn(APIView):
 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    The agent's checkin endpoint
 | 
					 | 
				
			||||||
    patch: called every 30 to 120 seconds
 | 
					 | 
				
			||||||
    post: called on agent windows service startup
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    authentication_classes = [TokenAuthentication]
 | 
					    authentication_classes = [TokenAuthentication]
 | 
				
			||||||
    permission_classes = [IsAuthenticated]
 | 
					    permission_classes = [IsAuthenticated]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def patch(self, request):
 | 
					    def patch(self, request):
 | 
				
			||||||
 | 
					        from alerts.models import Alert
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        updated = False
 | 
				
			||||||
 | 
					        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
				
			||||||
 | 
					        if pyver.parse(request.data["version"]) > pyver.parse(
 | 
				
			||||||
 | 
					            agent.version
 | 
				
			||||||
 | 
					        ) or pyver.parse(request.data["version"]) == pyver.parse(
 | 
				
			||||||
 | 
					            settings.LATEST_AGENT_VER
 | 
				
			||||||
 | 
					        ):
 | 
				
			||||||
 | 
					            updated = True
 | 
				
			||||||
 | 
					        agent.version = request.data["version"]
 | 
				
			||||||
 | 
					        agent.last_seen = djangotime.now()
 | 
				
			||||||
 | 
					        agent.save(update_fields=["version", "last_seen"])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # change agent update pending status to completed if agent has just updated
 | 
				
			||||||
 | 
					        if (
 | 
				
			||||||
 | 
					            updated
 | 
				
			||||||
 | 
					            and agent.pendingactions.filter(  # type: ignore
 | 
				
			||||||
 | 
					                action_type="agentupdate", status="pending"
 | 
				
			||||||
 | 
					            ).exists()
 | 
				
			||||||
 | 
					        ):
 | 
				
			||||||
 | 
					            agent.pendingactions.filter(  # type: ignore
 | 
				
			||||||
 | 
					                action_type="agentupdate", status="pending"
 | 
				
			||||||
 | 
					            ).update(status="completed")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # handles any alerting actions
 | 
				
			||||||
 | 
					        if Alert.objects.filter(agent=agent, resolved=False).exists():
 | 
				
			||||||
 | 
					            Alert.handle_alert_resolve(agent)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # sync scheduled tasks
 | 
				
			||||||
 | 
					        if agent.autotasks.exclude(sync_status="synced").exists():  # type: ignore
 | 
				
			||||||
 | 
					            tasks = agent.autotasks.exclude(sync_status="synced")  # type: ignore
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            for task in tasks:
 | 
				
			||||||
 | 
					                if task.sync_status == "pendingdeletion":
 | 
				
			||||||
 | 
					                    task.delete_task_on_agent()
 | 
				
			||||||
 | 
					                elif task.sync_status == "initial":
 | 
				
			||||||
 | 
					                    task.modify_task_on_agent()
 | 
				
			||||||
 | 
					                elif task.sync_status == "notsynced":
 | 
				
			||||||
 | 
					                    task.create_task_on_agent()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Response("ok")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def put(self, request):
 | 
				
			||||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
					        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
				
			||||||
        serializer = WinAgentSerializer(instance=agent, data=request.data, partial=True)
 | 
					        serializer = WinAgentSerializer(instance=agent, data=request.data, partial=True)
 | 
				
			||||||
        serializer.is_valid(raise_exception=True)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        disks = request.data["disks"]
 | 
					        if request.data["func"] == "disks":
 | 
				
			||||||
        new = []
 | 
					            disks = request.data["disks"]
 | 
				
			||||||
        # python agent
 | 
					            new = []
 | 
				
			||||||
        if isinstance(disks, dict):
 | 
					 | 
				
			||||||
            for k, v in disks.items():
 | 
					 | 
				
			||||||
                new.append(v)
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            # golang agent
 | 
					 | 
				
			||||||
            for disk in disks:
 | 
					            for disk in disks:
 | 
				
			||||||
                tmp = {}
 | 
					                tmp = {}
 | 
				
			||||||
                for k, v in disk.items():
 | 
					                for _, _ in disk.items():
 | 
				
			||||||
                    tmp["device"] = disk["device"]
 | 
					                    tmp["device"] = disk["device"]
 | 
				
			||||||
                    tmp["fstype"] = disk["fstype"]
 | 
					                    tmp["fstype"] = disk["fstype"]
 | 
				
			||||||
                    tmp["total"] = bytes2human(disk["total"])
 | 
					                    tmp["total"] = bytes2human(disk["total"])
 | 
				
			||||||
@@ -73,111 +97,249 @@ class Hello(APIView):
 | 
				
			|||||||
                    tmp["percent"] = int(disk["percent"])
 | 
					                    tmp["percent"] = int(disk["percent"])
 | 
				
			||||||
                new.append(tmp)
 | 
					                new.append(tmp)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if request.data["logged_in_username"] == "None":
 | 
					            serializer.is_valid(raise_exception=True)
 | 
				
			||||||
            serializer.save(last_seen=djangotime.now(), disks=new)
 | 
					            serializer.save(disks=new)
 | 
				
			||||||
        else:
 | 
					            return Response("ok")
 | 
				
			||||||
            serializer.save(
 | 
					
 | 
				
			||||||
                last_seen=djangotime.now(),
 | 
					        if request.data["func"] == "loggedonuser":
 | 
				
			||||||
                disks=new,
 | 
					            if request.data["logged_in_username"] != "None":
 | 
				
			||||||
                last_logged_in_user=request.data["logged_in_username"],
 | 
					                serializer.is_valid(raise_exception=True)
 | 
				
			||||||
 | 
					                serializer.save(last_logged_in_user=request.data["logged_in_username"])
 | 
				
			||||||
 | 
					                return Response("ok")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if request.data["func"] == "software":
 | 
				
			||||||
 | 
					            raw: SoftwareList = request.data["software"]
 | 
				
			||||||
 | 
					            if not isinstance(raw, list):
 | 
				
			||||||
 | 
					                return notify_error("err")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            sw = filter_software(raw)
 | 
				
			||||||
 | 
					            if not InstalledSoftware.objects.filter(agent=agent).exists():
 | 
				
			||||||
 | 
					                InstalledSoftware(agent=agent, software=sw).save()
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                s = agent.installedsoftware_set.first()  # type: ignore
 | 
				
			||||||
 | 
					                s.software = sw
 | 
				
			||||||
 | 
					                s.save(update_fields=["software"])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return Response("ok")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        serializer.is_valid(raise_exception=True)
 | 
				
			||||||
 | 
					        serializer.save()
 | 
				
			||||||
 | 
					        return Response("ok")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # called once during tacticalagent windows service startup
 | 
				
			||||||
 | 
					    def post(self, request):
 | 
				
			||||||
 | 
					        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
				
			||||||
 | 
					        if not agent.choco_installed:
 | 
				
			||||||
 | 
					            asyncio.run(agent.nats_cmd({"func": "installchoco"}, wait=False))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        time.sleep(0.5)
 | 
				
			||||||
 | 
					        asyncio.run(agent.nats_cmd({"func": "getwinupdates"}, wait=False))
 | 
				
			||||||
 | 
					        return Response("ok")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SyncMeshNodeID(APIView):
 | 
				
			||||||
 | 
					    authentication_classes = [TokenAuthentication]
 | 
				
			||||||
 | 
					    permission_classes = [IsAuthenticated]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def post(self, request):
 | 
				
			||||||
 | 
					        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
				
			||||||
 | 
					        if agent.mesh_node_id != request.data["nodeid"]:
 | 
				
			||||||
 | 
					            agent.mesh_node_id = request.data["nodeid"]
 | 
				
			||||||
 | 
					            agent.save(update_fields=["mesh_node_id"])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Response("ok")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Choco(APIView):
 | 
				
			||||||
 | 
					    authentication_classes = [TokenAuthentication]
 | 
				
			||||||
 | 
					    permission_classes = [IsAuthenticated]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def post(self, request):
 | 
				
			||||||
 | 
					        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
				
			||||||
 | 
					        agent.choco_installed = request.data["installed"]
 | 
				
			||||||
 | 
					        agent.save(update_fields=["choco_installed"])
 | 
				
			||||||
 | 
					        return Response("ok")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class WinUpdates(APIView):
 | 
				
			||||||
 | 
					    authentication_classes = [TokenAuthentication]
 | 
				
			||||||
 | 
					    permission_classes = [IsAuthenticated]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def put(self, request):
 | 
				
			||||||
 | 
					        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
				
			||||||
 | 
					        reboot_policy: str = agent.get_patch_policy().reboot_after_install
 | 
				
			||||||
 | 
					        reboot = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if reboot_policy == "always":
 | 
				
			||||||
 | 
					            reboot = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if request.data["needs_reboot"]:
 | 
				
			||||||
 | 
					            if reboot_policy == "required":
 | 
				
			||||||
 | 
					                reboot = True
 | 
				
			||||||
 | 
					            elif reboot_policy == "never":
 | 
				
			||||||
 | 
					                agent.needs_reboot = True
 | 
				
			||||||
 | 
					                agent.save(update_fields=["needs_reboot"])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if reboot:
 | 
				
			||||||
 | 
					            asyncio.run(agent.nats_cmd({"func": "rebootnow"}, wait=False))
 | 
				
			||||||
 | 
					            logger.info(f"{agent.hostname} is rebooting after updates were installed.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        agent.delete_superseded_updates()
 | 
				
			||||||
 | 
					        return Response("ok")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def patch(self, request):
 | 
				
			||||||
 | 
					        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
				
			||||||
 | 
					        u = agent.winupdates.filter(guid=request.data["guid"]).last()  # type: ignore
 | 
				
			||||||
 | 
					        success: bool = request.data["success"]
 | 
				
			||||||
 | 
					        if success:
 | 
				
			||||||
 | 
					            u.result = "success"
 | 
				
			||||||
 | 
					            u.downloaded = True
 | 
				
			||||||
 | 
					            u.installed = True
 | 
				
			||||||
 | 
					            u.date_installed = djangotime.now()
 | 
				
			||||||
 | 
					            u.save(
 | 
				
			||||||
 | 
					                update_fields=[
 | 
				
			||||||
 | 
					                    "result",
 | 
				
			||||||
 | 
					                    "downloaded",
 | 
				
			||||||
 | 
					                    "installed",
 | 
				
			||||||
 | 
					                    "date_installed",
 | 
				
			||||||
 | 
					                ]
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            u.result = "failed"
 | 
				
			||||||
 | 
					            u.save(update_fields=["result"])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if agent.agentoutages.exists() and agent.agentoutages.last().is_active:
 | 
					        agent.delete_superseded_updates()
 | 
				
			||||||
            last_outage = agent.agentoutages.last()
 | 
					 | 
				
			||||||
            last_outage.recovery_time = djangotime.now()
 | 
					 | 
				
			||||||
            last_outage.save(update_fields=["recovery_time"])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if agent.overdue_email_alert:
 | 
					 | 
				
			||||||
                agent_recovery_email_task.delay(pk=last_outage.pk)
 | 
					 | 
				
			||||||
            if agent.overdue_text_alert:
 | 
					 | 
				
			||||||
                agent_recovery_sms_task.delay(pk=last_outage.pk)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        recovery = agent.recoveryactions.filter(last_run=None).last()
 | 
					 | 
				
			||||||
        if recovery is not None:
 | 
					 | 
				
			||||||
            recovery.last_run = djangotime.now()
 | 
					 | 
				
			||||||
            recovery.save(update_fields=["last_run"])
 | 
					 | 
				
			||||||
            return Response(recovery.send())
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # handle agent update
 | 
					 | 
				
			||||||
        if agent.pendingactions.filter(
 | 
					 | 
				
			||||||
            action_type="agentupdate", status="pending"
 | 
					 | 
				
			||||||
        ).exists():
 | 
					 | 
				
			||||||
            update = agent.pendingactions.filter(
 | 
					 | 
				
			||||||
                action_type="agentupdate", status="pending"
 | 
					 | 
				
			||||||
            ).last()
 | 
					 | 
				
			||||||
            update.status = "completed"
 | 
					 | 
				
			||||||
            update.save(update_fields=["status"])
 | 
					 | 
				
			||||||
            return Response(update.details)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # get any pending actions
 | 
					 | 
				
			||||||
        if agent.pendingactions.filter(status="pending").exists():
 | 
					 | 
				
			||||||
            agent.handle_pending_actions()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return Response("ok")
 | 
					        return Response("ok")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def post(self, request):
 | 
					    def post(self, request):
 | 
				
			||||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
					        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
				
			||||||
 | 
					        updates = request.data["wua_updates"]
 | 
				
			||||||
 | 
					        for update in updates:
 | 
				
			||||||
 | 
					            if agent.winupdates.filter(guid=update["guid"]).exists():  # type: ignore
 | 
				
			||||||
 | 
					                u = agent.winupdates.filter(guid=update["guid"]).last()  # type: ignore
 | 
				
			||||||
 | 
					                u.downloaded = update["downloaded"]
 | 
				
			||||||
 | 
					                u.installed = update["installed"]
 | 
				
			||||||
 | 
					                u.save(update_fields=["downloaded", "installed"])
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                try:
 | 
				
			||||||
 | 
					                    kb = "KB" + update["kb_article_ids"][0]
 | 
				
			||||||
 | 
					                except:
 | 
				
			||||||
 | 
					                    continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        serializer = WinAgentSerializer(instance=agent, data=request.data, partial=True)
 | 
					                WinUpdate(
 | 
				
			||||||
        serializer.is_valid(raise_exception=True)
 | 
					                    agent=agent,
 | 
				
			||||||
        serializer.save(last_seen=djangotime.now())
 | 
					                    guid=update["guid"],
 | 
				
			||||||
 | 
					                    kb=kb,
 | 
				
			||||||
 | 
					                    title=update["title"],
 | 
				
			||||||
 | 
					                    installed=update["installed"],
 | 
				
			||||||
 | 
					                    downloaded=update["downloaded"],
 | 
				
			||||||
 | 
					                    description=update["description"],
 | 
				
			||||||
 | 
					                    severity=update["severity"],
 | 
				
			||||||
 | 
					                    categories=update["categories"],
 | 
				
			||||||
 | 
					                    category_ids=update["category_ids"],
 | 
				
			||||||
 | 
					                    kb_article_ids=update["kb_article_ids"],
 | 
				
			||||||
 | 
					                    more_info_urls=update["more_info_urls"],
 | 
				
			||||||
 | 
					                    support_url=update["support_url"],
 | 
				
			||||||
 | 
					                    revision_number=update["revision_number"],
 | 
				
			||||||
 | 
					                ).save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        sync_salt_modules_task.delay(agent.pk)
 | 
					        agent.delete_superseded_updates()
 | 
				
			||||||
        check_for_updates_task.apply_async(
 | 
					 | 
				
			||||||
            queue="wupdate", kwargs={"pk": agent.pk, "wait": True}
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if not agent.choco_installed:
 | 
					        # more superseded updates cleanup
 | 
				
			||||||
            install_chocolatey.delay(agent.pk, wait=True)
 | 
					        if pyver.parse(agent.version) <= pyver.parse("1.4.2"):
 | 
				
			||||||
 | 
					            for u in agent.winupdates.filter(  # type: ignore
 | 
				
			||||||
 | 
					                date_installed__isnull=True, result="failed"
 | 
				
			||||||
 | 
					            ).exclude(installed=True):
 | 
				
			||||||
 | 
					                u.delete()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return Response("ok")
 | 
					        return Response("ok")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CheckRunner(APIView):
 | 
					class SupersededWinUpdate(APIView):
 | 
				
			||||||
    """
 | 
					    authentication_classes = [TokenAuthentication]
 | 
				
			||||||
    For the windows golang agent
 | 
					    permission_classes = [IsAuthenticated]
 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def post(self, request):
 | 
				
			||||||
 | 
					        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
				
			||||||
 | 
					        updates = agent.winupdates.filter(guid=request.data["guid"])  # type: ignore
 | 
				
			||||||
 | 
					        for u in updates:
 | 
				
			||||||
 | 
					            u.delete()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Response("ok")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class RunChecks(APIView):
 | 
				
			||||||
    authentication_classes = [TokenAuthentication]
 | 
					    authentication_classes = [TokenAuthentication]
 | 
				
			||||||
    permission_classes = [IsAuthenticated]
 | 
					    permission_classes = [IsAuthenticated]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get(self, request, agentid):
 | 
					    def get(self, request, agentid):
 | 
				
			||||||
        agent = get_object_or_404(Agent, agent_id=agentid)
 | 
					        agent = get_object_or_404(Agent, agent_id=agentid)
 | 
				
			||||||
        checks = Check.objects.filter(agent__pk=agent.pk, overriden_by_policy=False)
 | 
					        checks = Check.objects.filter(agent__pk=agent.pk, overriden_by_policy=False)
 | 
				
			||||||
 | 
					 | 
				
			||||||
        ret = {
 | 
					        ret = {
 | 
				
			||||||
            "agent": agent.pk,
 | 
					            "agent": agent.pk,
 | 
				
			||||||
            "check_interval": agent.check_interval,
 | 
					            "check_interval": agent.check_interval,
 | 
				
			||||||
            "checks": CheckRunnerGetSerializerV3(checks, many=True).data,
 | 
					            "checks": CheckRunnerGetSerializer(checks, many=True).data,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return Response(ret)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CheckRunner(APIView):
 | 
				
			||||||
 | 
					    authentication_classes = [TokenAuthentication]
 | 
				
			||||||
 | 
					    permission_classes = [IsAuthenticated]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get(self, request, agentid):
 | 
				
			||||||
 | 
					        agent = get_object_or_404(Agent, agent_id=agentid)
 | 
				
			||||||
 | 
					        checks = agent.agentchecks.filter(overriden_by_policy=False)  # type: ignore
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        run_list = [
 | 
				
			||||||
 | 
					            check
 | 
				
			||||||
 | 
					            for check in checks
 | 
				
			||||||
 | 
					            # always run if check hasn't run yet
 | 
				
			||||||
 | 
					            if not check.last_run
 | 
				
			||||||
 | 
					            # if a check interval is set, see if the correct amount of seconds have passed
 | 
				
			||||||
 | 
					            or (
 | 
				
			||||||
 | 
					                check.run_interval
 | 
				
			||||||
 | 
					                and (
 | 
				
			||||||
 | 
					                    check.last_run
 | 
				
			||||||
 | 
					                    < djangotime.now()
 | 
				
			||||||
 | 
					                    - djangotime.timedelta(seconds=check.run_interval)
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                # if check interval isn't set, make sure the agent's check interval has passed before running
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            or (
 | 
				
			||||||
 | 
					                check.last_run
 | 
				
			||||||
 | 
					                < djangotime.now() - djangotime.timedelta(seconds=agent.check_interval)
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					        ret = {
 | 
				
			||||||
 | 
					            "agent": agent.pk,
 | 
				
			||||||
 | 
					            "check_interval": agent.check_run_interval(),
 | 
				
			||||||
 | 
					            "checks": CheckRunnerGetSerializer(run_list, many=True).data,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        return Response(ret)
 | 
					        return Response(ret)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def patch(self, request):
 | 
					    def patch(self, request):
 | 
				
			||||||
        from logs.models import AuditLog
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        check = get_object_or_404(Check, pk=request.data["id"])
 | 
					        check = get_object_or_404(Check, pk=request.data["id"])
 | 
				
			||||||
        check.last_run = djangotime.now()
 | 
					        check.last_run = djangotime.now()
 | 
				
			||||||
        check.save(update_fields=["last_run"])
 | 
					        check.save(update_fields=["last_run"])
 | 
				
			||||||
        status = check.handle_checkv2(request.data)
 | 
					        status = check.handle_checkv2(request.data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # create audit entry
 | 
					 | 
				
			||||||
        AuditLog.objects.create(
 | 
					 | 
				
			||||||
            username=check.agent.hostname,
 | 
					 | 
				
			||||||
            agent=check.agent.hostname,
 | 
					 | 
				
			||||||
            object_type="agent",
 | 
					 | 
				
			||||||
            action="check_run",
 | 
					 | 
				
			||||||
            message=f"{check.readable_desc} was run on {check.agent.hostname}. Status: {status}",
 | 
					 | 
				
			||||||
            after_value=Check.serialize(check),
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return Response(status)
 | 
					        return Response(status)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TaskRunner(APIView):
 | 
					class CheckRunnerInterval(APIView):
 | 
				
			||||||
    """
 | 
					    authentication_classes = [TokenAuthentication]
 | 
				
			||||||
    For the windows golang agent
 | 
					    permission_classes = [IsAuthenticated]
 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get(self, request, agentid):
 | 
				
			||||||
 | 
					        agent = get_object_or_404(Agent, agent_id=agentid)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Response(
 | 
				
			||||||
 | 
					            {"agent": agent.pk, "check_interval": agent.check_run_interval()}
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TaskRunner(APIView):
 | 
				
			||||||
    authentication_classes = [TokenAuthentication]
 | 
					    authentication_classes = [TokenAuthentication]
 | 
				
			||||||
    permission_classes = [IsAuthenticated]
 | 
					    permission_classes = [IsAuthenticated]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -187,6 +349,7 @@ class TaskRunner(APIView):
 | 
				
			|||||||
        return Response(TaskGOGetSerializer(task).data)
 | 
					        return Response(TaskGOGetSerializer(task).data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def patch(self, request, pk, agentid):
 | 
					    def patch(self, request, pk, agentid):
 | 
				
			||||||
 | 
					        from alerts.models import Alert
 | 
				
			||||||
        from logs.models import AuditLog
 | 
					        from logs.models import AuditLog
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        agent = get_object_or_404(Agent, agent_id=agentid)
 | 
					        agent = get_object_or_404(Agent, agent_id=agentid)
 | 
				
			||||||
@@ -196,9 +359,51 @@ class TaskRunner(APIView):
 | 
				
			|||||||
            instance=task, data=request.data, partial=True
 | 
					            instance=task, data=request.data, partial=True
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        serializer.is_valid(raise_exception=True)
 | 
					        serializer.is_valid(raise_exception=True)
 | 
				
			||||||
        serializer.save(last_run=djangotime.now())
 | 
					        new_task = serializer.save(last_run=djangotime.now())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # check if task is a collector and update the custom field
 | 
				
			||||||
 | 
					        if task.custom_field:
 | 
				
			||||||
 | 
					            if not task.stderr:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if AgentCustomField.objects.filter(
 | 
				
			||||||
 | 
					                    field=task.custom_field, agent=task.agent
 | 
				
			||||||
 | 
					                ).exists():
 | 
				
			||||||
 | 
					                    agent_field = AgentCustomField.objects.get(
 | 
				
			||||||
 | 
					                        field=task.custom_field, agent=task.agent
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                else:
 | 
				
			||||||
 | 
					                    agent_field = AgentCustomField.objects.create(
 | 
				
			||||||
 | 
					                        field=task.custom_field, agent=task.agent
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                # get last line of stdout
 | 
				
			||||||
 | 
					                value = new_task.stdout.split("\n")[-1].strip()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if task.custom_field.type in ["text", "number", "single", "datetime"]:
 | 
				
			||||||
 | 
					                    agent_field.string_value = value
 | 
				
			||||||
 | 
					                    agent_field.save()
 | 
				
			||||||
 | 
					                elif task.custom_field.type == "multiple":
 | 
				
			||||||
 | 
					                    agent_field.multiple_value = value.split(",")
 | 
				
			||||||
 | 
					                    agent_field.save()
 | 
				
			||||||
 | 
					                elif task.custom_field.type == "checkbox":
 | 
				
			||||||
 | 
					                    agent_field.bool_value = bool(value)
 | 
				
			||||||
 | 
					                    agent_field.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                status = "passing"
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                status = "failing"
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            status = "failing" if task.retcode != 0 else "passing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        new_task.status = status
 | 
				
			||||||
 | 
					        new_task.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if status == "passing":
 | 
				
			||||||
 | 
					            if Alert.objects.filter(assigned_task=new_task, resolved=False).exists():
 | 
				
			||||||
 | 
					                Alert.handle_alert_resolve(new_task)
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            Alert.handle_alert_failure(new_task)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        new_task = AutomatedTask.objects.get(pk=task.pk)
 | 
					 | 
				
			||||||
        AuditLog.objects.create(
 | 
					        AuditLog.objects.create(
 | 
				
			||||||
            username=agent.hostname,
 | 
					            username=agent.hostname,
 | 
				
			||||||
            agent=agent.hostname,
 | 
					            agent=agent.hostname,
 | 
				
			||||||
@@ -211,154 +416,6 @@ class TaskRunner(APIView):
 | 
				
			|||||||
        return Response("ok")
 | 
					        return Response("ok")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SaltMinion(APIView):
 | 
					 | 
				
			||||||
    authentication_classes = [TokenAuthentication]
 | 
					 | 
				
			||||||
    permission_classes = [IsAuthenticated]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get(self, request, agentid):
 | 
					 | 
				
			||||||
        agent = get_object_or_404(Agent, agent_id=agentid)
 | 
					 | 
				
			||||||
        ret = {
 | 
					 | 
				
			||||||
            "latestVer": settings.LATEST_SALT_VER,
 | 
					 | 
				
			||||||
            "currentVer": agent.salt_ver,
 | 
					 | 
				
			||||||
            "salt_id": agent.salt_id,
 | 
					 | 
				
			||||||
            "downloadURL": agent.winsalt_dl,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return Response(ret)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def post(self, request):
 | 
					 | 
				
			||||||
        # accept the salt key
 | 
					 | 
				
			||||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
					 | 
				
			||||||
        if agent.salt_id != request.data["saltid"]:
 | 
					 | 
				
			||||||
            return notify_error("Salt keys do not match")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            resp = requests.post(
 | 
					 | 
				
			||||||
                f"http://{settings.SALT_HOST}:8123/run",
 | 
					 | 
				
			||||||
                json=[
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        "client": "wheel",
 | 
					 | 
				
			||||||
                        "fun": "key.accept",
 | 
					 | 
				
			||||||
                        "match": request.data["saltid"],
 | 
					 | 
				
			||||||
                        "username": settings.SALT_USERNAME,
 | 
					 | 
				
			||||||
                        "password": settings.SALT_PASSWORD,
 | 
					 | 
				
			||||||
                        "eauth": "pam",
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                ],
 | 
					 | 
				
			||||||
                timeout=30,
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        except Exception:
 | 
					 | 
				
			||||||
            return notify_error("No communication between agent and salt-api")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            data = resp.json()["return"][0]["data"]
 | 
					 | 
				
			||||||
            minion = data["return"]["minions"][0]
 | 
					 | 
				
			||||||
        except Exception:
 | 
					 | 
				
			||||||
            return notify_error("Key error")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if data["success"] and minion == request.data["saltid"]:
 | 
					 | 
				
			||||||
            return Response("Salt key was accepted")
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            return notify_error("Not accepted")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def patch(self, request):
 | 
					 | 
				
			||||||
        # sync modules
 | 
					 | 
				
			||||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
					 | 
				
			||||||
        r = agent.salt_api_cmd(timeout=45, func="saltutil.sync_modules")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if r == "timeout" or r == "error":
 | 
					 | 
				
			||||||
            return notify_error("Failed to sync salt modules")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if isinstance(r, list) and any("modules" in i for i in r):
 | 
					 | 
				
			||||||
            return Response("Successfully synced salt modules")
 | 
					 | 
				
			||||||
        elif isinstance(r, list) and not r:
 | 
					 | 
				
			||||||
            return Response("Modules are already in sync")
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            return notify_error(f"Failed to sync salt modules: {str(r)}")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def put(self, request):
 | 
					 | 
				
			||||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
					 | 
				
			||||||
        agent.salt_ver = request.data["ver"]
 | 
					 | 
				
			||||||
        agent.save(update_fields=["salt_ver"])
 | 
					 | 
				
			||||||
        return Response("ok")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class WinUpdater(APIView):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    authentication_classes = [TokenAuthentication]
 | 
					 | 
				
			||||||
    permission_classes = [IsAuthenticated]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get(self, request, agentid):
 | 
					 | 
				
			||||||
        agent = get_object_or_404(Agent, agent_id=agentid)
 | 
					 | 
				
			||||||
        agent.delete_superseded_updates()
 | 
					 | 
				
			||||||
        patches = agent.winupdates.filter(action="approve").exclude(installed=True)
 | 
					 | 
				
			||||||
        return Response(ApprovedUpdateSerializer(patches, many=True).data)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # agent sends patch results as it's installing them
 | 
					 | 
				
			||||||
    def patch(self, request):
 | 
					 | 
				
			||||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
					 | 
				
			||||||
        kb = request.data["kb"]
 | 
					 | 
				
			||||||
        results = request.data["results"]
 | 
					 | 
				
			||||||
        update = agent.winupdates.get(kb=kb)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if results == "error" or results == "failed":
 | 
					 | 
				
			||||||
            update.result = results
 | 
					 | 
				
			||||||
            update.save(update_fields=["result"])
 | 
					 | 
				
			||||||
        elif results == "success":
 | 
					 | 
				
			||||||
            update.result = "success"
 | 
					 | 
				
			||||||
            update.downloaded = True
 | 
					 | 
				
			||||||
            update.installed = True
 | 
					 | 
				
			||||||
            update.date_installed = djangotime.now()
 | 
					 | 
				
			||||||
            update.save(
 | 
					 | 
				
			||||||
                update_fields=[
 | 
					 | 
				
			||||||
                    "result",
 | 
					 | 
				
			||||||
                    "downloaded",
 | 
					 | 
				
			||||||
                    "installed",
 | 
					 | 
				
			||||||
                    "date_installed",
 | 
					 | 
				
			||||||
                ]
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        elif results == "alreadyinstalled":
 | 
					 | 
				
			||||||
            update.result = "success"
 | 
					 | 
				
			||||||
            update.downloaded = True
 | 
					 | 
				
			||||||
            update.installed = True
 | 
					 | 
				
			||||||
            update.save(update_fields=["result", "downloaded", "installed"])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return Response("ok")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # agent calls this after it's finished installing all patches
 | 
					 | 
				
			||||||
    def post(self, request):
 | 
					 | 
				
			||||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
					 | 
				
			||||||
        reboot_policy = agent.get_patch_policy().reboot_after_install
 | 
					 | 
				
			||||||
        reboot = False
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if reboot_policy == "always":
 | 
					 | 
				
			||||||
            reboot = True
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if request.data["reboot"]:
 | 
					 | 
				
			||||||
            if reboot_policy == "required":
 | 
					 | 
				
			||||||
                reboot = True
 | 
					 | 
				
			||||||
            elif reboot_policy == "never":
 | 
					 | 
				
			||||||
                agent.needs_reboot = True
 | 
					 | 
				
			||||||
                agent.save(update_fields=["needs_reboot"])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if reboot:
 | 
					 | 
				
			||||||
            if agent.has_nats:
 | 
					 | 
				
			||||||
                asyncio.run(agent.nats_cmd({"func": "rebootnow"}, wait=False))
 | 
					 | 
				
			||||||
            else:
 | 
					 | 
				
			||||||
                agent.salt_api_async(
 | 
					 | 
				
			||||||
                    func="system.reboot",
 | 
					 | 
				
			||||||
                    arg=7,
 | 
					 | 
				
			||||||
                    kwargs={"in_seconds": True},
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            logger.info(f"{agent.hostname} is rebooting after updates were installed.")
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            check_for_updates_task.apply_async(
 | 
					 | 
				
			||||||
                queue="wupdate", kwargs={"pk": agent.pk, "wait": False}
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return Response("ok")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class SysInfo(APIView):
 | 
					class SysInfo(APIView):
 | 
				
			||||||
    authentication_classes = [TokenAuthentication]
 | 
					    authentication_classes = [TokenAuthentication]
 | 
				
			||||||
    permission_classes = [IsAuthenticated]
 | 
					    permission_classes = [IsAuthenticated]
 | 
				
			||||||
@@ -374,31 +431,8 @@ class SysInfo(APIView):
 | 
				
			|||||||
        return Response("ok")
 | 
					        return Response("ok")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class MeshInfo(APIView):
 | 
					 | 
				
			||||||
    authentication_classes = [TokenAuthentication]
 | 
					 | 
				
			||||||
    permission_classes = [IsAuthenticated]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get(self, request, pk):
 | 
					 | 
				
			||||||
        agent = get_object_or_404(Agent, pk=pk)
 | 
					 | 
				
			||||||
        return Response(agent.mesh_node_id)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def patch(self, request, pk):
 | 
					 | 
				
			||||||
        agent = get_object_or_404(Agent, pk=pk)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if "nodeidhex" in request.data:
 | 
					 | 
				
			||||||
            # agent <= 1.1.0
 | 
					 | 
				
			||||||
            nodeid = request.data["nodeidhex"]
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            # agent >= 1.1.1
 | 
					 | 
				
			||||||
            nodeid = request.data["nodeid"]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        agent.mesh_node_id = nodeid
 | 
					 | 
				
			||||||
        agent.save(update_fields=["mesh_node_id"])
 | 
					 | 
				
			||||||
        return Response("ok")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class MeshExe(APIView):
 | 
					class MeshExe(APIView):
 | 
				
			||||||
    """ Sends the mesh exe to the installer """
 | 
					    """Sends the mesh exe to the installer"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def post(self, request):
 | 
					    def post(self, request):
 | 
				
			||||||
        exe = "meshagent.exe" if request.data["arch"] == "64" else "meshagent-x86.exe"
 | 
					        exe = "meshagent.exe" if request.data["arch"] == "64" else "meshagent-x86.exe"
 | 
				
			||||||
@@ -446,10 +480,10 @@ class NewAgent(APIView):
 | 
				
			|||||||
        agent.salt_id = f"{agent.hostname}-{agent.pk}"
 | 
					        agent.salt_id = f"{agent.hostname}-{agent.pk}"
 | 
				
			||||||
        agent.save(update_fields=["salt_id"])
 | 
					        agent.save(update_fields=["salt_id"])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        user = User.objects.create_user(
 | 
					        user = User.objects.create_user(  # type: ignore
 | 
				
			||||||
            username=request.data["agent_id"],
 | 
					            username=request.data["agent_id"],
 | 
				
			||||||
            agent=agent,
 | 
					            agent=agent,
 | 
				
			||||||
            password=User.objects.make_random_password(60),
 | 
					            password=User.objects.make_random_password(60),  # type: ignore
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        token = Token.objects.create(user=user)
 | 
					        token = Token.objects.create(user=user)
 | 
				
			||||||
@@ -461,10 +495,6 @@ class NewAgent(APIView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        reload_nats()
 | 
					        reload_nats()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Generate policies for new agent
 | 
					 | 
				
			||||||
        agent.generate_checks_from_policies()
 | 
					 | 
				
			||||||
        agent.generate_tasks_from_policies()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # create agent install audit record
 | 
					        # create agent install audit record
 | 
				
			||||||
        AuditLog.objects.create(
 | 
					        AuditLog.objects.create(
 | 
				
			||||||
            username=request.user,
 | 
					            username=request.user,
 | 
				
			||||||
@@ -498,7 +528,7 @@ class Software(APIView):
 | 
				
			|||||||
        if not InstalledSoftware.objects.filter(agent=agent).exists():
 | 
					        if not InstalledSoftware.objects.filter(agent=agent).exists():
 | 
				
			||||||
            InstalledSoftware(agent=agent, software=sw).save()
 | 
					            InstalledSoftware(agent=agent, software=sw).save()
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            s = agent.installedsoftware_set.first()
 | 
					            s = agent.installedsoftware_set.first()  # type: ignore
 | 
				
			||||||
            s.software = sw
 | 
					            s.software = sw
 | 
				
			||||||
            s.save(update_fields=["software"])
 | 
					            s.save(update_fields=["software"])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -521,3 +551,59 @@ class Installer(APIView):
 | 
				
			|||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return Response("ok")
 | 
					        return Response("ok")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ChocoResult(APIView):
 | 
				
			||||||
 | 
					    authentication_classes = [TokenAuthentication]
 | 
				
			||||||
 | 
					    permission_classes = [IsAuthenticated]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def patch(self, request, pk):
 | 
				
			||||||
 | 
					        action = get_object_or_404(PendingAction, pk=pk)
 | 
				
			||||||
 | 
					        results: str = request.data["results"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        software_name = action.details["name"].lower()
 | 
				
			||||||
 | 
					        success = [
 | 
				
			||||||
 | 
					            "install",
 | 
				
			||||||
 | 
					            "of",
 | 
				
			||||||
 | 
					            software_name,
 | 
				
			||||||
 | 
					            "was",
 | 
				
			||||||
 | 
					            "successful",
 | 
				
			||||||
 | 
					            "installed",
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					        duplicate = [software_name, "already", "installed", "--force", "reinstall"]
 | 
				
			||||||
 | 
					        installed = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if all(x in results.lower() for x in success):
 | 
				
			||||||
 | 
					            installed = True
 | 
				
			||||||
 | 
					        elif all(x in results.lower() for x in duplicate):
 | 
				
			||||||
 | 
					            installed = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        action.details["output"] = results
 | 
				
			||||||
 | 
					        action.details["installed"] = installed
 | 
				
			||||||
 | 
					        action.status = "completed"
 | 
				
			||||||
 | 
					        action.save(update_fields=["details", "status"])
 | 
				
			||||||
 | 
					        return Response("ok")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AgentRecovery(APIView):
 | 
				
			||||||
 | 
					    authentication_classes = [TokenAuthentication]
 | 
				
			||||||
 | 
					    permission_classes = [IsAuthenticated]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get(self, request, agentid):
 | 
				
			||||||
 | 
					        agent = get_object_or_404(Agent, agent_id=agentid)
 | 
				
			||||||
 | 
					        recovery = agent.recoveryactions.filter(last_run=None).last()  # type: ignore
 | 
				
			||||||
 | 
					        ret = {"mode": "pass", "shellcmd": ""}
 | 
				
			||||||
 | 
					        if recovery is None:
 | 
				
			||||||
 | 
					            return Response(ret)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        recovery.last_run = djangotime.now()
 | 
				
			||||||
 | 
					        recovery.save(update_fields=["last_run"])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        ret["mode"] = recovery.mode
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if recovery.mode == "command":
 | 
				
			||||||
 | 
					            ret["shellcmd"] = recovery.command
 | 
				
			||||||
 | 
					        elif recovery.mode == "rpc":
 | 
				
			||||||
 | 
					            reload_nats()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Response(ret)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
# Generated by Django 3.0.6 on 2020-06-04 17:13
 | 
					# Generated by Django 3.0.6 on 2020-06-04 17:13
 | 
				
			||||||
 | 
					
 | 
				
			||||||
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,11 +6,11 @@ from django.db import migrations
 | 
				
			|||||||
class Migration(migrations.Migration):
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    dependencies = [
 | 
					    dependencies = [
 | 
				
			||||||
        ('automation', '0005_auto_20200922_1344'),
 | 
					        ("automation", "0005_auto_20200922_1344"),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    operations = [
 | 
					    operations = [
 | 
				
			||||||
        migrations.DeleteModel(
 | 
					        migrations.DeleteModel(
 | 
				
			||||||
            name='PolicyExclusions',
 | 
					            name="PolicyExclusions",
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.1.4 on 2021-02-12 14:08
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import django.db.models.deletion
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('alerts', '0004_auto_20210212_1408'),
 | 
				
			||||||
 | 
					        ('automation', '0006_delete_policyexclusions'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='policy',
 | 
				
			||||||
 | 
					            name='alert_template',
 | 
				
			||||||
 | 
					            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='policies', to='alerts.alerttemplate'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.1.7 on 2021-03-02 04:15
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('agents', '0030_agent_offline_time'),
 | 
				
			||||||
 | 
					        ('clients', '0009_auto_20210212_1408'),
 | 
				
			||||||
 | 
					        ('automation', '0007_policy_alert_template'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='policy',
 | 
				
			||||||
 | 
					            name='excluded_agents',
 | 
				
			||||||
 | 
					            field=models.ManyToManyField(blank=True, related_name='policy_exclusions', to='agents.Agent'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='policy',
 | 
				
			||||||
 | 
					            name='excluded_clients',
 | 
				
			||||||
 | 
					            field=models.ManyToManyField(blank=True, related_name='policy_exclusions', to='clients.Client'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='policy',
 | 
				
			||||||
 | 
					            name='excluded_sites',
 | 
				
			||||||
 | 
					            field=models.ManyToManyField(blank=True, related_name='policy_exclusions', to='clients.Site'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -1,7 +1,6 @@
 | 
				
			|||||||
from django.db import models
 | 
					 | 
				
			||||||
from agents.models import Agent
 | 
					from agents.models import Agent
 | 
				
			||||||
from clients.models import Site, Client
 | 
					 | 
				
			||||||
from core.models import CoreSettings
 | 
					from core.models import CoreSettings
 | 
				
			||||||
 | 
					from django.db import models
 | 
				
			||||||
from logs.models import BaseAuditModel
 | 
					from logs.models import BaseAuditModel
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -10,39 +9,114 @@ class Policy(BaseAuditModel):
 | 
				
			|||||||
    desc = models.CharField(max_length=255, null=True, blank=True)
 | 
					    desc = models.CharField(max_length=255, null=True, blank=True)
 | 
				
			||||||
    active = models.BooleanField(default=False)
 | 
					    active = models.BooleanField(default=False)
 | 
				
			||||||
    enforced = models.BooleanField(default=False)
 | 
					    enforced = models.BooleanField(default=False)
 | 
				
			||||||
 | 
					    alert_template = models.ForeignKey(
 | 
				
			||||||
 | 
					        "alerts.AlertTemplate",
 | 
				
			||||||
 | 
					        related_name="policies",
 | 
				
			||||||
 | 
					        on_delete=models.SET_NULL,
 | 
				
			||||||
 | 
					        null=True,
 | 
				
			||||||
 | 
					        blank=True,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    excluded_sites = models.ManyToManyField(
 | 
				
			||||||
 | 
					        "clients.Site", related_name="policy_exclusions", blank=True
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    excluded_clients = models.ManyToManyField(
 | 
				
			||||||
 | 
					        "clients.Client", related_name="policy_exclusions", blank=True
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    excluded_agents = models.ManyToManyField(
 | 
				
			||||||
 | 
					        "agents.Agent", related_name="policy_exclusions", blank=True
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    def save(self, *args, **kwargs):
 | 
				
			||||||
    def is_default_server_policy(self):
 | 
					        from alerts.tasks import cache_agents_alert_template
 | 
				
			||||||
        return self.default_server_policy.exists()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					        from automation.tasks import generate_agent_checks_task
 | 
				
			||||||
    def is_default_workstation_policy(self):
 | 
					
 | 
				
			||||||
        return self.default_workstation_policy.exists()
 | 
					        # get old policy if exists
 | 
				
			||||||
 | 
					        old_policy = type(self).objects.get(pk=self.pk) if self.pk else None
 | 
				
			||||||
 | 
					        super(BaseAuditModel, self).save(*args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # generate agent checks only if active and enforced were changed
 | 
				
			||||||
 | 
					        if old_policy:
 | 
				
			||||||
 | 
					            if old_policy.active != self.active or old_policy.enforced != self.enforced:
 | 
				
			||||||
 | 
					                generate_agent_checks_task.delay(
 | 
				
			||||||
 | 
					                    policy=self.pk,
 | 
				
			||||||
 | 
					                    create_tasks=True,
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if old_policy.alert_template != self.alert_template:
 | 
				
			||||||
 | 
					                cache_agents_alert_template.delay()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def delete(self, *args, **kwargs):
 | 
				
			||||||
 | 
					        from automation.tasks import generate_agent_checks_task
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        agents = list(self.related_agents().only("pk").values_list("pk", flat=True))
 | 
				
			||||||
 | 
					        super(BaseAuditModel, self).delete(*args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        generate_agent_checks_task.delay(agents=agents, create_tasks=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __str__(self):
 | 
					    def __str__(self):
 | 
				
			||||||
        return self.name
 | 
					        return self.name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def is_default_server_policy(self):
 | 
				
			||||||
 | 
					        return self.default_server_policy.exists()  # type: ignore
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def is_default_workstation_policy(self):
 | 
				
			||||||
 | 
					        return self.default_workstation_policy.exists()  # type: ignore
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def is_agent_excluded(self, agent):
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					            agent in self.excluded_agents.all()
 | 
				
			||||||
 | 
					            or agent.site in self.excluded_sites.all()
 | 
				
			||||||
 | 
					            or agent.client in self.excluded_clients.all()
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def related_agents(self):
 | 
					    def related_agents(self):
 | 
				
			||||||
        return self.get_related("server") | self.get_related("workstation")
 | 
					        return self.get_related("server") | self.get_related("workstation")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_related(self, mon_type):
 | 
					    def get_related(self, mon_type):
 | 
				
			||||||
        explicit_agents = self.agents.filter(monitoring_type=mon_type)
 | 
					        explicit_agents = (
 | 
				
			||||||
        explicit_clients = getattr(self, f"{mon_type}_clients").all()
 | 
					            self.agents.filter(monitoring_type=mon_type)  # type: ignore
 | 
				
			||||||
        explicit_sites = getattr(self, f"{mon_type}_sites").all()
 | 
					            .exclude(
 | 
				
			||||||
 | 
					                pk__in=self.excluded_agents.only("pk").values_list("pk", flat=True)
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .exclude(site__in=self.excluded_sites.all())
 | 
				
			||||||
 | 
					            .exclude(site__client__in=self.excluded_clients.all())
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        explicit_clients = getattr(self, f"{mon_type}_clients").exclude(
 | 
				
			||||||
 | 
					            pk__in=self.excluded_clients.all()
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        explicit_sites = getattr(self, f"{mon_type}_sites").exclude(
 | 
				
			||||||
 | 
					            pk__in=self.excluded_sites.all()
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        filtered_agents_pks = Policy.objects.none()
 | 
					        filtered_agents_pks = Policy.objects.none()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        filtered_agents_pks |= Agent.objects.filter(
 | 
					        filtered_agents_pks |= (
 | 
				
			||||||
            site__in=[
 | 
					            Agent.objects.exclude(block_policy_inheritance=True)
 | 
				
			||||||
                site for site in explicit_sites if site.client not in explicit_clients
 | 
					            .filter(
 | 
				
			||||||
            ],
 | 
					                site__in=[
 | 
				
			||||||
            monitoring_type=mon_type,
 | 
					                    site
 | 
				
			||||||
        ).values_list("pk", flat=True)
 | 
					                    for site in explicit_sites
 | 
				
			||||||
 | 
					                    if site.client not in explicit_clients
 | 
				
			||||||
 | 
					                    and site.client not in self.excluded_clients.all()
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					                monitoring_type=mon_type,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .values_list("pk", flat=True)
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        filtered_agents_pks |= Agent.objects.filter(
 | 
					        filtered_agents_pks |= (
 | 
				
			||||||
            site__client__in=[client for client in explicit_clients],
 | 
					            Agent.objects.exclude(block_policy_inheritance=True)
 | 
				
			||||||
            monitoring_type=mon_type,
 | 
					            .exclude(site__block_policy_inheritance=True)
 | 
				
			||||||
        ).values_list("pk", flat=True)
 | 
					            .filter(
 | 
				
			||||||
 | 
					                site__client__in=[client for client in explicit_clients],
 | 
				
			||||||
 | 
					                monitoring_type=mon_type,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .values_list("pk", flat=True)
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return Agent.objects.filter(
 | 
					        return Agent.objects.filter(
 | 
				
			||||||
            models.Q(pk__in=filtered_agents_pks)
 | 
					            models.Q(pk__in=filtered_agents_pks)
 | 
				
			||||||
@@ -58,6 +132,7 @@ class Policy(BaseAuditModel):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    @staticmethod
 | 
					    @staticmethod
 | 
				
			||||||
    def cascade_policy_tasks(agent):
 | 
					    def cascade_policy_tasks(agent):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # List of all tasks to be applied
 | 
					        # List of all tasks to be applied
 | 
				
			||||||
        tasks = list()
 | 
					        tasks = list()
 | 
				
			||||||
        added_task_pks = list()
 | 
					        added_task_pks = list()
 | 
				
			||||||
@@ -80,33 +155,79 @@ class Policy(BaseAuditModel):
 | 
				
			|||||||
            default_policy = CoreSettings.objects.first().server_policy
 | 
					            default_policy = CoreSettings.objects.first().server_policy
 | 
				
			||||||
            client_policy = client.server_policy
 | 
					            client_policy = client.server_policy
 | 
				
			||||||
            site_policy = site.server_policy
 | 
					            site_policy = site.server_policy
 | 
				
			||||||
        else:
 | 
					        elif agent.monitoring_type == "workstation":
 | 
				
			||||||
            default_policy = CoreSettings.objects.first().workstation_policy
 | 
					            default_policy = CoreSettings.objects.first().workstation_policy
 | 
				
			||||||
            client_policy = client.workstation_policy
 | 
					            client_policy = client.workstation_policy
 | 
				
			||||||
            site_policy = site.workstation_policy
 | 
					            site_policy = site.workstation_policy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if agent_policy and agent_policy.active:
 | 
					        # check if client/site/agent is blocking inheritance and blank out policies
 | 
				
			||||||
 | 
					        if agent.block_policy_inheritance:
 | 
				
			||||||
 | 
					            site_policy = None
 | 
				
			||||||
 | 
					            client_policy = None
 | 
				
			||||||
 | 
					            default_policy = None
 | 
				
			||||||
 | 
					        elif site.block_policy_inheritance:
 | 
				
			||||||
 | 
					            client_policy = None
 | 
				
			||||||
 | 
					            default_policy = None
 | 
				
			||||||
 | 
					        elif client.block_policy_inheritance:
 | 
				
			||||||
 | 
					            default_policy = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (
 | 
				
			||||||
 | 
					            agent_policy
 | 
				
			||||||
 | 
					            and agent_policy.active
 | 
				
			||||||
 | 
					            and not agent_policy.is_agent_excluded(agent)
 | 
				
			||||||
 | 
					        ):
 | 
				
			||||||
            for task in agent_policy.autotasks.all():
 | 
					            for task in agent_policy.autotasks.all():
 | 
				
			||||||
                if task.pk not in added_task_pks:
 | 
					                if task.pk not in added_task_pks:
 | 
				
			||||||
                    tasks.append(task)
 | 
					                    tasks.append(task)
 | 
				
			||||||
                    added_task_pks.append(task.pk)
 | 
					                    added_task_pks.append(task.pk)
 | 
				
			||||||
        if site_policy and site_policy.active:
 | 
					        if (
 | 
				
			||||||
 | 
					            site_policy
 | 
				
			||||||
 | 
					            and site_policy.active
 | 
				
			||||||
 | 
					            and not site_policy.is_agent_excluded(agent)
 | 
				
			||||||
 | 
					        ):
 | 
				
			||||||
            for task in site_policy.autotasks.all():
 | 
					            for task in site_policy.autotasks.all():
 | 
				
			||||||
                if task.pk not in added_task_pks:
 | 
					                if task.pk not in added_task_pks:
 | 
				
			||||||
                    tasks.append(task)
 | 
					                    tasks.append(task)
 | 
				
			||||||
                    added_task_pks.append(task.pk)
 | 
					                    added_task_pks.append(task.pk)
 | 
				
			||||||
        if client_policy and client_policy.active:
 | 
					        if (
 | 
				
			||||||
 | 
					            client_policy
 | 
				
			||||||
 | 
					            and client_policy.active
 | 
				
			||||||
 | 
					            and not client_policy.is_agent_excluded(agent)
 | 
				
			||||||
 | 
					        ):
 | 
				
			||||||
            for task in client_policy.autotasks.all():
 | 
					            for task in client_policy.autotasks.all():
 | 
				
			||||||
                if task.pk not in added_task_pks:
 | 
					                if task.pk not in added_task_pks:
 | 
				
			||||||
                    tasks.append(task)
 | 
					                    tasks.append(task)
 | 
				
			||||||
                    added_task_pks.append(task.pk)
 | 
					                    added_task_pks.append(task.pk)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if default_policy and default_policy.active:
 | 
					        if (
 | 
				
			||||||
 | 
					            default_policy
 | 
				
			||||||
 | 
					            and default_policy.active
 | 
				
			||||||
 | 
					            and not default_policy.is_agent_excluded(agent)
 | 
				
			||||||
 | 
					        ):
 | 
				
			||||||
            for task in default_policy.autotasks.all():
 | 
					            for task in default_policy.autotasks.all():
 | 
				
			||||||
                if task.pk not in added_task_pks:
 | 
					                if task.pk not in added_task_pks:
 | 
				
			||||||
                    tasks.append(task)
 | 
					                    tasks.append(task)
 | 
				
			||||||
                    added_task_pks.append(task.pk)
 | 
					                    added_task_pks.append(task.pk)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # remove policy tasks from agent not included in policy
 | 
				
			||||||
 | 
					        for task in agent.autotasks.filter(
 | 
				
			||||||
 | 
					            parent_task__in=[
 | 
				
			||||||
 | 
					                taskpk
 | 
				
			||||||
 | 
					                for taskpk in agent_tasks_parent_pks
 | 
				
			||||||
 | 
					                if taskpk not in added_task_pks
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					        ):
 | 
				
			||||||
 | 
					            if task.sync_status == "initial":
 | 
				
			||||||
 | 
					                task.delete()
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                task.sync_status = "pendingdeletion"
 | 
				
			||||||
 | 
					                task.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # change tasks from pendingdeletion to notsynced if policy was added or changed
 | 
				
			||||||
 | 
					        agent.autotasks.filter(sync_status="pendingdeletion").filter(
 | 
				
			||||||
 | 
					            parent_task__in=[taskpk for taskpk in added_task_pks]
 | 
				
			||||||
 | 
					        ).update(sync_status="notsynced")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return [task for task in tasks if task.pk not in agent_tasks_parent_pks]
 | 
					        return [task for task in tasks if task.pk not in agent_tasks_parent_pks]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @staticmethod
 | 
					    @staticmethod
 | 
				
			||||||
@@ -132,17 +253,32 @@ class Policy(BaseAuditModel):
 | 
				
			|||||||
            default_policy = CoreSettings.objects.first().server_policy
 | 
					            default_policy = CoreSettings.objects.first().server_policy
 | 
				
			||||||
            client_policy = client.server_policy
 | 
					            client_policy = client.server_policy
 | 
				
			||||||
            site_policy = site.server_policy
 | 
					            site_policy = site.server_policy
 | 
				
			||||||
        else:
 | 
					        elif agent.monitoring_type == "workstation":
 | 
				
			||||||
            default_policy = CoreSettings.objects.first().workstation_policy
 | 
					            default_policy = CoreSettings.objects.first().workstation_policy
 | 
				
			||||||
            client_policy = client.workstation_policy
 | 
					            client_policy = client.workstation_policy
 | 
				
			||||||
            site_policy = site.workstation_policy
 | 
					            site_policy = site.workstation_policy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # check if client/site/agent is blocking inheritance and blank out policies
 | 
				
			||||||
 | 
					        if agent.block_policy_inheritance:
 | 
				
			||||||
 | 
					            site_policy = None
 | 
				
			||||||
 | 
					            client_policy = None
 | 
				
			||||||
 | 
					            default_policy = None
 | 
				
			||||||
 | 
					        elif site.block_policy_inheritance:
 | 
				
			||||||
 | 
					            client_policy = None
 | 
				
			||||||
 | 
					            default_policy = None
 | 
				
			||||||
 | 
					        elif client.block_policy_inheritance:
 | 
				
			||||||
 | 
					            default_policy = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Used to hold the policies that will be applied and the order in which they are applied
 | 
					        # Used to hold the policies that will be applied and the order in which they are applied
 | 
				
			||||||
        # Enforced policies are applied first
 | 
					        # Enforced policies are applied first
 | 
				
			||||||
        enforced_checks = list()
 | 
					        enforced_checks = list()
 | 
				
			||||||
        policy_checks = list()
 | 
					        policy_checks = list()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if agent_policy and agent_policy.active:
 | 
					        if (
 | 
				
			||||||
 | 
					            agent_policy
 | 
				
			||||||
 | 
					            and agent_policy.active
 | 
				
			||||||
 | 
					            and not agent_policy.is_agent_excluded(agent)
 | 
				
			||||||
 | 
					        ):
 | 
				
			||||||
            if agent_policy.enforced:
 | 
					            if agent_policy.enforced:
 | 
				
			||||||
                for check in agent_policy.policychecks.all():
 | 
					                for check in agent_policy.policychecks.all():
 | 
				
			||||||
                    enforced_checks.append(check)
 | 
					                    enforced_checks.append(check)
 | 
				
			||||||
@@ -150,7 +286,11 @@ class Policy(BaseAuditModel):
 | 
				
			|||||||
                for check in agent_policy.policychecks.all():
 | 
					                for check in agent_policy.policychecks.all():
 | 
				
			||||||
                    policy_checks.append(check)
 | 
					                    policy_checks.append(check)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if site_policy and site_policy.active:
 | 
					        if (
 | 
				
			||||||
 | 
					            site_policy
 | 
				
			||||||
 | 
					            and site_policy.active
 | 
				
			||||||
 | 
					            and not site_policy.is_agent_excluded(agent)
 | 
				
			||||||
 | 
					        ):
 | 
				
			||||||
            if site_policy.enforced:
 | 
					            if site_policy.enforced:
 | 
				
			||||||
                for check in site_policy.policychecks.all():
 | 
					                for check in site_policy.policychecks.all():
 | 
				
			||||||
                    enforced_checks.append(check)
 | 
					                    enforced_checks.append(check)
 | 
				
			||||||
@@ -158,7 +298,11 @@ class Policy(BaseAuditModel):
 | 
				
			|||||||
                for check in site_policy.policychecks.all():
 | 
					                for check in site_policy.policychecks.all():
 | 
				
			||||||
                    policy_checks.append(check)
 | 
					                    policy_checks.append(check)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if client_policy and client_policy.active:
 | 
					        if (
 | 
				
			||||||
 | 
					            client_policy
 | 
				
			||||||
 | 
					            and client_policy.active
 | 
				
			||||||
 | 
					            and not client_policy.is_agent_excluded(agent)
 | 
				
			||||||
 | 
					        ):
 | 
				
			||||||
            if client_policy.enforced:
 | 
					            if client_policy.enforced:
 | 
				
			||||||
                for check in client_policy.policychecks.all():
 | 
					                for check in client_policy.policychecks.all():
 | 
				
			||||||
                    enforced_checks.append(check)
 | 
					                    enforced_checks.append(check)
 | 
				
			||||||
@@ -166,7 +310,11 @@ class Policy(BaseAuditModel):
 | 
				
			|||||||
                for check in client_policy.policychecks.all():
 | 
					                for check in client_policy.policychecks.all():
 | 
				
			||||||
                    policy_checks.append(check)
 | 
					                    policy_checks.append(check)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if default_policy and default_policy.active:
 | 
					        if (
 | 
				
			||||||
 | 
					            default_policy
 | 
				
			||||||
 | 
					            and default_policy.active
 | 
				
			||||||
 | 
					            and not default_policy.is_agent_excluded(agent)
 | 
				
			||||||
 | 
					        ):
 | 
				
			||||||
            if default_policy.enforced:
 | 
					            if default_policy.enforced:
 | 
				
			||||||
                for check in default_policy.policychecks.all():
 | 
					                for check in default_policy.policychecks.all():
 | 
				
			||||||
                    enforced_checks.append(check)
 | 
					                    enforced_checks.append(check)
 | 
				
			||||||
@@ -280,6 +428,15 @@ class Policy(BaseAuditModel):
 | 
				
			|||||||
            + eventlog_checks
 | 
					            + eventlog_checks
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # remove policy checks from agent that fell out of policy scope
 | 
				
			||||||
 | 
					        agent.agentchecks.filter(
 | 
				
			||||||
 | 
					            parent_check__in=[
 | 
				
			||||||
 | 
					                checkpk
 | 
				
			||||||
 | 
					                for checkpk in agent_checks_parent_pks
 | 
				
			||||||
 | 
					                if checkpk not in [check.pk for check in final_list]
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					        ).delete()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return [
 | 
					        return [
 | 
				
			||||||
            check for check in final_list if check.pk not in agent_checks_parent_pks
 | 
					            check for check in final_list if check.pk not in agent_checks_parent_pks
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,20 +1,18 @@
 | 
				
			|||||||
from rest_framework.serializers import (
 | 
					from rest_framework.serializers import (
 | 
				
			||||||
    ModelSerializer,
 | 
					    ModelSerializer,
 | 
				
			||||||
    SerializerMethodField,
 | 
					 | 
				
			||||||
    StringRelatedField,
 | 
					 | 
				
			||||||
    ReadOnlyField,
 | 
					    ReadOnlyField,
 | 
				
			||||||
 | 
					    SerializerMethodField,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from clients.serializers import ClientSerializer, SiteSerializer
 | 
					 | 
				
			||||||
from agents.serializers import AgentHostnameSerializer
 | 
					from agents.serializers import AgentHostnameSerializer
 | 
				
			||||||
 | 
					 | 
				
			||||||
from .models import Policy
 | 
					 | 
				
			||||||
from agents.models import Agent
 | 
					 | 
				
			||||||
from autotasks.models import AutomatedTask
 | 
					from autotasks.models import AutomatedTask
 | 
				
			||||||
from checks.models import Check
 | 
					from checks.models import Check
 | 
				
			||||||
from clients.models import Client, Site
 | 
					from clients.models import Client
 | 
				
			||||||
 | 
					from clients.serializers import ClientSerializer, SiteSerializer
 | 
				
			||||||
from winupdate.serializers import WinUpdatePolicySerializer
 | 
					from winupdate.serializers import WinUpdatePolicySerializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .models import Policy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PolicySerializer(ModelSerializer):
 | 
					class PolicySerializer(ModelSerializer):
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
@@ -24,15 +22,14 @@ class PolicySerializer(ModelSerializer):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class PolicyTableSerializer(ModelSerializer):
 | 
					class PolicyTableSerializer(ModelSerializer):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    server_clients = ClientSerializer(many=True, read_only=True)
 | 
					 | 
				
			||||||
    server_sites = SiteSerializer(many=True, read_only=True)
 | 
					 | 
				
			||||||
    workstation_clients = ClientSerializer(many=True, read_only=True)
 | 
					 | 
				
			||||||
    workstation_sites = SiteSerializer(many=True, read_only=True)
 | 
					 | 
				
			||||||
    agents = AgentHostnameSerializer(many=True, read_only=True)
 | 
					 | 
				
			||||||
    default_server_policy = ReadOnlyField(source="is_default_server_policy")
 | 
					    default_server_policy = ReadOnlyField(source="is_default_server_policy")
 | 
				
			||||||
    default_workstation_policy = ReadOnlyField(source="is_default_workstation_policy")
 | 
					    default_workstation_policy = ReadOnlyField(source="is_default_workstation_policy")
 | 
				
			||||||
    agents_count = SerializerMethodField(read_only=True)
 | 
					    agents_count = SerializerMethodField(read_only=True)
 | 
				
			||||||
    winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True)
 | 
					    winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True)
 | 
				
			||||||
 | 
					    alert_template = ReadOnlyField(source="alert_template.id")
 | 
				
			||||||
 | 
					    excluded_clients = ClientSerializer(many=True)
 | 
				
			||||||
 | 
					    excluded_sites = SiteSerializer(many=True)
 | 
				
			||||||
 | 
					    excluded_agents = AgentHostnameSerializer(many=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = Policy
 | 
					        model = Policy
 | 
				
			||||||
@@ -78,49 +75,16 @@ class PolicyCheckSerializer(ModelSerializer):
 | 
				
			|||||||
            "assignedtask",
 | 
					            "assignedtask",
 | 
				
			||||||
            "text_alert",
 | 
					            "text_alert",
 | 
				
			||||||
            "email_alert",
 | 
					            "email_alert",
 | 
				
			||||||
 | 
					            "dashboard_alert",
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        depth = 1
 | 
					        depth = 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AutoTasksFieldSerializer(ModelSerializer):
 | 
					class AutoTasksFieldSerializer(ModelSerializer):
 | 
				
			||||||
    assigned_check = PolicyCheckSerializer(read_only=True)
 | 
					    assigned_check = PolicyCheckSerializer(read_only=True)
 | 
				
			||||||
 | 
					    script = ReadOnlyField(source="script.id")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = AutomatedTask
 | 
					        model = AutomatedTask
 | 
				
			||||||
        fields = ("id", "enabled", "name", "schedule", "assigned_check")
 | 
					        fields = "__all__"
 | 
				
			||||||
        depth = 1
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class AutoTaskPolicySerializer(ModelSerializer):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    autotasks = AutoTasksFieldSerializer(many=True, read_only=True)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    class Meta:
 | 
					 | 
				
			||||||
        model = Policy
 | 
					 | 
				
			||||||
        fields = (
 | 
					 | 
				
			||||||
            "id",
 | 
					 | 
				
			||||||
            "name",
 | 
					 | 
				
			||||||
            "autotasks",
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        depth = 2
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class RelatedClientPolicySerializer(ModelSerializer):
 | 
					 | 
				
			||||||
    class Meta:
 | 
					 | 
				
			||||||
        model = Client
 | 
					 | 
				
			||||||
        fields = ("workstation_policy", "server_policy")
 | 
					 | 
				
			||||||
        depth = 1
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class RelatedSitePolicySerializer(ModelSerializer):
 | 
					 | 
				
			||||||
    class Meta:
 | 
					 | 
				
			||||||
        model = Site
 | 
					 | 
				
			||||||
        fields = ("workstation_policy", "server_policy")
 | 
					 | 
				
			||||||
        depth = 1
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class RelatedAgentPolicySerializer(ModelSerializer):
 | 
					 | 
				
			||||||
    class Meta:
 | 
					 | 
				
			||||||
        model = Agent
 | 
					 | 
				
			||||||
        fields = ("policy",)
 | 
					 | 
				
			||||||
        depth = 1
 | 
					        depth = 1
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,127 +1,143 @@
 | 
				
			|||||||
from automation.models import Policy
 | 
					from typing import Any, Dict, List, Union
 | 
				
			||||||
from checks.models import Check
 | 
					 | 
				
			||||||
from agents.models import Agent
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
from tacticalrmm.celery import app
 | 
					from tacticalrmm.celery import app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@app.task
 | 
					@app.task
 | 
				
			||||||
def generate_agent_checks_from_policies_task(
 | 
					def generate_agent_checks_task(
 | 
				
			||||||
    ###
 | 
					    policy: int = None,
 | 
				
			||||||
    # copies the policy checks to all affected agents
 | 
					    site: int = None,
 | 
				
			||||||
    #
 | 
					    client: int = None,
 | 
				
			||||||
    # clear: clears all policy checks first
 | 
					    agents: List[int] = list(),
 | 
				
			||||||
    # create_tasks: also create tasks after checks are generated
 | 
					    all: bool = False,
 | 
				
			||||||
    ###
 | 
					    create_tasks: bool = False,
 | 
				
			||||||
    policypk,
 | 
					) -> Union[str, None]:
 | 
				
			||||||
    clear=False,
 | 
					    from agents.models import Agent
 | 
				
			||||||
    create_tasks=False,
 | 
					 | 
				
			||||||
):
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    policy = Policy.objects.get(pk=policypk)
 | 
					    from automation.models import Policy
 | 
				
			||||||
    for agent in policy.related_agents():
 | 
					
 | 
				
			||||||
        agent.generate_checks_from_policies(clear=clear)
 | 
					    p = Policy.objects.get(pk=policy) if policy else None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # generate checks on all agents if all is specified or if policy is default server/workstation policy
 | 
				
			||||||
 | 
					    if (p and p.is_default_server_policy and p.is_default_workstation_policy) or all:
 | 
				
			||||||
 | 
					        a = Agent.objects.prefetch_related("policy").only("pk", "monitoring_type")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # generate checks on all servers if policy is a default servers policy
 | 
				
			||||||
 | 
					    elif p and p.is_default_server_policy:
 | 
				
			||||||
 | 
					        a = Agent.objects.filter(monitoring_type="server").only("pk", "monitoring_type")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # generate checks on all workstations if policy is a default workstations policy
 | 
				
			||||||
 | 
					    elif p and p.is_default_workstation_policy:
 | 
				
			||||||
 | 
					        a = Agent.objects.filter(monitoring_type="workstation").only(
 | 
				
			||||||
 | 
					            "pk", "monitoring_type"
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # generate checks on a list of supplied agents
 | 
				
			||||||
 | 
					    elif agents:
 | 
				
			||||||
 | 
					        a = Agent.objects.filter(pk__in=agents)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # generate checks on agents affected by supplied policy
 | 
				
			||||||
 | 
					    elif policy:
 | 
				
			||||||
 | 
					        a = p.related_agents().only("pk")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # generate checks that has specified site
 | 
				
			||||||
 | 
					    elif site:
 | 
				
			||||||
 | 
					        a = Agent.objects.filter(site_id=site)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # generate checks that has specified client
 | 
				
			||||||
 | 
					    elif client:
 | 
				
			||||||
 | 
					        a = Agent.objects.filter(site__client_id=client)
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        a = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for agent in a:
 | 
				
			||||||
 | 
					        agent.generate_checks_from_policies()
 | 
				
			||||||
        if create_tasks:
 | 
					        if create_tasks:
 | 
				
			||||||
            agent.generate_tasks_from_policies(
 | 
					            agent.generate_tasks_from_policies()
 | 
				
			||||||
                clear=clear,
 | 
					
 | 
				
			||||||
            )
 | 
					    return "ok"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@app.task
 | 
					@app.task
 | 
				
			||||||
def generate_agent_checks_by_location_task(
 | 
					# updates policy managed check fields on agents
 | 
				
			||||||
    location, mon_type, clear=False, create_tasks=False
 | 
					def update_policy_check_fields_task(check: int) -> str:
 | 
				
			||||||
):
 | 
					    from checks.models import Check
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for agent in Agent.objects.filter(**location).filter(monitoring_type=mon_type):
 | 
					    c: Check = Check.objects.get(pk=check)
 | 
				
			||||||
        agent.generate_checks_from_policies(clear=clear)
 | 
					    update_fields: Dict[Any, Any] = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if create_tasks:
 | 
					    for field in c.policy_fields_to_copy:
 | 
				
			||||||
            agent.generate_tasks_from_policies(clear=clear)
 | 
					        update_fields[field] = getattr(c, field)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Check.objects.filter(parent_check=check).update(**update_fields)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return "ok"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@app.task
 | 
					@app.task
 | 
				
			||||||
def generate_all_agent_checks_task(mon_type, clear=False, create_tasks=False):
 | 
					# generates policy tasks on agents affected by a policy
 | 
				
			||||||
    for agent in Agent.objects.filter(monitoring_type=mon_type):
 | 
					def generate_agent_autotasks_task(policy: int = None) -> str:
 | 
				
			||||||
        agent.generate_checks_from_policies(clear=clear)
 | 
					    from agents.models import Agent
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if create_tasks:
 | 
					    from automation.models import Policy
 | 
				
			||||||
            agent.generate_tasks_from_policies(clear=clear)
 | 
					
 | 
				
			||||||
 | 
					    p: Policy = Policy.objects.get(pk=policy)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if p and p.is_default_server_policy and p.is_default_workstation_policy:
 | 
				
			||||||
 | 
					        agents = Agent.objects.prefetch_related("policy").only("pk", "monitoring_type")
 | 
				
			||||||
 | 
					    elif p and p.is_default_server_policy:
 | 
				
			||||||
 | 
					        agents = Agent.objects.filter(monitoring_type="server").only(
 | 
				
			||||||
 | 
					            "pk", "monitoring_type"
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    elif p and p.is_default_workstation_policy:
 | 
				
			||||||
 | 
					        agents = Agent.objects.filter(monitoring_type="workstation").only(
 | 
				
			||||||
 | 
					            "pk", "monitoring_type"
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        agents = p.related_agents().only("pk")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for agent in agents:
 | 
				
			||||||
 | 
					        agent.generate_tasks_from_policies()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return "ok"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@app.task
 | 
					@app.task
 | 
				
			||||||
def delete_policy_check_task(checkpk):
 | 
					def delete_policy_autotasks_task(task: int) -> str:
 | 
				
			||||||
 | 
					 | 
				
			||||||
    Check.objects.filter(parent_check=checkpk).delete()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@app.task
 | 
					 | 
				
			||||||
def update_policy_check_fields_task(checkpk):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    check = Check.objects.get(pk=checkpk)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Check.objects.filter(parent_check=checkpk).update(
 | 
					 | 
				
			||||||
        threshold=check.threshold,
 | 
					 | 
				
			||||||
        name=check.name,
 | 
					 | 
				
			||||||
        fails_b4_alert=check.fails_b4_alert,
 | 
					 | 
				
			||||||
        ip=check.ip,
 | 
					 | 
				
			||||||
        script_args=check.script_args,
 | 
					 | 
				
			||||||
        timeout=check.timeout,
 | 
					 | 
				
			||||||
        pass_if_start_pending=check.pass_if_start_pending,
 | 
					 | 
				
			||||||
        pass_if_svc_not_exist=check.pass_if_svc_not_exist,
 | 
					 | 
				
			||||||
        restart_if_stopped=check.restart_if_stopped,
 | 
					 | 
				
			||||||
        log_name=check.log_name,
 | 
					 | 
				
			||||||
        event_id=check.event_id,
 | 
					 | 
				
			||||||
        event_id_is_wildcard=check.event_id_is_wildcard,
 | 
					 | 
				
			||||||
        event_type=check.event_type,
 | 
					 | 
				
			||||||
        event_source=check.event_source,
 | 
					 | 
				
			||||||
        event_message=check.event_message,
 | 
					 | 
				
			||||||
        fail_when=check.fail_when,
 | 
					 | 
				
			||||||
        search_last_days=check.search_last_days,
 | 
					 | 
				
			||||||
        email_alert=check.email_alert,
 | 
					 | 
				
			||||||
        text_alert=check.text_alert,
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@app.task
 | 
					 | 
				
			||||||
def generate_agent_tasks_from_policies_task(policypk, clear=False):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    policy = Policy.objects.get(pk=policypk)
 | 
					 | 
				
			||||||
    for agent in policy.related_agents():
 | 
					 | 
				
			||||||
        agent.generate_tasks_from_policies(clear=clear)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@app.task
 | 
					 | 
				
			||||||
def generate_agent_tasks_by_location_task(location, mon_type, clear=False):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for agent in Agent.objects.filter(**location).filter(monitoring_type=mon_type):
 | 
					 | 
				
			||||||
        agent.generate_tasks_from_policies(clear=clear)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@app.task
 | 
					 | 
				
			||||||
def delete_policy_autotask_task(taskpk):
 | 
					 | 
				
			||||||
    from autotasks.tasks import delete_win_task_schedule
 | 
					 | 
				
			||||||
    from autotasks.models import AutomatedTask
 | 
					    from autotasks.models import AutomatedTask
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for task in AutomatedTask.objects.filter(parent_task=taskpk):
 | 
					    for t in AutomatedTask.objects.filter(parent_task=task):
 | 
				
			||||||
        delete_win_task_schedule.delay(task.pk)
 | 
					        t.delete_task_on_agent()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return "ok"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@app.task
 | 
					@app.task
 | 
				
			||||||
def run_win_policy_autotask_task(task_pks):
 | 
					def run_win_policy_autotasks_task(task: int) -> str:
 | 
				
			||||||
    from autotasks.tasks import run_win_task
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for task in task_pks:
 | 
					 | 
				
			||||||
        run_win_task.delay(task)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@app.task
 | 
					 | 
				
			||||||
def update_policy_task_fields_task(taskpk, enabled):
 | 
					 | 
				
			||||||
    from autotasks.models import AutomatedTask
 | 
					    from autotasks.models import AutomatedTask
 | 
				
			||||||
    from autotasks.tasks import enable_or_disable_win_task
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    tasks = AutomatedTask.objects.filter(parent_task=taskpk)
 | 
					    for t in AutomatedTask.objects.filter(parent_task=task):
 | 
				
			||||||
 | 
					        t.run_win_task()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    tasks.update(enabled=enabled)
 | 
					    return "ok"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for autotask in tasks:
 | 
					
 | 
				
			||||||
        enable_or_disable_win_task(autotask.pk, enabled)
 | 
					@app.task
 | 
				
			||||||
 | 
					def update_policy_autotasks_fields_task(task: int, update_agent: bool = False) -> str:
 | 
				
			||||||
 | 
					    from autotasks.models import AutomatedTask
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    t = AutomatedTask.objects.get(pk=task)
 | 
				
			||||||
 | 
					    update_fields: Dict[str, Any] = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for field in t.policy_fields_to_copy:
 | 
				
			||||||
 | 
					        update_fields[field] = getattr(t, field)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    AutomatedTask.objects.filter(parent_task=task).update(**update_fields)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if update_agent:
 | 
				
			||||||
 | 
					        for t in AutomatedTask.objects.filter(parent_task=task).exclude(
 | 
				
			||||||
 | 
					            sync_status="initial"
 | 
				
			||||||
 | 
					        ):
 | 
				
			||||||
 | 
					            t.modify_task_on_agent()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return "ok"
 | 
				
			||||||
 
 | 
				
			|||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user