Compare commits
	
		
			877 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					7284d9fcd8 | ||
| 
						 | 
					515394049a | ||
| 
						 | 
					35c8b4f535 | ||
| 
						 | 
					1a325a66b4 | ||
| 
						 | 
					7d82116fb9 | ||
| 
						 | 
					8a7bd4f21b | ||
| 
						 | 
					2e5a2ef12d | ||
| 
						 | 
					89aceda65a | ||
| 
						 | 
					39fd83aa16 | ||
| 
						 | 
					a23d811fe8 | ||
| 
						 | 
					a238779724 | ||
| 
						 | 
					3a848bc037 | ||
| 
						 | 
					0528ecb454 | ||
| 
						 | 
					141835593c | ||
| 
						 | 
					3d06200368 | ||
| 
						 | 
					729bef9a77 | ||
| 
						 | 
					94f33bd642 | ||
| 
						 | 
					7e010cdbca | ||
| 
						 | 
					8887bcd941 | ||
| 
						 | 
					56aeeee04c | ||
| 
						 | 
					98eb3c7287 | ||
| 
						 | 
					6819c1989b | ||
| 
						 | 
					7e01dd3e97 | ||
| 
						 | 
					ea4f2c3de8 | ||
| 
						 | 
					b2f63b8761 | ||
| 
						 | 
					65865101ce | ||
| 
						 | 
					c3637afe69 | ||
| 
						 | 
					ab543ddf0c | ||
| 
						 | 
					80595e76e7 | ||
| 
						 | 
					d49e68737a | ||
| 
						 | 
					712e15ba80 | ||
| 
						 | 
					986160e667 | ||
| 
						 | 
					1ae4e23db1 | ||
| 
						 | 
					bad646141c | ||
| 
						 | 
					7911235b68 | ||
| 
						 | 
					12dee4d14d | ||
| 
						 | 
					cba841beb8 | ||
| 
						 | 
					4e3ebf7078 | ||
| 
						 | 
					1c34969f64 | ||
| 
						 | 
					dc26cabacd | ||
| 
						 | 
					a7bffcd471 | ||
| 
						 | 
					6ae56ac2cc | ||
| 
						 | 
					03c087020c | ||
| 
						 | 
					857a1ab9c4 | ||
| 
						 | 
					64d9530e13 | ||
| 
						 | 
					5dac1efc30 | ||
| 
						 | 
					18bc74bc96 | ||
| 
						 | 
					f64efc63f8 | ||
| 
						 | 
					e84b897991 | ||
| 
						 | 
					519647ef93 | ||
| 
						 | 
					f694fe00e4 | ||
| 
						 | 
					0b951f27b6 | ||
| 
						 | 
					8aa082c9df | ||
| 
						 | 
					f2c5d47bd8 | ||
| 
						 | 
					ac7642cc15 | ||
| 
						 | 
					8f34865dab | ||
| 
						 | 
					c762d12a40 | ||
| 
						 | 
					fe1e71dc07 | ||
| 
						 | 
					85b0350ed4 | ||
| 
						 | 
					a980491455 | ||
| 
						 | 
					5798c0ccaa | ||
| 
						 | 
					742f49ca1f | ||
| 
						 | 
					5560fc805b | ||
| 
						 | 
					9d4f8a4e8c | ||
| 
						 | 
					b4d25d6285 | ||
| 
						 | 
					a504a376bd | ||
| 
						 | 
					f61ea6e90a | ||
| 
						 | 
					b2651df36f | ||
| 
						 | 
					b56c086841 | ||
| 
						 | 
					0b92fee42e | ||
| 
						 | 
					4343478c7b | ||
| 
						 | 
					94649cbfc7 | ||
| 
						 | 
					fb83f84d84 | ||
| 
						 | 
					e099a5a32e | ||
| 
						 | 
					84c2632d40 | ||
| 
						 | 
					3417ee25eb | ||
| 
						 | 
					6ada30102c | ||
| 
						 | 
					ac86ca7266 | ||
| 
						 | 
					bb1d3edf71 | ||
| 
						 | 
					97b9253017 | ||
| 
						 | 
					971c2180c9 | ||
| 
						 | 
					f96dc6991e | ||
| 
						 | 
					6855493b2f | ||
| 
						 | 
					ff0d1f7c42 | ||
| 
						 | 
					3ae5824761 | ||
| 
						 | 
					702e865715 | ||
| 
						 | 
					6bcf64c83f | ||
| 
						 | 
					18b270c9d0 | ||
| 
						 | 
					783376acb0 | ||
| 
						 | 
					81dab470d2 | ||
| 
						 | 
					a12f0feb66 | ||
| 
						 | 
					d3c99d9c1c | ||
| 
						 | 
					3eb3586c0f | ||
| 
						 | 
					fdde16cf56 | ||
| 
						 | 
					b8bc5596fd | ||
| 
						 | 
					47842a79c7 | ||
| 
						 | 
					391d5bc386 | ||
| 
						 | 
					ba8561e357 | ||
| 
						 | 
					6aa1170cef | ||
| 
						 | 
					6d4363e685 | ||
| 
						 | 
					6b02b1e1e8 | ||
| 
						 | 
					df3e68fbaf | ||
| 
						 | 
					58a5550989 | ||
| 
						 | 
					ccc9e44ace | ||
| 
						 | 
					f225c5cf9a | ||
| 
						 | 
					5c62c7992c | ||
| 
						 | 
					70b8f09ccb | ||
| 
						 | 
					abfeafa026 | ||
| 
						 | 
					aa029b005f | ||
| 
						 | 
					6cc55e8f36 | ||
| 
						 | 
					b753d2ca1e | ||
| 
						 | 
					1e50329c9e | ||
| 
						 | 
					4942811694 | ||
| 
						 | 
					59e37e0ccb | ||
| 
						 | 
					20aa86d8a9 | ||
| 
						 | 
					64c5ab7042 | ||
| 
						 | 
					d210f5171a | ||
| 
						 | 
					c7eee0f14d | ||
| 
						 | 
					221753b62e | ||
| 
						 | 
					d213e4d37f | ||
| 
						 | 
					f8695f21d3 | ||
| 
						 | 
					4ac1030289 | ||
| 
						 | 
					93c7117319 | ||
| 
						 | 
					974afd92ce | ||
| 
						 | 
					dd1d15f1a4 | ||
| 
						 | 
					be847baaed | ||
| 
						 | 
					2b819e6751 | ||
| 
						 | 
					66247cc005 | ||
| 
						 | 
					eafd38d3f2 | ||
| 
						 | 
					c4e590e7a0 | ||
| 
						 | 
					b92a594114 | ||
| 
						 | 
					9dfb16f6b8 | ||
| 
						 | 
					4b74866d85 | ||
| 
						 | 
					f532c85247 | ||
| 
						 | 
					b1cc00c1bc | ||
| 
						 | 
					5696aa49d5 | ||
| 
						 | 
					e12dc936fd | ||
| 
						 | 
					6d39a7fb75 | ||
| 
						 | 
					c87c312349 | ||
| 
						 | 
					e9c1886cdd | ||
| 
						 | 
					13e4b1a781 | ||
| 
						 | 
					3766fb14ef | ||
| 
						 | 
					29ee50e38b | ||
| 
						 | 
					d1ab69dc31 | ||
| 
						 | 
					e3c4a54193 | ||
| 
						 | 
					2abbd2e3cf | ||
| 
						 | 
					f9387a5851 | ||
| 
						 | 
					7a9fb74b54 | ||
| 
						 | 
					d754f3dd4c | ||
| 
						 | 
					f54fc9e990 | ||
| 
						 | 
					8952095da5 | ||
| 
						 | 
					597240d501 | ||
| 
						 | 
					7377906d02 | ||
| 
						 | 
					ce6da1bce3 | ||
| 
						 | 
					1bf8ff73f8 | ||
| 
						 | 
					564aaaf3df | ||
| 
						 | 
					64ba69b2d0 | ||
| 
						 | 
					ce5ada42af | ||
| 
						 | 
					1ce5973713 | ||
| 
						 | 
					b035b53092 | ||
| 
						 | 
					7d0e02358c | ||
| 
						 | 
					374ff0aeb5 | ||
| 
						 | 
					947a43111e | ||
| 
						 | 
					9970911249 | ||
| 
						 | 
					5fed81c27b | ||
| 
						 | 
					dce4f1a5ae | ||
| 
						 | 
					7e1fc32a1c | ||
| 
						 | 
					a69f14f504 | ||
| 
						 | 
					931069458d | ||
| 
						 | 
					a5259baab0 | ||
| 
						 | 
					8aaa27350d | ||
| 
						 | 
					6db6eb70da | ||
| 
						 | 
					ac74d2b7c2 | ||
| 
						 | 
					2b316aeae9 | ||
| 
						 | 
					aff96a45c6 | ||
| 
						 | 
					9ee246440f | ||
| 
						 | 
					e2f524ce7a | ||
| 
						 | 
					a58b054292 | ||
| 
						 | 
					ea9e5be1fc | ||
| 
						 | 
					760ea4727c | ||
| 
						 | 
					f57f2e53a0 | ||
| 
						 | 
					136a393a17 | ||
| 
						 | 
					8bbaab78b7 | ||
| 
						 | 
					067cd59637 | ||
| 
						 | 
					ce6ac7bf53 | ||
| 
						 | 
					99271c4477 | ||
| 
						 | 
					156142ed58 | ||
| 
						 | 
					4b5516c0eb | ||
| 
						 | 
					c3d8d2d240 | ||
| 
						 | 
					c29cf70025 | ||
| 
						 | 
					6ebce55be3 | ||
| 
						 | 
					01c4a85bc0 | ||
| 
						 | 
					12d4206d84 | ||
| 
						 | 
					946de18bea | ||
| 
						 | 
					904eb3538c | ||
| 
						 | 
					c851ca9328 | ||
| 
						 | 
					0ac415ad83 | ||
| 
						 | 
					b3ba34d980 | ||
| 
						 | 
					52740271d9 | ||
| 
						 | 
					c2e444249a | ||
| 
						 | 
					97310b091e | ||
| 
						 | 
					4dda9cc3a1 | ||
| 
						 | 
					a0538b57e2 | ||
| 
						 | 
					d7f394eeb6 | ||
| 
						 | 
					1bc4571d42 | ||
| 
						 | 
					22e878502a | ||
| 
						 | 
					03c1b6e30c | ||
| 
						 | 
					374a434d98 | ||
| 
						 | 
					f1e85ff0e9 | ||
| 
						 | 
					6b010f76ea | ||
| 
						 | 
					0c3e9f7824 | ||
| 
						 | 
					ccca578622 | ||
| 
						 | 
					56f7c18550 | ||
| 
						 | 
					d438f71bbb | ||
| 
						 | 
					ca5df24b6d | ||
| 
						 | 
					4a6c2d106f | ||
| 
						 | 
					cd25a9568b | ||
| 
						 | 
					f78a787adb | ||
| 
						 | 
					dc520fa77c | ||
| 
						 | 
					8f06d4dd9d | ||
| 
						 | 
					a7047183e1 | ||
| 
						 | 
					c0b145da24 | ||
| 
						 | 
					52e7fd6f72 | ||
| 
						 | 
					4bbe22b1c7 | ||
| 
						 | 
					4747ffc08b | ||
| 
						 | 
					9d07131fd6 | ||
| 
						 | 
					721126d3db | ||
| 
						 | 
					2b65f5e3dc | ||
| 
						 | 
					57f10cf387 | ||
| 
						 | 
					f60c8a173b | ||
| 
						 | 
					857cd690be | ||
| 
						 | 
					a407b60152 | ||
| 
						 | 
					2c3c55adc0 | ||
| 
						 | 
					f586b4da17 | ||
| 
						 | 
					0b7eb41049 | ||
| 
						 | 
					bd19c4e2bd | ||
| 
						 | 
					e8a73087d6 | ||
| 
						 | 
					dde4fd82f4 | ||
| 
						 | 
					0420c393f3 | ||
| 
						 | 
					c88dac6437 | ||
| 
						 | 
					cd450f55e2 | ||
| 
						 | 
					190ee7f9fb | ||
| 
						 | 
					fd057300cc | ||
| 
						 | 
					56791089c1 | ||
| 
						 | 
					e91cb32ca3 | ||
| 
						 | 
					9ab20df8d2 | ||
| 
						 | 
					050350501c | ||
| 
						 | 
					d078acdf73 | ||
| 
						 | 
					b786a688b5 | ||
| 
						 | 
					6b7fe40dd2 | ||
| 
						 | 
					6f6c422246 | ||
| 
						 | 
					d371ff4f60 | ||
| 
						 | 
					d1a8348912 | ||
| 
						 | 
					be956d3cb6 | ||
| 
						 | 
					ba5beb81b7 | ||
| 
						 | 
					106bbe5244 | ||
| 
						 | 
					f39d0e7ba2 | ||
| 
						 | 
					de7a1fd8ff | ||
| 
						 | 
					1ac2b25876 | ||
| 
						 | 
					9e014d1371 | ||
| 
						 | 
					93b274a113 | ||
| 
						 | 
					474c7ae873 | ||
| 
						 | 
					31690d4cad | ||
| 
						 | 
					bbfc7e7e49 | ||
| 
						 | 
					1c0aa55e7a | ||
| 
						 | 
					29778ca19e | ||
| 
						 | 
					9e87318cc5 | ||
| 
						 | 
					c645be6b70 | ||
| 
						 | 
					57fc5ac088 | ||
| 
						 | 
					924774f52a | ||
| 
						 | 
					446a7a0844 | ||
| 
						 | 
					5cfeed76d0 | ||
| 
						 | 
					de419319d8 | ||
| 
						 | 
					7a3d36899b | ||
| 
						 | 
					f5dbb363f4 | ||
| 
						 | 
					2bbc59a212 | ||
| 
						 | 
					3403d76aae | ||
| 
						 | 
					58399cedb6 | ||
| 
						 | 
					9bca7e9e11 | ||
| 
						 | 
					3a61430e44 | ||
| 
						 | 
					7d8c783a7d | ||
| 
						 | 
					a2e996b550 | ||
| 
						 | 
					cfc1c31050 | ||
| 
						 | 
					45106bf6f9 | ||
| 
						 | 
					6e3cfe491b | ||
| 
						 | 
					12f2158afd | ||
| 
						 | 
					6d78773c55 | ||
| 
						 | 
					43a62d4eb6 | ||
| 
						 | 
					cc08dfda96 | ||
| 
						 | 
					622e33588e | ||
| 
						 | 
					67980b58a0 | ||
| 
						 | 
					027e444955 | ||
| 
						 | 
					d838750389 | ||
| 
						 | 
					71d8bd5266 | ||
| 
						 | 
					ec4ae24bbd | ||
| 
						 | 
					1128149359 | ||
| 
						 | 
					bdfc6634ec | ||
| 
						 | 
					ca4d19667b | ||
| 
						 | 
					c71aa7baa7 | ||
| 
						 | 
					fd80ccd2c5 | ||
| 
						 | 
					9dc0b24399 | ||
| 
						 | 
					747954e6fb | ||
| 
						 | 
					274f4f227e | ||
| 
						 | 
					92197d8d49 | ||
| 
						 | 
					aee06920eb | ||
| 
						 | 
					5111b17d3c | ||
| 
						 | 
					2849d8f45d | ||
| 
						 | 
					bac60d9bd4 | ||
| 
						 | 
					9c797162f4 | ||
| 
						 | 
					09d184e2f8 | ||
| 
						 | 
					7bca618906 | ||
| 
						 | 
					67607103e9 | ||
| 
						 | 
					73c9956fe4 | ||
| 
						 | 
					b42f2ffe33 | ||
| 
						 | 
					30a3f185ef | ||
| 
						 | 
					4f1b41227f | ||
| 
						 | 
					83b9d13ec9 | ||
| 
						 | 
					cee7896c37 | ||
| 
						 | 
					0377009d2b | ||
| 
						 | 
					b472f3644e | ||
| 
						 | 
					5d8ea837c8 | ||
| 
						 | 
					82de6bc849 | ||
| 
						 | 
					cb4bc68c48 | ||
| 
						 | 
					3ce6b38247 | ||
| 
						 | 
					716c0fe979 | ||
| 
						 | 
					c993790b7a | ||
| 
						 | 
					aa32286531 | ||
| 
						 | 
					6f94abde00 | ||
| 
						 | 
					fa19538c9d | ||
| 
						 | 
					84c858b878 | ||
| 
						 | 
					865de142d4 | ||
| 
						 | 
					9118162553 | ||
| 
						 | 
					f4fc6ee9b4 | ||
| 
						 | 
					108c38d57b | ||
| 
						 | 
					a1d73eb830 | ||
| 
						 | 
					997906a610 | ||
| 
						 | 
					b6e5d120d3 | ||
| 
						 | 
					d469d0b435 | ||
| 
						 | 
					e9f823e000 | ||
| 
						 | 
					d7fb76ba74 | ||
| 
						 | 
					b7dde1a0d9 | ||
| 
						 | 
					15095d8c23 | ||
| 
						 | 
					dfbebc7606 | ||
| 
						 | 
					895309d93d | ||
| 
						 | 
					bcf50e821a | ||
| 
						 | 
					30195800dd | ||
| 
						 | 
					6532b0f149 | ||
| 
						 | 
					5e108e4057 | ||
| 
						 | 
					c2b2f4d222 | ||
| 
						 | 
					bc4329ad21 | ||
| 
						 | 
					aec6d1b2f6 | ||
| 
						 | 
					2baf119299 | ||
| 
						 | 
					6fe4c5a2ed | ||
| 
						 | 
					4abc8e41d8 | ||
| 
						 | 
					af694f1ce9 | ||
| 
						 | 
					7c3a5fcb83 | ||
| 
						 | 
					57f64b18c6 | ||
| 
						 | 
					4cccc7c2f8 | ||
| 
						 | 
					903a2d6a6e | ||
| 
						 | 
					34c674487a | ||
| 
						 | 
					d15a8c5af3 | ||
| 
						 | 
					3e0dec9383 | ||
| 
						 | 
					8b810aad81 | ||
| 
						 | 
					e676bcb4f4 | ||
| 
						 | 
					a7aed77764 | ||
| 
						 | 
					88875c0257 | ||
| 
						 | 
					f711a0c91a | ||
| 
						 | 
					d8a076cc6e | ||
| 
						 | 
					c900831ee9 | ||
| 
						 | 
					76a30c7ef4 | ||
| 
						 | 
					ae5d0b1d81 | ||
| 
						 | 
					cd5e87be34 | ||
| 
						 | 
					3e967f58d2 | ||
| 
						 | 
					1ea005ba7e | ||
| 
						 | 
					092772ba90 | ||
| 
						 | 
					b959854a76 | ||
| 
						 | 
					8ccb1ebe4f | ||
| 
						 | 
					91b3be6467 | ||
| 
						 | 
					d79d5feacc | ||
| 
						 | 
					5cc78ef9d5 | ||
| 
						 | 
					8639cd5a72 | ||
| 
						 | 
					021ddc17e7 | ||
| 
						 | 
					ee47b8d004 | ||
| 
						 | 
					55d267c935 | ||
| 
						 | 
					0fd0b9128d | ||
| 
						 | 
					d9cf505b50 | ||
| 
						 | 
					6079332dda | ||
| 
						 | 
					929ec20365 | ||
| 
						 | 
					d0cad3055f | ||
| 
						 | 
					4974a13bc0 | ||
| 
						 | 
					bd048df225 | ||
| 
						 | 
					ed83cbd574 | ||
| 
						 | 
					7230207853 | ||
| 
						 | 
					1ead8a72ab | ||
| 
						 | 
					36a2e9d931 | ||
| 
						 | 
					0f147a5518 | ||
| 
						 | 
					fce511a18b | ||
| 
						 | 
					64bb61b009 | ||
| 
						 | 
					c6eefec5ce | ||
| 
						 | 
					4c6f829c92 | ||
| 
						 | 
					8c5cdd2acb | ||
| 
						 | 
					e5357599c4 | ||
| 
						 | 
					3800f19966 | ||
| 
						 | 
					7336f84a4b | ||
| 
						 | 
					7bf4a5b2b5 | ||
| 
						 | 
					43a7b97218 | ||
| 
						 | 
					9f95c57a09 | ||
| 
						 | 
					8f6056ae66 | ||
| 
						 | 
					9bcac6b10e | ||
| 
						 | 
					86318e1b7d | ||
| 
						 | 
					a8a1458833 | ||
| 
						 | 
					942c1e2dfe | ||
| 
						 | 
					a6b6814eae | ||
| 
						 | 
					0af95aa9b1 | ||
| 
						 | 
					b4b9256867 | ||
| 
						 | 
					a6f1281a98 | ||
| 
						 | 
					b54480928a | ||
| 
						 | 
					741c74e267 | ||
| 
						 | 
					3061dba5ed | ||
| 
						 | 
					09f5f4027e | ||
| 
						 | 
					925695fd56 | ||
| 
						 | 
					3c758be856 | ||
| 
						 | 
					569b76a7e3 | ||
| 
						 | 
					dca69eff9c | ||
| 
						 | 
					6b8fedc675 | ||
| 
						 | 
					c42a379e7c | ||
| 
						 | 
					a40858adbf | ||
| 
						 | 
					19bc720bc9 | ||
| 
						 | 
					bf79ca30bb | ||
| 
						 | 
					75454895e5 | ||
| 
						 | 
					c81aa2d6fe | ||
| 
						 | 
					376f6369b8 | ||
| 
						 | 
					b1e67a1ed3 | ||
| 
						 | 
					7393a30bd1 | ||
| 
						 | 
					c934065f8e | ||
| 
						 | 
					56124d2b50 | ||
| 
						 | 
					e8a003ff8a | ||
| 
						 | 
					4c789225b2 | ||
| 
						 | 
					59dcdd5393 | ||
| 
						 | 
					b28316a4f2 | ||
| 
						 | 
					4f44671acd | ||
| 
						 | 
					b5eed69712 | ||
| 
						 | 
					b79aacb2a7 | ||
| 
						 | 
					8a2eb7b058 | ||
| 
						 | 
					7316d076a2 | ||
| 
						 | 
					479d3bcb40 | ||
| 
						 | 
					f2358f1530 | ||
| 
						 | 
					47d9e1b966 | ||
| 
						 | 
					c53657d693 | ||
| 
						 | 
					f19ce59e00 | ||
| 
						 | 
					076f3e05d6 | ||
| 
						 | 
					7d017f9494 | ||
| 
						 | 
					675de4e420 | ||
| 
						 | 
					418a709c6c | ||
| 
						 | 
					1d7dd1b754 | ||
| 
						 | 
					3fa70d6d2b | ||
| 
						 | 
					9c67f52161 | ||
| 
						 | 
					9f2f23fa96 | ||
| 
						 | 
					46d955691a | ||
| 
						 | 
					3f8800187d | ||
| 
						 | 
					ebbe90dfa8 | ||
| 
						 | 
					074f898160 | ||
| 
						 | 
					a0e1783e18 | ||
| 
						 | 
					fc83e11d8b | ||
| 
						 | 
					f43627b170 | ||
| 
						 | 
					8964441f44 | ||
| 
						 | 
					cfd7a0c621 | ||
| 
						 | 
					15a422873e | ||
| 
						 | 
					d1f5583cd7 | ||
| 
						 | 
					08f07c6f3e | ||
| 
						 | 
					35a08debc3 | ||
| 
						 | 
					a3424c480f | ||
| 
						 | 
					118ced0a43 | ||
| 
						 | 
					6d355ef0cd | ||
| 
						 | 
					a8aa5ac231 | ||
| 
						 | 
					df6bc0b3c9 | ||
| 
						 | 
					6b965b765c | ||
| 
						 | 
					d7aea6b5ba | ||
| 
						 | 
					1e9a46855d | ||
| 
						 | 
					91e9c18110 | ||
| 
						 | 
					8ffa6088d7 | ||
| 
						 | 
					52d2f8364f | ||
| 
						 | 
					1f679af6fa | ||
| 
						 | 
					1ba92cdcd5 | ||
| 
						 | 
					45c60ba5f5 | ||
| 
						 | 
					d3eef45608 | ||
| 
						 | 
					1960c113d4 | ||
| 
						 | 
					63d6b4a1c9 | ||
| 
						 | 
					9f47bb1252 | ||
| 
						 | 
					df4fea31d0 | ||
| 
						 | 
					98ef1484c8 | ||
| 
						 | 
					c4ef9960b9 | ||
| 
						 | 
					6b6f7744aa | ||
| 
						 | 
					9192fa0fe2 | ||
| 
						 | 
					3c7c2dc1a5 | ||
| 
						 | 
					5c176a1af0 | ||
| 
						 | 
					6d03a1cc76 | ||
| 
						 | 
					1cf10edef1 | ||
| 
						 | 
					6a97c63bf4 | ||
| 
						 | 
					15f9612bfa | ||
| 
						 | 
					9a7c90b194 | ||
| 
						 | 
					91f2708a87 | ||
| 
						 | 
					7bf3ecd89d | ||
| 
						 | 
					4768581631 | ||
| 
						 | 
					aa4cd10e13 | ||
| 
						 | 
					066396916d | ||
| 
						 | 
					34ae57e6fe | ||
| 
						 | 
					107c2b50e2 | ||
| 
						 | 
					a832765203 | ||
| 
						 | 
					977fee82b5 | ||
| 
						 | 
					8c74cbc1c6 | ||
| 
						 | 
					b38eec5039 | ||
| 
						 | 
					6c20b932fa | ||
| 
						 | 
					deb24c638f | ||
| 
						 | 
					40fcdb4d28 | ||
| 
						 | 
					f3e44cf458 | ||
| 
						 | 
					498748217d | ||
| 
						 | 
					483bf331fa | ||
| 
						 | 
					9d62b4acdd | ||
| 
						 | 
					c9deef6e76 | ||
| 
						 | 
					8ba6f8b0e1 | ||
| 
						 | 
					824cbdc84b | ||
| 
						 | 
					448c59ea88 | ||
| 
						 | 
					91b858bf33 | ||
| 
						 | 
					c12bede980 | ||
| 
						 | 
					71e9fa3d16 | ||
| 
						 | 
					6800b9aaae | ||
| 
						 | 
					77d44f25f9 | ||
| 
						 | 
					ab6227828b | ||
| 
						 | 
					719ba56c59 | ||
| 
						 | 
					dacedf4018 | ||
| 
						 | 
					2526fa3c47 | ||
| 
						 | 
					7e2295c382 | ||
| 
						 | 
					6ef02004ff | ||
| 
						 | 
					0e60d062e9 | ||
| 
						 | 
					80a94f97c4 | ||
| 
						 | 
					c18bc5fe67 | ||
| 
						 | 
					02b98a2429 | ||
| 
						 | 
					0383aeaa87 | ||
| 
						 | 
					15a41d532e | ||
| 
						 | 
					0f49725789 | ||
| 
						 | 
					1db6733e66 | ||
| 
						 | 
					0343ee4f6b | ||
| 
						 | 
					2c37d2233a | ||
| 
						 | 
					0cb8ccfddd | ||
| 
						 | 
					41c0e85d00 | ||
| 
						 | 
					35b1a39ed8 | ||
| 
						 | 
					61a577ba70 | ||
| 
						 | 
					a1e32584fa | ||
| 
						 | 
					28e0ee536d | ||
| 
						 | 
					9d64a9c038 | ||
| 
						 | 
					702ba969c2 | ||
| 
						 | 
					6dde8ee2b8 | ||
| 
						 | 
					018420310c | ||
| 
						 | 
					6d49d34033 | ||
| 
						 | 
					1fbd403164 | ||
| 
						 | 
					13f544d2be | ||
| 
						 | 
					3c9e64de81 | ||
| 
						 | 
					5a9bafbc32 | ||
| 
						 | 
					b89d96b66f | ||
| 
						 | 
					b7176191ac | ||
| 
						 | 
					453c5f47c2 | ||
| 
						 | 
					eea62e1263 | ||
| 
						 | 
					4fb2a0f1ca | ||
| 
						 | 
					1d102ef096 | ||
| 
						 | 
					bf3c65778e | ||
| 
						 | 
					df7fe3e6b4 | ||
| 
						 | 
					b657468b62 | ||
| 
						 | 
					4edc0058d3 | ||
| 
						 | 
					2c3b35293b | ||
| 
						 | 
					be0c9a4d46 | ||
| 
						 | 
					dd4140558e | ||
| 
						 | 
					71c2519b8e | ||
| 
						 | 
					badfc26aed | ||
| 
						 | 
					b2bc3adb3d | ||
| 
						 | 
					5ccf408fd6 | ||
| 
						 | 
					da185875bb | ||
| 
						 | 
					af16912541 | ||
| 
						 | 
					1bf9e2a5e6 | ||
| 
						 | 
					5a572651ff | ||
| 
						 | 
					5a191e387f | ||
| 
						 | 
					18f29f5790 | ||
| 
						 | 
					054a73e0f8 | ||
| 
						 | 
					14824db7b0 | ||
| 
						 | 
					721c48ea88 | ||
| 
						 | 
					ed7bfcfb58 | ||
| 
						 | 
					773a40a126 | ||
| 
						 | 
					961252ef26 | ||
| 
						 | 
					a2650f3c47 | ||
| 
						 | 
					d71ee194e1 | ||
| 
						 | 
					22e1a4cf41 | ||
| 
						 | 
					a50bf901d3 | ||
| 
						 | 
					c9469635b5 | ||
| 
						 | 
					36df3278e5 | ||
| 
						 | 
					cb2258aaa8 | ||
| 
						 | 
					0391d9eb7e | ||
| 
						 | 
					12698b4c20 | ||
| 
						 | 
					f7b9d459ab | ||
| 
						 | 
					65ab14e68b | ||
| 
						 | 
					93a5dd5de4 | ||
| 
						 | 
					61807bdaaa | ||
| 
						 | 
					a1a5d1adba | ||
| 
						 | 
					9dd4aefea5 | ||
| 
						 | 
					db4540089a | ||
| 
						 | 
					24c899c91a | ||
| 
						 | 
					ade1a73966 | ||
| 
						 | 
					fb9ec2b040 | ||
| 
						 | 
					3a683812e9 | ||
| 
						 | 
					6d317603c9 | ||
| 
						 | 
					5a3d2d196c | ||
| 
						 | 
					e740c4d980 | ||
| 
						 | 
					253e4596e2 | ||
| 
						 | 
					70e75a355c | ||
| 
						 | 
					4f885c9a79 | ||
| 
						 | 
					b519d2afac | ||
| 
						 | 
					6b61e3b76b | ||
| 
						 | 
					30b9c72c31 | ||
| 
						 | 
					385bf74f6e | ||
| 
						 | 
					be5615e530 | ||
| 
						 | 
					d81a03c093 | ||
| 
						 | 
					f8249c8267 | ||
| 
						 | 
					5a1cbdcd3b | ||
| 
						 | 
					e0c99d87bd | ||
| 
						 | 
					548250029d | ||
| 
						 | 
					66a354dbdc | ||
| 
						 | 
					834e602686 | ||
| 
						 | 
					1f693ca4f6 | ||
| 
						 | 
					97a0bc6045 | ||
| 
						 | 
					8b75cdfefd | ||
| 
						 | 
					917aecf1ff | ||
| 
						 | 
					663dcd0396 | ||
| 
						 | 
					8f2dffb1ad | ||
| 
						 | 
					20228e3d19 | ||
| 
						 | 
					81c6cc11b3 | ||
| 
						 | 
					2ccacbe5f3 | ||
| 
						 | 
					a5345e8468 | ||
| 
						 | 
					8f5d62bb81 | ||
| 
						 | 
					28f6838560 | ||
| 
						 | 
					c86aacb31c | ||
| 
						 | 
					f62f5192d6 | ||
| 
						 | 
					b14ea1fe3e | ||
| 
						 | 
					552633a00b | ||
| 
						 | 
					7faba2a690 | ||
| 
						 | 
					db910aff06 | ||
| 
						 | 
					72126052ad | ||
| 
						 | 
					75d9f6a7e7 | ||
| 
						 | 
					de677294c6 | ||
| 
						 | 
					da1e6b8259 | ||
| 
						 | 
					a9633b3990 | ||
| 
						 | 
					7ac9af1cc1 | ||
| 
						 | 
					00d8b8cd61 | ||
| 
						 | 
					af7ff7f5cf | ||
| 
						 | 
					2a20719130 | ||
| 
						 | 
					f481940180 | ||
| 
						 | 
					ca8824d1e3 | ||
| 
						 | 
					f4be199b77 | ||
| 
						 | 
					6bcef8334e | ||
| 
						 | 
					3955eff683 | ||
| 
						 | 
					aa0f6ecd75 | ||
| 
						 | 
					ef4a94ed78 | ||
| 
						 | 
					b5c803ce65 | ||
| 
						 | 
					d4325ed82e | ||
| 
						 | 
					3805fb8f26 | ||
| 
						 | 
					d4d938c655 | ||
| 
						 | 
					1c6911e361 | ||
| 
						 | 
					a1b364f337 | ||
| 
						 | 
					ece5c3da86 | ||
| 
						 | 
					5d1ae6047b | ||
| 
						 | 
					5605c72253 | ||
| 
						 | 
					66bbcf0733 | ||
| 
						 | 
					acc23ea7bb | ||
| 
						 | 
					663bd0c9f0 | ||
| 
						 | 
					39b1025dfa | ||
| 
						 | 
					d2875e90b2 | ||
| 
						 | 
					ff461d1d02 | ||
| 
						 | 
					58164ea2d3 | ||
| 
						 | 
					1bf4834004 | ||
| 
						 | 
					bf58d78281 | ||
| 
						 | 
					0dc749bb3d | ||
| 
						 | 
					a8aedfde55 | ||
| 
						 | 
					b174a89032 | ||
| 
						 | 
					9b92d1b673 | ||
| 
						 | 
					febc9aed11 | ||
| 
						 | 
					de2462677e | ||
| 
						 | 
					8bd94d46eb | ||
| 
						 | 
					d43cefe28f | ||
| 
						 | 
					b82874e261 | ||
| 
						 | 
					8554cb5d6c | ||
| 
						 | 
					f901614056 | ||
| 
						 | 
					b555d217ab | ||
| 
						 | 
					775c600234 | ||
| 
						 | 
					128f2570b8 | ||
| 
						 | 
					3cd53e79b4 | ||
| 
						 | 
					ebba84ffda | ||
| 
						 | 
					1e1a42fe98 | ||
| 
						 | 
					8a744a440d | ||
| 
						 | 
					f4fc3c7d55 | ||
| 
						 | 
					0594d121de | ||
| 
						 | 
					12c85d6234 | ||
| 
						 | 
					87d05223af | ||
| 
						 | 
					babf6366e8 | ||
| 
						 | 
					5e37728f66 | ||
| 
						 | 
					e8e19fede7 | ||
| 
						 | 
					e565dbfa66 | ||
| 
						 | 
					d180d6820c | ||
| 
						 | 
					7f252e9b7c | ||
| 
						 | 
					41db8681f8 | ||
| 
						 | 
					26cd58fd6d | ||
| 
						 | 
					63c7e1aa9d | ||
| 
						 | 
					d5a6063e5e | ||
| 
						 | 
					00affdbdec | ||
| 
						 | 
					db3f0bbd4f | ||
| 
						 | 
					020a59cb97 | ||
| 
						 | 
					ff4fa6402d | ||
| 
						 | 
					80f7555499 | ||
| 
						 | 
					10cc187c5d | ||
| 
						 | 
					def4a8a67e | ||
| 
						 | 
					25843edb48 | ||
| 
						 | 
					54294141b0 | ||
| 
						 | 
					f3a8886b50 | ||
| 
						 | 
					268cfaf234 | ||
| 
						 | 
					651ae20304 | ||
| 
						 | 
					e22f69a5dc | ||
| 
						 | 
					a39808f44c | ||
| 
						 | 
					fcb541f734 | ||
| 
						 | 
					79ca0f1684 | ||
| 
						 | 
					f2ebc38044 | ||
| 
						 | 
					d4335675f1 | ||
| 
						 | 
					be4b05423e | ||
| 
						 | 
					d9fe8db2a7 | ||
| 
						 | 
					f92e780765 | ||
| 
						 | 
					7aebdb7c78 | ||
| 
						 | 
					abb2dd842b | ||
| 
						 | 
					75713c8015 | ||
| 
						 | 
					42e1717455 | ||
| 
						 | 
					bfb19a9eb7 | ||
| 
						 | 
					3e08585114 | ||
| 
						 | 
					12e82c7a8d | ||
| 
						 | 
					0fcc683903 | ||
| 
						 | 
					ed7a8dc0f5 | ||
| 
						 | 
					0a9d29c98d | ||
| 
						 | 
					f63e801608 | ||
| 
						 | 
					77f04e1a32 | ||
| 
						 | 
					362819ce16 | ||
| 
						 | 
					1d9165a627 | ||
| 
						 | 
					7ee8aaa027 | ||
| 
						 | 
					516e279fc3 | ||
| 
						 | 
					880611eddb | ||
| 
						 | 
					c4bf776069 | ||
| 
						 | 
					097d6464c0 | ||
| 
						 | 
					b86e4e017f | ||
| 
						 | 
					bbec17d498 | ||
| 
						 | 
					3b7b5f4ec3 | ||
| 
						 | 
					0986efef29 | ||
| 
						 | 
					06091cbf1c | ||
| 
						 | 
					b588bab268 | ||
| 
						 | 
					0736cfe959 | ||
| 
						 | 
					400352254a | ||
| 
						 | 
					259c3dc781 | ||
| 
						 | 
					506055a815 | ||
| 
						 | 
					3edf6c57ba | ||
| 
						 | 
					c404ae7ac8 | ||
| 
						 | 
					312774e472 | ||
| 
						 | 
					c540f802b0 | ||
| 
						 | 
					6a2a2761e1 | ||
| 
						 | 
					2508458c80 | ||
| 
						 | 
					025d9e0141 | ||
| 
						 | 
					734b3b07ab | ||
| 
						 | 
					e4250a857a | ||
| 
						 | 
					56d1b2716c | ||
| 
						 | 
					c5d7e61e6c | ||
| 
						 | 
					6222a127bd | ||
| 
						 | 
					f0b7e515b6 | ||
| 
						 | 
					98d8c23868 | ||
| 
						 | 
					978bb9afd0 | ||
| 
						 | 
					058598b5f3 | ||
| 
						 | 
					5b7ab3a10f | ||
| 
						 | 
					e42243c78b | ||
| 
						 | 
					c650ee8498 | ||
| 
						 | 
					50f8968901 | ||
| 
						 | 
					b0fa2e6d80 | ||
| 
						 | 
					d59589425e | ||
| 
						 | 
					6c810e514b | ||
| 
						 | 
					efa41dbd22 | ||
| 
						 | 
					f34bcfd56d | ||
| 
						 | 
					8ff2e3fb29 | ||
| 
						 | 
					033c04a0f2 | ||
| 
						 | 
					6ae2da22c1 | ||
| 
						 | 
					cef1ab9512 | ||
| 
						 | 
					94f02bfca3 | ||
| 
						 | 
					a941bb1744 | ||
| 
						 | 
					6ff591427a | ||
| 
						 | 
					809e172280 | ||
| 
						 | 
					17aedae0a9 | ||
| 
						 | 
					ef817ccb3a | ||
| 
						 | 
					0fb55b0bee | ||
| 
						 | 
					a1a6eddc31 | ||
| 
						 | 
					ff3d0b6b57 | ||
| 
						 | 
					dd64cef4c4 | ||
| 
						 | 
					9796848079 | ||
| 
						 | 
					fea7eb4312 | ||
| 
						 | 
					c12cd0e755 | ||
| 
						 | 
					d86a72f858 | ||
| 
						 | 
					50cd7f219a | ||
| 
						 | 
					8252b3eccc | ||
| 
						 | 
					d0c6e3a158 | ||
| 
						 | 
					1505fa547e | ||
| 
						 | 
					9017bad884 | ||
| 
						 | 
					2ac5e316a5 | ||
| 
						 | 
					29f9113062 | ||
| 
						 | 
					46349672d8 | ||
| 
						 | 
					4787be2db0 | ||
| 
						 | 
					f0a8c5d732 | ||
| 
						 | 
					9ad520bf7c | ||
| 
						 | 
					bd0cc51554 | ||
| 
						 | 
					12f599f974 | ||
| 
						 | 
					0118d5fb40 | ||
| 
						 | 
					65cadb311a | ||
| 
						 | 
					dd75bd197d | ||
| 
						 | 
					7e155bdb43 | ||
| 
						 | 
					993b6fddf4 | ||
| 
						 | 
					6ba51df6a7 | ||
| 
						 | 
					1185ac58e1 | ||
| 
						 | 
					f835997f49 | ||
| 
						 | 
					a597dba775 | ||
| 
						 | 
					3194e83a66 | ||
| 
						 | 
					096c3cdd34 | ||
| 
						 | 
					3a1ea42333 | ||
| 
						 | 
					64877d4299 | ||
| 
						 | 
					e957dc5e2c | ||
| 
						 | 
					578d5c5830 | ||
| 
						 | 
					96284f9508 | ||
| 
						 | 
					698b38dcba | ||
| 
						 | 
					6db826befe | ||
| 
						 | 
					1a3d412d73 | ||
| 
						 | 
					b8461c9dd8 | ||
| 
						 | 
					699bd9de10 | ||
| 
						 | 
					54b6866e21 | ||
| 
						 | 
					afd155e9c1 | ||
| 
						 | 
					910a717230 | ||
| 
						 | 
					70fbd33d61 | ||
| 
						 | 
					2da0d5ee21 | ||
| 
						 | 
					98f64e057a | ||
| 
						 | 
					3d9d936c56 | ||
| 
						 | 
					2b4cb59df8 | ||
| 
						 | 
					9d80da52e3 | ||
| 
						 | 
					fd176d2c64 | ||
| 
						 | 
					538b6de36b | ||
| 
						 | 
					f7eca8aee0 | ||
| 
						 | 
					a754d94c2c | ||
| 
						 | 
					5e3493e6a9 | ||
| 
						 | 
					619a14c26b | ||
| 
						 | 
					7d9a8decf0 | ||
| 
						 | 
					d11e14ad89 | ||
| 
						 | 
					69189cf2af | ||
| 
						 | 
					6e7d2f19d2 | ||
| 
						 | 
					d99ebf5d6a | ||
| 
						 | 
					ef2d19e95b | ||
| 
						 | 
					e3a66f017e | ||
| 
						 | 
					9e544ad471 | ||
| 
						 | 
					5f19aa527a | ||
| 
						 | 
					bfd5bc5c26 | ||
| 
						 | 
					2d0ec3accd | ||
| 
						 | 
					0999d98225 | ||
| 
						 | 
					d8dd3e133f | ||
| 
						 | 
					528470c37f | ||
| 
						 | 
					c03cd53853 | ||
| 
						 | 
					b57fc8a29c | ||
| 
						 | 
					a04ed5c3ca | ||
| 
						 | 
					3ad1df14f6 | ||
| 
						 | 
					d8caf12fdc | ||
| 
						 | 
					5ca9d30d5f | ||
| 
						 | 
					a7a71b4a46 | ||
| 
						 | 
					638603ac6b | ||
| 
						 | 
					1d70c15027 | ||
| 
						 | 
					7a5f03d672 | ||
| 
						 | 
					653c482ff7 | 
@@ -1,11 +1,11 @@
 | 
			
		||||
# pulls community scripts from git repo
 | 
			
		||||
FROM python:3.10-slim AS GET_SCRIPTS_STAGE
 | 
			
		||||
FROM python:3.11.8-slim AS GET_SCRIPTS_STAGE
 | 
			
		||||
 | 
			
		||||
RUN apt-get update && \
 | 
			
		||||
    apt-get install -y --no-install-recommends git && \
 | 
			
		||||
    git clone https://github.com/amidaware/community-scripts.git /community-scripts
 | 
			
		||||
 | 
			
		||||
FROM python:3.10-slim
 | 
			
		||||
FROM python:3.11.8-slim
 | 
			
		||||
 | 
			
		||||
ENV TACTICAL_DIR /opt/tactical
 | 
			
		||||
ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready
 | 
			
		||||
@@ -18,7 +18,7 @@ ENV PYTHONUNBUFFERED=1
 | 
			
		||||
EXPOSE 8000 8383 8005
 | 
			
		||||
 | 
			
		||||
RUN apt-get update && \
 | 
			
		||||
    apt-get install -y build-essential
 | 
			
		||||
    apt-get install -y build-essential weasyprint
 | 
			
		||||
 | 
			
		||||
RUN groupadd -g 1000 tactical && \
 | 
			
		||||
    useradd -u 1000 -g 1000 tactical
 | 
			
		||||
@@ -27,7 +27,7 @@ RUN groupadd -g 1000 tactical && \
 | 
			
		||||
COPY --from=GET_SCRIPTS_STAGE /community-scripts /community-scripts
 | 
			
		||||
 | 
			
		||||
# Copy dev python reqs
 | 
			
		||||
COPY .devcontainer/requirements.txt  /
 | 
			
		||||
COPY .devcontainer/requirements.txt /
 | 
			
		||||
 | 
			
		||||
# Copy docker entrypoint.sh
 | 
			
		||||
COPY .devcontainer/entrypoint.sh /
 | 
			
		||||
 
 | 
			
		||||
@@ -22,21 +22,6 @@ services:
 | 
			
		||||
        aliases:
 | 
			
		||||
          - tactical-backend
 | 
			
		||||
 | 
			
		||||
  app-dev:
 | 
			
		||||
    container_name: trmm-app-dev
 | 
			
		||||
    image: node:16-alpine
 | 
			
		||||
    restart: always
 | 
			
		||||
    command: /bin/sh -c "npm install npm@latest -g && npm install && npm run serve -- --host 0.0.0.0 --port ${APP_PORT}"
 | 
			
		||||
    working_dir: /workspace/web
 | 
			
		||||
    volumes:
 | 
			
		||||
      - ..:/workspace:cached
 | 
			
		||||
    ports:
 | 
			
		||||
      - "8080:${APP_PORT}"
 | 
			
		||||
    networks:
 | 
			
		||||
      dev:
 | 
			
		||||
        aliases:
 | 
			
		||||
          - tactical-frontend
 | 
			
		||||
 | 
			
		||||
  # nats
 | 
			
		||||
  nats-dev:
 | 
			
		||||
    container_name: trmm-nats-dev
 | 
			
		||||
@@ -231,6 +216,7 @@ services:
 | 
			
		||||
      - "443:4443"
 | 
			
		||||
    volumes:
 | 
			
		||||
      - tactical-data-dev:/opt/tactical
 | 
			
		||||
      - ..:/workspace:cached
 | 
			
		||||
 | 
			
		||||
volumes:
 | 
			
		||||
  tactical-data-dev: null
 | 
			
		||||
 
 | 
			
		||||
@@ -15,10 +15,7 @@ set -e
 | 
			
		||||
: "${MESH_PASS:=meshcentralpass}"
 | 
			
		||||
: "${MESH_HOST:=tactical-meshcentral}"
 | 
			
		||||
: "${API_HOST:=tactical-backend}"
 | 
			
		||||
: "${APP_HOST:=tactical-frontend}"
 | 
			
		||||
: "${REDIS_HOST:=tactical-redis}"
 | 
			
		||||
: "${HTTP_PROTOCOL:=http}"
 | 
			
		||||
: "${APP_PORT:=8080}"
 | 
			
		||||
: "${API_PORT:=8000}"
 | 
			
		||||
 | 
			
		||||
: "${CERT_PRIV_PATH:=${TACTICAL_DIR}/certs/privkey.pem}"
 | 
			
		||||
@@ -81,6 +78,17 @@ DATABASES = {
 | 
			
		||||
        'PASSWORD': '${POSTGRES_PASS}',
 | 
			
		||||
        'HOST': '${POSTGRES_HOST}',
 | 
			
		||||
        'PORT': '${POSTGRES_PORT}',
 | 
			
		||||
    },
 | 
			
		||||
    'reporting': {
 | 
			
		||||
        'ENGINE': 'django.db.backends.postgresql',
 | 
			
		||||
        'NAME': '${POSTGRES_DB}',
 | 
			
		||||
        'USER': 'reporting_user',
 | 
			
		||||
        'PASSWORD': 'read_password',
 | 
			
		||||
        'HOST': '${POSTGRES_HOST}',
 | 
			
		||||
        'PORT': '${POSTGRES_PORT}',
 | 
			
		||||
        'OPTIONS': {
 | 
			
		||||
            'options': '-c default_transaction_read_only=on'
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -90,6 +98,7 @@ MESH_TOKEN_KEY = '${MESH_TOKEN}'
 | 
			
		||||
REDIS_HOST    = '${REDIS_HOST}'
 | 
			
		||||
MESH_WS_URL = '${MESH_WS_URL}'
 | 
			
		||||
ADMIN_ENABLED = True
 | 
			
		||||
TRMM_INSECURE = True
 | 
			
		||||
EOF
 | 
			
		||||
)"
 | 
			
		||||
 | 
			
		||||
@@ -98,6 +107,7 @@ EOF
 | 
			
		||||
  # run migrations and init scripts
 | 
			
		||||
  "${VIRTUAL_ENV}"/bin/python manage.py pre_update_tasks
 | 
			
		||||
  "${VIRTUAL_ENV}"/bin/python manage.py migrate --no-input
 | 
			
		||||
  "${VIRTUAL_ENV}"/bin/python manage.py generate_json_schemas
 | 
			
		||||
  "${VIRTUAL_ENV}"/bin/python manage.py collectstatic --no-input
 | 
			
		||||
  "${VIRTUAL_ENV}"/bin/python manage.py initial_db_setup
 | 
			
		||||
  "${VIRTUAL_ENV}"/bin/python manage.py initial_mesh_setup
 | 
			
		||||
@@ -123,6 +133,8 @@ if [ "$1" = 'tactical-init-dev' ]; then
 | 
			
		||||
  mkdir -p /meshcentral-data
 | 
			
		||||
  mkdir -p ${TACTICAL_DIR}/tmp
 | 
			
		||||
  mkdir -p ${TACTICAL_DIR}/certs
 | 
			
		||||
  mkdir -p ${TACTICAL_DIR}/reporting
 | 
			
		||||
  mkdir -p ${TACTICAL_DIR}/reporting/assets
 | 
			
		||||
  mkdir -p /mongo/data/db
 | 
			
		||||
  mkdir -p /redis/data
 | 
			
		||||
  touch /meshcentral-data/.initialized && chown -R 1000:1000 /meshcentral-data
 | 
			
		||||
@@ -130,6 +142,7 @@ if [ "$1" = 'tactical-init-dev' ]; then
 | 
			
		||||
  touch ${TACTICAL_DIR}/certs/.initialized && chown -R 1000:1000 ${TACTICAL_DIR}/certs
 | 
			
		||||
  touch /mongo/data/db/.initialized && chown -R 1000:1000 /mongo/data/db
 | 
			
		||||
  touch /redis/data/.initialized && chown -R 1000:1000 /redis/data
 | 
			
		||||
  touch ${TACTICAL_DIR}/reporting && chown -R 1000:1000 ${TACTICAL_DIR}/reporting
 | 
			
		||||
  mkdir -p ${TACTICAL_DIR}/api/tacticalrmm/private/exe
 | 
			
		||||
  mkdir -p ${TACTICAL_DIR}/api/tacticalrmm/private/log
 | 
			
		||||
  touch ${TACTICAL_DIR}/api/tacticalrmm/private/log/django_debug.log
 | 
			
		||||
@@ -142,16 +155,6 @@ if [ "$1" = 'tactical-init-dev' ]; then
 | 
			
		||||
 | 
			
		||||
  django_setup
 | 
			
		||||
 | 
			
		||||
  # create .env file for frontend
 | 
			
		||||
  webenv="$(cat << EOF
 | 
			
		||||
PROD_URL = "${HTTP_PROTOCOL}://${API_HOST}"
 | 
			
		||||
DEV_URL = "${HTTP_PROTOCOL}://${API_HOST}"
 | 
			
		||||
APP_URL = "https://${APP_HOST}"
 | 
			
		||||
DOCKER_BUILD = 1
 | 
			
		||||
EOF
 | 
			
		||||
)"
 | 
			
		||||
  echo "${webenv}" | tee "${WORKSPACE_DIR}"/web/.env > /dev/null
 | 
			
		||||
 | 
			
		||||
  # chown everything to tactical user
 | 
			
		||||
  chown -R "${TACTICAL_USER}":"${TACTICAL_USER}" "${WORKSPACE_DIR}"
 | 
			
		||||
  chown -R "${TACTICAL_USER}":"${TACTICAL_USER}" "${TACTICAL_DIR}"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,41 +1,3 @@
 | 
			
		||||
# To ensure app dependencies are ported from your virtual environment/host machine into your container, run 'pip freeze > requirements.txt' in the terminal to overwrite this file
 | 
			
		||||
asgiref==3.5.0
 | 
			
		||||
celery==5.2.6
 | 
			
		||||
channels==3.0.4
 | 
			
		||||
channels_redis==3.4.0
 | 
			
		||||
daphne==3.0.2
 | 
			
		||||
Django==4.0.4
 | 
			
		||||
django-cors-headers==3.11.0
 | 
			
		||||
django-ipware==4.0.2
 | 
			
		||||
django-rest-knox==4.2.0
 | 
			
		||||
djangorestframework==3.13.1
 | 
			
		||||
future==0.18.2
 | 
			
		||||
msgpack==1.0.3
 | 
			
		||||
nats-py==2.1.0
 | 
			
		||||
packaging==21.3
 | 
			
		||||
psycopg2-binary==2.9.3
 | 
			
		||||
pycryptodome==3.14.1
 | 
			
		||||
pyotp==2.6.0
 | 
			
		||||
pytz==2022.1
 | 
			
		||||
qrcode==7.3.1
 | 
			
		||||
redis==4.2.2
 | 
			
		||||
requests==2.27.1
 | 
			
		||||
twilio==7.8.1
 | 
			
		||||
urllib3==1.26.9
 | 
			
		||||
validators==0.18.2
 | 
			
		||||
websockets==10.2
 | 
			
		||||
drf_spectacular==0.22.0
 | 
			
		||||
meshctrl==0.1.15
 | 
			
		||||
hiredis==2.0.0
 | 
			
		||||
 | 
			
		||||
# dev
 | 
			
		||||
black==22.3.0
 | 
			
		||||
django-extensions==3.1.5
 | 
			
		||||
isort==5.10.1
 | 
			
		||||
mypy==0.942
 | 
			
		||||
types-pytz==2021.3.6
 | 
			
		||||
model-bakery==1.5.0
 | 
			
		||||
coverage==6.3.2
 | 
			
		||||
django-silk==4.3.0
 | 
			
		||||
django-stubs==1.10.1
 | 
			
		||||
djangorestframework-stubs==1.5.0
 | 
			
		||||
-r /workspace/api/tacticalrmm/requirements.txt
 | 
			
		||||
-r /workspace/api/tacticalrmm/requirements-dev.txt
 | 
			
		||||
-r /workspace/api/tacticalrmm/requirements-test.txt
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								.github/FUNDING.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/FUNDING.yml
									
									
									
									
										vendored
									
									
								
							@@ -1,9 +1,9 @@
 | 
			
		||||
# These are supported funding model platforms
 | 
			
		||||
 | 
			
		||||
github: wh1te909
 | 
			
		||||
github: amidaware
 | 
			
		||||
patreon: # Replace with a single Patreon username
 | 
			
		||||
open_collective: # Replace with a single Open Collective username
 | 
			
		||||
ko_fi: tacticalrmm
 | 
			
		||||
ko_fi: # tacticalrmm
 | 
			
		||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
 | 
			
		||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
 | 
			
		||||
liberapay: # Replace with a single Liberapay username
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										75
									
								
								.github/workflows/ci-tests.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										75
									
								
								.github/workflows/ci-tests.yml
									
									
									
									
										vendored
									
									
								
							@@ -10,23 +10,37 @@ on:
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  test:
 | 
			
		||||
    runs-on: self-hosted
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    name: Tests
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
        python-version: ["3.11.8"]
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v2
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
 | 
			
		||||
      - name: Setup virtual env and install requirements
 | 
			
		||||
      - uses: harmon758/postgresql-action@v1
 | 
			
		||||
        with:
 | 
			
		||||
          postgresql version: "15"
 | 
			
		||||
          postgresql db: "pipeline"
 | 
			
		||||
          postgresql user: "pipeline"
 | 
			
		||||
          postgresql password: "pipeline123456"
 | 
			
		||||
 | 
			
		||||
      - name: Setup Python ${{ matrix.python-version }}
 | 
			
		||||
        uses: actions/setup-python@v4
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: ${{ matrix.python-version }}
 | 
			
		||||
          check-latest: true
 | 
			
		||||
 | 
			
		||||
      - name: Install redis
 | 
			
		||||
        run: |
 | 
			
		||||
          sudo apt update
 | 
			
		||||
          sudo apt install -y redis
 | 
			
		||||
          redis-server --version
 | 
			
		||||
 | 
			
		||||
      - name: Install requirements
 | 
			
		||||
        working-directory: api/tacticalrmm
 | 
			
		||||
        run: |
 | 
			
		||||
          sudo -u postgres psql -c 'DROP DATABASE IF EXISTS pipeline'
 | 
			
		||||
          sudo -u postgres psql -c 'DROP DATABASE IF EXISTS test_pipeline'
 | 
			
		||||
          sudo -u postgres psql -c 'CREATE DATABASE pipeline'
 | 
			
		||||
          sudo -u postgres psql -c "SET client_encoding = 'UTF8'" pipeline
 | 
			
		||||
          pwd
 | 
			
		||||
          rm -rf /actions-runner/_work/trmm-actions/trmm-actions/api/env
 | 
			
		||||
          cd api
 | 
			
		||||
          python3.10 -m venv env
 | 
			
		||||
          source env/bin/activate
 | 
			
		||||
          cd tacticalrmm
 | 
			
		||||
          python --version
 | 
			
		||||
          SETTINGS_FILE="tacticalrmm/settings.py"
 | 
			
		||||
          SETUPTOOLS_VER=$(grep "^SETUPTOOLS_VER" "$SETTINGS_FILE" | awk -F'[= "]' '{print $5}')
 | 
			
		||||
@@ -35,28 +49,33 @@ jobs:
 | 
			
		||||
          pip install setuptools==${SETUPTOOLS_VER} wheel==${WHEEL_VER}
 | 
			
		||||
          pip install -r requirements.txt -r requirements-test.txt
 | 
			
		||||
 | 
			
		||||
      - name: Run django tests
 | 
			
		||||
        env:
 | 
			
		||||
          GHACTIONS: "yes"
 | 
			
		||||
        run: |
 | 
			
		||||
          cd api/tacticalrmm
 | 
			
		||||
          source ../env/bin/activate
 | 
			
		||||
          rm -f .coverage coverage.lcov
 | 
			
		||||
          pytest
 | 
			
		||||
          if [ $? -ne 0 ]; then
 | 
			
		||||
              exit 1
 | 
			
		||||
          fi
 | 
			
		||||
 | 
			
		||||
      - name: Codestyle black
 | 
			
		||||
        working-directory: api
 | 
			
		||||
        run: |
 | 
			
		||||
          cd api
 | 
			
		||||
          source env/bin/activate
 | 
			
		||||
          black --exclude migrations/ --check tacticalrmm
 | 
			
		||||
          if [ $? -ne 0 ]; then
 | 
			
		||||
              exit 1
 | 
			
		||||
          fi
 | 
			
		||||
 | 
			
		||||
      - uses: codecov/codecov-action@v2
 | 
			
		||||
      - name: Lint with flake8
 | 
			
		||||
        working-directory: api/tacticalrmm
 | 
			
		||||
        run: |
 | 
			
		||||
          flake8 --config .flake8 .
 | 
			
		||||
          if [ $? -ne 0 ]; then
 | 
			
		||||
              exit 1
 | 
			
		||||
          fi
 | 
			
		||||
 | 
			
		||||
      - name: Run django tests
 | 
			
		||||
        env:
 | 
			
		||||
          GHACTIONS: "yes"
 | 
			
		||||
        working-directory: api/tacticalrmm
 | 
			
		||||
        run: |
 | 
			
		||||
          pytest
 | 
			
		||||
          if [ $? -ne 0 ]; then
 | 
			
		||||
              exit 1
 | 
			
		||||
          fi
 | 
			
		||||
 | 
			
		||||
      - uses: codecov/codecov-action@v3
 | 
			
		||||
        with:
 | 
			
		||||
          directory: ./api/tacticalrmm
 | 
			
		||||
          files: ./api/tacticalrmm/coverage.xml
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										70
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										70
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							@@ -1,70 +0,0 @@
 | 
			
		||||
# For most projects, this workflow file will not need changing; you simply need
 | 
			
		||||
# to commit it to your repository.
 | 
			
		||||
#
 | 
			
		||||
# You may wish to alter this file to override the set of languages analyzed,
 | 
			
		||||
# or to provide custom queries or build logic.
 | 
			
		||||
#
 | 
			
		||||
# ******** NOTE ********
 | 
			
		||||
# We have attempted to detect the languages in your repository. Please check
 | 
			
		||||
# the `language` matrix defined below to confirm you have the correct set of
 | 
			
		||||
# supported CodeQL languages.
 | 
			
		||||
#
 | 
			
		||||
name: "CodeQL"
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches: [ develop ]
 | 
			
		||||
  pull_request:
 | 
			
		||||
    # The branches below must be a subset of the branches above
 | 
			
		||||
    branches: [ develop ]
 | 
			
		||||
  schedule:
 | 
			
		||||
    - cron: '19 14 * * 6'
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  analyze:
 | 
			
		||||
    name: Analyze
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    permissions:
 | 
			
		||||
      actions: read
 | 
			
		||||
      contents: read
 | 
			
		||||
      security-events: write
 | 
			
		||||
 | 
			
		||||
    strategy:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
      matrix:
 | 
			
		||||
        language: [ 'go', 'javascript', 'python' ]
 | 
			
		||||
        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
 | 
			
		||||
        # Learn more about CodeQL language support at https://git.io/codeql-language-support
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
    - name: Checkout repository
 | 
			
		||||
      uses: actions/checkout@v2
 | 
			
		||||
 | 
			
		||||
    # Initializes the CodeQL tools for scanning.
 | 
			
		||||
    - name: Initialize CodeQL
 | 
			
		||||
      uses: github/codeql-action/init@v1
 | 
			
		||||
      with:
 | 
			
		||||
        languages: ${{ matrix.language }}
 | 
			
		||||
        # If you wish to specify custom queries, you can do so here or in a config file.
 | 
			
		||||
        # By default, queries listed here will override any specified in a config file.
 | 
			
		||||
        # Prefix the list here with "+" to use these queries and those in the config file.
 | 
			
		||||
        # queries: ./path/to/local/query, your-org/your-repo/queries@main
 | 
			
		||||
 | 
			
		||||
    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
 | 
			
		||||
    # If this step fails, then you should remove it and run the build manually (see below)
 | 
			
		||||
    - name: Autobuild
 | 
			
		||||
      uses: github/codeql-action/autobuild@v1
 | 
			
		||||
 | 
			
		||||
    # ℹ️ Command-line programs to run using the OS shell.
 | 
			
		||||
    # 📚 https://git.io/JvXDl
 | 
			
		||||
 | 
			
		||||
    # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
 | 
			
		||||
    #    and modify them (or add more) to build your code if your project
 | 
			
		||||
    #    uses a compiled language
 | 
			
		||||
 | 
			
		||||
    #- run: |
 | 
			
		||||
    #   make bootstrap
 | 
			
		||||
    #   make release
 | 
			
		||||
 | 
			
		||||
    - name: Perform CodeQL Analysis
 | 
			
		||||
      uses: github/codeql-action/analyze@v1
 | 
			
		||||
							
								
								
									
										34
									
								
								.github/workflows/devskim-analysis.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										34
									
								
								.github/workflows/devskim-analysis.yml
									
									
									
									
										vendored
									
									
								
							@@ -1,34 +0,0 @@
 | 
			
		||||
# This workflow uses actions that are not certified by GitHub.
 | 
			
		||||
# They are provided by a third-party and are governed by
 | 
			
		||||
# separate terms of service, privacy policy, and support
 | 
			
		||||
# documentation.
 | 
			
		||||
 | 
			
		||||
name: DevSkim
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches: [ develop ]
 | 
			
		||||
  pull_request:
 | 
			
		||||
    branches: [ develop ]
 | 
			
		||||
  schedule:
 | 
			
		||||
    - cron: '19 5 * * 0'
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  lint:
 | 
			
		||||
    name: DevSkim
 | 
			
		||||
    runs-on: ubuntu-20.04
 | 
			
		||||
    permissions:
 | 
			
		||||
      actions: read
 | 
			
		||||
      contents: read
 | 
			
		||||
      security-events: write
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout code
 | 
			
		||||
        uses: actions/checkout@v2
 | 
			
		||||
 | 
			
		||||
      - name: Run DevSkim scanner
 | 
			
		||||
        uses: microsoft/DevSkim-Action@v1
 | 
			
		||||
        
 | 
			
		||||
      - name: Upload DevSkim scan results to GitHub Security tab
 | 
			
		||||
        uses: github/codeql-action/upload-sarif@v1
 | 
			
		||||
        with:
 | 
			
		||||
          sarif_file: devskim-results.sarif
 | 
			
		||||
							
								
								
									
										20
									
								
								.github/workflows/docker-build-push.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										20
									
								
								.github/workflows/docker-build-push.yml
									
									
									
									
										vendored
									
									
								
							@@ -9,24 +9,24 @@ jobs:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out the repo
 | 
			
		||||
        uses: actions/checkout@v2
 | 
			
		||||
        
 | 
			
		||||
        uses: actions/checkout@v4
 | 
			
		||||
 | 
			
		||||
      - name: Get Github Tag
 | 
			
		||||
        id: prep
 | 
			
		||||
        run: |
 | 
			
		||||
          echo ::set-output name=version::${GITHUB_REF#refs/tags/v}
 | 
			
		||||
      - name: Set up QEMU
 | 
			
		||||
        uses: docker/setup-qemu-action@v1
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
      - name: Set up Docker Buildx
 | 
			
		||||
        uses: docker/setup-buildx-action@v1
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
      - name: Login to DockerHub
 | 
			
		||||
        uses: docker/login-action@v1 
 | 
			
		||||
        uses: docker/login-action@v1
 | 
			
		||||
        with:
 | 
			
		||||
          username: ${{ secrets.DOCKERHUB_USERNAME }}
 | 
			
		||||
          password: ${{ secrets.DOCKERHUB_TOKEN }}
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
      - name: Build and Push Tactical Image
 | 
			
		||||
        uses: docker/build-push-action@v2
 | 
			
		||||
        with:
 | 
			
		||||
@@ -36,7 +36,7 @@ jobs:
 | 
			
		||||
          file: ./docker/containers/tactical/dockerfile
 | 
			
		||||
          platforms: linux/amd64
 | 
			
		||||
          tags: tacticalrmm/tactical:${{ steps.prep.outputs.version }},tacticalrmm/tactical:latest
 | 
			
		||||
          
 | 
			
		||||
 | 
			
		||||
      - name: Build and Push Tactical MeshCentral Image
 | 
			
		||||
        uses: docker/build-push-action@v2
 | 
			
		||||
        with:
 | 
			
		||||
@@ -46,7 +46,7 @@ jobs:
 | 
			
		||||
          file: ./docker/containers/tactical-meshcentral/dockerfile
 | 
			
		||||
          platforms: linux/amd64
 | 
			
		||||
          tags: tacticalrmm/tactical-meshcentral:${{ steps.prep.outputs.version }},tacticalrmm/tactical-meshcentral:latest
 | 
			
		||||
          
 | 
			
		||||
 | 
			
		||||
      - name: Build and Push Tactical NATS Image
 | 
			
		||||
        uses: docker/build-push-action@v2
 | 
			
		||||
        with:
 | 
			
		||||
@@ -56,7 +56,7 @@ jobs:
 | 
			
		||||
          file: ./docker/containers/tactical-nats/dockerfile
 | 
			
		||||
          platforms: linux/amd64
 | 
			
		||||
          tags: tacticalrmm/tactical-nats:${{ steps.prep.outputs.version }},tacticalrmm/tactical-nats:latest
 | 
			
		||||
          
 | 
			
		||||
 | 
			
		||||
      - name: Build and Push Tactical Frontend Image
 | 
			
		||||
        uses: docker/build-push-action@v2
 | 
			
		||||
        with:
 | 
			
		||||
@@ -66,7 +66,7 @@ jobs:
 | 
			
		||||
          file: ./docker/containers/tactical-frontend/dockerfile
 | 
			
		||||
          platforms: linux/amd64
 | 
			
		||||
          tags: tacticalrmm/tactical-frontend:${{ steps.prep.outputs.version }},tacticalrmm/tactical-frontend:latest
 | 
			
		||||
          
 | 
			
		||||
 | 
			
		||||
      - name: Build and Push Tactical Nginx Image
 | 
			
		||||
        uses: docker/build-push-action@v2
 | 
			
		||||
        with:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -55,3 +55,7 @@ coverage.lcov
 | 
			
		||||
daphne.sock.lock
 | 
			
		||||
.pytest_cache
 | 
			
		||||
coverage.xml
 | 
			
		||||
setup_dev.yml
 | 
			
		||||
11env/
 | 
			
		||||
query_schema.json
 | 
			
		||||
gunicorn_config.py
 | 
			
		||||
							
								
								
									
										23
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
{
 | 
			
		||||
  "recommendations": [
 | 
			
		||||
    // frontend
 | 
			
		||||
    "dbaeumer.vscode-eslint",
 | 
			
		||||
    "esbenp.prettier-vscode",
 | 
			
		||||
    "editorconfig.editorconfig",
 | 
			
		||||
    "vue.volar",
 | 
			
		||||
    "wayou.vscode-todo-highlight",
 | 
			
		||||
 | 
			
		||||
    // python
 | 
			
		||||
    "matangover.mypy",
 | 
			
		||||
    "ms-python.python",
 | 
			
		||||
 | 
			
		||||
    // golang
 | 
			
		||||
    "golang.go"
 | 
			
		||||
  ],
 | 
			
		||||
  "unwantedRecommendations": [
 | 
			
		||||
    "octref.vetur",
 | 
			
		||||
    "hookyqr.beautify",
 | 
			
		||||
    "dbaeumer.jshint",
 | 
			
		||||
    "ms-vscode.vscode-typescript-tslint-plugin"
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										140
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										140
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							@@ -1,86 +1,58 @@
 | 
			
		||||
{
 | 
			
		||||
    "python.defaultInterpreterPath": "api/tacticalrmm/env/bin/python",
 | 
			
		||||
    "python.languageServer": "Pylance",
 | 
			
		||||
    "python.analysis.extraPaths": [
 | 
			
		||||
        "api/tacticalrmm",
 | 
			
		||||
        "api/env",
 | 
			
		||||
    ],
 | 
			
		||||
    "python.analysis.diagnosticSeverityOverrides": {
 | 
			
		||||
        "reportUnusedImport": "error",
 | 
			
		||||
        "reportDuplicateImport": "error",
 | 
			
		||||
        "reportGeneralTypeIssues": "none"
 | 
			
		||||
  "python.defaultInterpreterPath": "api/env/bin/python",
 | 
			
		||||
  "python.languageServer": "Pylance",
 | 
			
		||||
  "python.analysis.extraPaths": ["api/tacticalrmm", "api/env"],
 | 
			
		||||
  "python.analysis.diagnosticSeverityOverrides": {
 | 
			
		||||
    "reportUnusedImport": "error",
 | 
			
		||||
    "reportDuplicateImport": "error",
 | 
			
		||||
    "reportGeneralTypeIssues": "none"
 | 
			
		||||
  },
 | 
			
		||||
  "python.analysis.typeCheckingMode": "basic",
 | 
			
		||||
  "editor.bracketPairColorization.enabled": true,
 | 
			
		||||
  "editor.guides.bracketPairs": true,
 | 
			
		||||
  "editor.formatOnSave": true,
 | 
			
		||||
  "files.associations": {
 | 
			
		||||
    "**/ansible/**/*.yml": "ansible",
 | 
			
		||||
    "**/docker/**/docker-compose*.yml": "dockercompose"
 | 
			
		||||
  },
 | 
			
		||||
  "files.watcherExclude": {
 | 
			
		||||
      "**/.git/objects/**": true,
 | 
			
		||||
      "**/.git/subtree-cache/**": true,
 | 
			
		||||
      "**/node_modules/": true,
 | 
			
		||||
      "/node_modules/**": true,
 | 
			
		||||
      "**/env/": true,
 | 
			
		||||
      "/env/**": true,
 | 
			
		||||
      "**/__pycache__": true,
 | 
			
		||||
      "/__pycache__/**": true,
 | 
			
		||||
      "**/.cache": true,
 | 
			
		||||
      "**/.eggs": true,
 | 
			
		||||
      "**/.ipynb_checkpoints": true,
 | 
			
		||||
      "**/.mypy_cache": true,
 | 
			
		||||
      "**/.pytest_cache": true,
 | 
			
		||||
      "**/*.egg-info": true,
 | 
			
		||||
      "**/*.feather": true,
 | 
			
		||||
      "**/*.parquet*": true,
 | 
			
		||||
      "**/*.pyc": true,
 | 
			
		||||
      "**/*.zip": true
 | 
			
		||||
  },
 | 
			
		||||
  "go.useLanguageServer": true,
 | 
			
		||||
  "[go]": {
 | 
			
		||||
    "editor.codeActionsOnSave": {
 | 
			
		||||
      "source.organizeImports": "never"
 | 
			
		||||
    },
 | 
			
		||||
    "python.analysis.typeCheckingMode": "basic",
 | 
			
		||||
    "mypy.runUsingActiveInterpreter": true,
 | 
			
		||||
    "python.linting.enabled": true,
 | 
			
		||||
    "python.linting.mypyEnabled": true,
 | 
			
		||||
    "python.linting.mypyArgs": [
 | 
			
		||||
        "--ignore-missing-imports",
 | 
			
		||||
        "--follow-imports=silent",
 | 
			
		||||
        "--show-column-numbers",
 | 
			
		||||
        "--strict"
 | 
			
		||||
    ],
 | 
			
		||||
    "python.formatting.provider": "black",
 | 
			
		||||
    "editor.formatOnSave": true,
 | 
			
		||||
    "vetur.format.defaultFormatter.js": "prettier",
 | 
			
		||||
    "vetur.format.defaultFormatterOptions": {
 | 
			
		||||
        "prettier": {
 | 
			
		||||
            "semi": true,
 | 
			
		||||
            "printWidth": 120,
 | 
			
		||||
            "tabWidth": 2,
 | 
			
		||||
            "useTabs": false,
 | 
			
		||||
            "arrowParens": "avoid",
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    "vetur.format.options.tabSize": 2,
 | 
			
		||||
    "vetur.format.options.useTabs": false,
 | 
			
		||||
    "files.watcherExclude": {
 | 
			
		||||
        "files.watcherExclude": {
 | 
			
		||||
            "**/.git/objects/**": true,
 | 
			
		||||
            "**/.git/subtree-cache/**": true,
 | 
			
		||||
            "**/node_modules/": true,
 | 
			
		||||
            "/node_modules/**": true,
 | 
			
		||||
            "**/env/": true,
 | 
			
		||||
            "/env/**": true,
 | 
			
		||||
            "**/__pycache__": true,
 | 
			
		||||
            "/__pycache__/**": true,
 | 
			
		||||
            "**/.cache": true,
 | 
			
		||||
            "**/.eggs": true,
 | 
			
		||||
            "**/.ipynb_checkpoints": true,
 | 
			
		||||
            "**/.mypy_cache": true,
 | 
			
		||||
            "**/.pytest_cache": true,
 | 
			
		||||
            "**/*.egg-info": true,
 | 
			
		||||
            "**/*.feather": true,
 | 
			
		||||
            "**/*.parquet*": true,
 | 
			
		||||
            "**/*.pyc": true,
 | 
			
		||||
            "**/*.zip": true
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    "go.useLanguageServer": true,
 | 
			
		||||
    "[go]": {
 | 
			
		||||
        "editor.formatOnSave": true,
 | 
			
		||||
        "editor.codeActionsOnSave": {
 | 
			
		||||
            "source.organizeImports": false,
 | 
			
		||||
        },
 | 
			
		||||
        "editor.snippetSuggestions": "none",
 | 
			
		||||
    },
 | 
			
		||||
    "[go.mod]": {
 | 
			
		||||
        "editor.formatOnSave": true,
 | 
			
		||||
        "editor.codeActionsOnSave": {
 | 
			
		||||
            "source.organizeImports": true,
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    "gopls": {
 | 
			
		||||
        "usePlaceholders": true,
 | 
			
		||||
        "completeUnimported": true,
 | 
			
		||||
        "staticcheck": true,
 | 
			
		||||
    },
 | 
			
		||||
    "mypy.targets": [
 | 
			
		||||
        "api/tacticalrmm"
 | 
			
		||||
    ],
 | 
			
		||||
    "python.linting.ignorePatterns": [
 | 
			
		||||
        "**/site-packages/**/*.py",
 | 
			
		||||
        ".vscode/*.py",
 | 
			
		||||
        "**env/**"
 | 
			
		||||
    ]
 | 
			
		||||
}
 | 
			
		||||
    "editor.snippetSuggestions": "none"
 | 
			
		||||
  },
 | 
			
		||||
  "[go.mod]": {
 | 
			
		||||
    "editor.codeActionsOnSave": {
 | 
			
		||||
      "source.organizeImports": "explicit"
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "gopls": {
 | 
			
		||||
    "usePlaceholders": true,
 | 
			
		||||
    "completeUnimported": true,
 | 
			
		||||
    "staticcheck": true
 | 
			
		||||
  },
 | 
			
		||||
  "[python]": {
 | 
			
		||||
    "editor.defaultFormatter": "ms-python.black-formatter"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@
 | 
			
		||||
Tactical RMM is a remote monitoring & management tool, built with Django and Vue.\
 | 
			
		||||
It uses an [agent](https://github.com/amidaware/rmmagent) written in golang and integrates with [MeshCentral](https://github.com/Ylianst/MeshCentral)
 | 
			
		||||
 | 
			
		||||
# [LIVE DEMO](https://rmm.tacticalrmm.io/)
 | 
			
		||||
# [LIVE DEMO](https://demo.tacticalrmm.com/)
 | 
			
		||||
Demo database resets every hour. A lot of features are disabled for obvious reasons due to the nature of this app.
 | 
			
		||||
 | 
			
		||||
### [Discord Chat](https://discord.gg/upGTkWp)
 | 
			
		||||
@@ -19,7 +19,7 @@ Demo database resets every hour. A lot of features are disabled for obvious reas
 | 
			
		||||
- Teamviewer-like remote desktop control
 | 
			
		||||
- Real-time remote shell
 | 
			
		||||
- Remote file browser (download and upload files)
 | 
			
		||||
- Remote command and script execution (batch, powershell and python scripts)
 | 
			
		||||
- Remote command and script execution (batch, powershell, python, nushell and deno scripts)
 | 
			
		||||
- Event log viewer
 | 
			
		||||
- Services management
 | 
			
		||||
- Windows patch management
 | 
			
		||||
@@ -33,7 +33,10 @@ Demo database resets every hour. A lot of features are disabled for obvious reas
 | 
			
		||||
- Windows 7, 8.1, 10, 11, Server 2008R2, 2012R2, 2016, 2019, 2022
 | 
			
		||||
 | 
			
		||||
## Linux agent versions supported
 | 
			
		||||
- Any distro with systemd
 | 
			
		||||
- Any distro with systemd which includes but is not limited to: Debian (10, 11), Ubuntu x86_64 (18.04, 20.04, 22.04), Synology 7, centos, freepbx and more!
 | 
			
		||||
 | 
			
		||||
## Mac agent versions supported
 | 
			
		||||
- 64 bit Intel and Apple Silicon (M1, M2)
 | 
			
		||||
 | 
			
		||||
## Installation / Backup / Restore / Usage
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,10 +2,7 @@
 | 
			
		||||
 | 
			
		||||
## Supported Versions
 | 
			
		||||
 | 
			
		||||
| Version | Supported          |
 | 
			
		||||
| ------- | ------------------ |
 | 
			
		||||
| 0.12.2   | :white_check_mark: |
 | 
			
		||||
| < 0.12.2 | :x:                |
 | 
			
		||||
[Latest](https://github.com/amidaware/tacticalrmm/releases/latest) release
 | 
			
		||||
 | 
			
		||||
## Reporting a Vulnerability
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3
									
								
								ansible/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								ansible/README.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
### tacticalrmm ansible WIP
 | 
			
		||||
 | 
			
		||||
ansible role to setup a Debian 11 VM for tacticalrmm local development
 | 
			
		||||
							
								
								
									
										40
									
								
								ansible/roles/trmm_dev/defaults/main.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								ansible/roles/trmm_dev/defaults/main.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
			
		||||
---
 | 
			
		||||
user: "tactical"
 | 
			
		||||
python_ver: "3.11.8"
 | 
			
		||||
go_ver: "1.20.7"
 | 
			
		||||
backend_repo: "https://github.com/amidaware/tacticalrmm.git"
 | 
			
		||||
frontend_repo: "https://github.com/amidaware/tacticalrmm-web.git"
 | 
			
		||||
scripts_repo: "https://github.com/amidaware/community-scripts.git"
 | 
			
		||||
backend_dir: "/opt/trmm"
 | 
			
		||||
frontend_dir: "/opt/trmm-web"
 | 
			
		||||
scripts_dir: "/opt/trmm-community-scripts"
 | 
			
		||||
trmm_dir: "{{ backend_dir }}/api/tacticalrmm/tacticalrmm"
 | 
			
		||||
mesh_dir: "/opt/meshcentral"
 | 
			
		||||
settings_file: "{{ trmm_dir }}/settings.py"
 | 
			
		||||
local_settings_file: "{{ trmm_dir }}/local_settings.py"
 | 
			
		||||
fullchain_dest: /etc/ssl/certs/fullchain.pem
 | 
			
		||||
privkey_dest: /etc/ssl/certs/privkey.pem
 | 
			
		||||
 | 
			
		||||
base_pkgs:
 | 
			
		||||
  - build-essential
 | 
			
		||||
  - curl
 | 
			
		||||
  - wget
 | 
			
		||||
  - dirmngr
 | 
			
		||||
  - gnupg
 | 
			
		||||
  - openssl
 | 
			
		||||
  - gcc
 | 
			
		||||
  - g++
 | 
			
		||||
  - make
 | 
			
		||||
  - ca-certificates
 | 
			
		||||
  - git
 | 
			
		||||
 | 
			
		||||
python_pkgs:
 | 
			
		||||
  - zlib1g-dev
 | 
			
		||||
  - libncurses5-dev
 | 
			
		||||
  - libgdbm-dev
 | 
			
		||||
  - libnss3-dev
 | 
			
		||||
  - libssl-dev
 | 
			
		||||
  - libreadline-dev
 | 
			
		||||
  - libffi-dev
 | 
			
		||||
  - libsqlite3-dev
 | 
			
		||||
  - libbz2-dev
 | 
			
		||||
							
								
								
									
										31
									
								
								ansible/roles/trmm_dev/files/nginx-default.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								ansible/roles/trmm_dev/files/nginx-default.conf
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
			
		||||
worker_rlimit_nofile 1000000;
 | 
			
		||||
user www-data;
 | 
			
		||||
worker_processes auto;
 | 
			
		||||
pid /run/nginx.pid;
 | 
			
		||||
include /etc/nginx/modules-enabled/*.conf;
 | 
			
		||||
 | 
			
		||||
events {
 | 
			
		||||
        worker_connections 4096;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
http {
 | 
			
		||||
        sendfile on;
 | 
			
		||||
        server_tokens off;
 | 
			
		||||
        tcp_nopush on;
 | 
			
		||||
        types_hash_max_size 2048;
 | 
			
		||||
        server_names_hash_bucket_size 256;
 | 
			
		||||
        include /etc/nginx/mime.types;
 | 
			
		||||
        default_type application/octet-stream;
 | 
			
		||||
        ssl_protocols TLSv1.2 TLSv1.3;
 | 
			
		||||
        ssl_prefer_server_ciphers on;
 | 
			
		||||
        ssl_ciphers EECDH+AESGCM:EDH+AESGCM;
 | 
			
		||||
        ssl_ecdh_curve secp384r1;
 | 
			
		||||
        ssl_stapling on;
 | 
			
		||||
        ssl_stapling_verify on;
 | 
			
		||||
        add_header X-Content-Type-Options nosniff;
 | 
			
		||||
        access_log /var/log/nginx/access.log;
 | 
			
		||||
        error_log /var/log/nginx/error.log;
 | 
			
		||||
        gzip on;
 | 
			
		||||
        include /etc/nginx/conf.d/*.conf;
 | 
			
		||||
        include /etc/nginx/sites-enabled/*;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										20
									
								
								ansible/roles/trmm_dev/files/vimrc.local
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								ansible/roles/trmm_dev/files/vimrc.local
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
" This file loads the default vim options at the beginning and prevents
 | 
			
		||||
" that they are being loaded again later. All other options that will be set,
 | 
			
		||||
" are added, or overwrite the default settings. Add as many options as you
 | 
			
		||||
" whish at the end of this file.
 | 
			
		||||
 | 
			
		||||
" Load the defaults
 | 
			
		||||
source $VIMRUNTIME/defaults.vim
 | 
			
		||||
 | 
			
		||||
" Prevent the defaults from being loaded again later, if the user doesn't
 | 
			
		||||
" have a local vimrc (~/.vimrc)
 | 
			
		||||
let skip_defaults_vim = 1
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
" Set more options (overwrites settings from /usr/share/vim/vim80/defaults.vim)
 | 
			
		||||
" Add as many options as you whish
 | 
			
		||||
 | 
			
		||||
" Set the mouse mode to 'r'
 | 
			
		||||
if has('mouse')
 | 
			
		||||
  set mouse=r
 | 
			
		||||
endif
 | 
			
		||||
							
								
								
									
										634
									
								
								ansible/roles/trmm_dev/tasks/main.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										634
									
								
								ansible/roles/trmm_dev/tasks/main.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,634 @@
 | 
			
		||||
---
 | 
			
		||||
- name: Append subdomains to hosts
 | 
			
		||||
  tags: hosts
 | 
			
		||||
  become: yes
 | 
			
		||||
  ansible.builtin.lineinfile:
 | 
			
		||||
    path: /etc/hosts
 | 
			
		||||
    backrefs: yes
 | 
			
		||||
    regexp: '^(127\.0\.1\.1 .*)$'
 | 
			
		||||
    line: "\\1 {{ api }} {{ mesh }} {{ rmm }}"
 | 
			
		||||
 | 
			
		||||
- name: set mouse mode for vim
 | 
			
		||||
  tags: vim
 | 
			
		||||
  become: yes
 | 
			
		||||
  ansible.builtin.copy:
 | 
			
		||||
    src: vimrc.local
 | 
			
		||||
    dest: /etc/vim/vimrc.local
 | 
			
		||||
    owner: "root"
 | 
			
		||||
    group: "root"
 | 
			
		||||
    mode: "0644"
 | 
			
		||||
 | 
			
		||||
- name: set max_user_watches
 | 
			
		||||
  tags: sysctl
 | 
			
		||||
  become: yes
 | 
			
		||||
  ansible.builtin.lineinfile:
 | 
			
		||||
    path: /etc/sysctl.conf
 | 
			
		||||
    line: fs.inotify.max_user_watches=524288
 | 
			
		||||
 | 
			
		||||
- name: reload sysctl
 | 
			
		||||
  tags: sysctl
 | 
			
		||||
  become: yes
 | 
			
		||||
  ansible.builtin.command:
 | 
			
		||||
    cmd: sysctl -p
 | 
			
		||||
 | 
			
		||||
- name: install base packages
 | 
			
		||||
  tags: base
 | 
			
		||||
  become: yes
 | 
			
		||||
  ansible.builtin.apt:
 | 
			
		||||
    pkg: "{{ item }}"
 | 
			
		||||
    state: present
 | 
			
		||||
    update_cache: yes
 | 
			
		||||
  with_items:
 | 
			
		||||
    - "{{ base_pkgs }}"
 | 
			
		||||
 | 
			
		||||
- name: set arch fact
 | 
			
		||||
  ansible.builtin.set_fact:
 | 
			
		||||
    goarch: "{{ 'amd64' if ansible_architecture == 'x86_64' else 'arm64' }}"
 | 
			
		||||
 | 
			
		||||
- name: download and install golang
 | 
			
		||||
  tags: golang
 | 
			
		||||
  become: yes
 | 
			
		||||
  ansible.builtin.unarchive:
 | 
			
		||||
    src: "https://go.dev/dl/go{{ go_ver }}.linux-{{ goarch }}.tar.gz"
 | 
			
		||||
    dest: /usr/local
 | 
			
		||||
    remote_src: yes
 | 
			
		||||
 | 
			
		||||
- name: add golang to path
 | 
			
		||||
  become: yes
 | 
			
		||||
  tags: golang
 | 
			
		||||
  ansible.builtin.copy:
 | 
			
		||||
    dest: /etc/profile.d/golang.sh
 | 
			
		||||
    content: "PATH=$PATH:/usr/local/go/bin"
 | 
			
		||||
 | 
			
		||||
- name: install python prereqs
 | 
			
		||||
  tags: python
 | 
			
		||||
  become: yes
 | 
			
		||||
  ansible.builtin.apt:
 | 
			
		||||
    pkg: "{{ item }}"
 | 
			
		||||
    state: present
 | 
			
		||||
  with_items:
 | 
			
		||||
    - "{{ python_pkgs }}"
 | 
			
		||||
 | 
			
		||||
- name: get cpu core count
 | 
			
		||||
  tags: python
 | 
			
		||||
  ansible.builtin.command: nproc
 | 
			
		||||
  register: numprocs
 | 
			
		||||
 | 
			
		||||
- name: Create python tmpdir
 | 
			
		||||
  tags: python
 | 
			
		||||
  ansible.builtin.tempfile:
 | 
			
		||||
    state: directory
 | 
			
		||||
    suffix: python
 | 
			
		||||
  register: python_tmp
 | 
			
		||||
 | 
			
		||||
- name: download and extract python
 | 
			
		||||
  tags: python
 | 
			
		||||
  ansible.builtin.unarchive:
 | 
			
		||||
    src: "https://www.python.org/ftp/python/{{ python_ver }}/Python-{{ python_ver }}.tgz"
 | 
			
		||||
    dest: "{{ python_tmp.path }}"
 | 
			
		||||
    remote_src: yes
 | 
			
		||||
 | 
			
		||||
- name: compile python
 | 
			
		||||
  tags: python
 | 
			
		||||
  ansible.builtin.shell:
 | 
			
		||||
    chdir: "{{ python_tmp.path }}/Python-{{ python_ver }}"
 | 
			
		||||
    cmd: |
 | 
			
		||||
      ./configure --enable-optimizations
 | 
			
		||||
      make -j {{ numprocs.stdout }}
 | 
			
		||||
 | 
			
		||||
- name: alt install python
 | 
			
		||||
  tags: python
 | 
			
		||||
  become: yes
 | 
			
		||||
  ansible.builtin.shell:
 | 
			
		||||
    chdir: "{{ python_tmp.path }}/Python-{{ python_ver }}"
 | 
			
		||||
    cmd: |
 | 
			
		||||
      make altinstall
 | 
			
		||||
 | 
			
		||||
- name: install redis
 | 
			
		||||
  tags: redis
 | 
			
		||||
  become: yes
 | 
			
		||||
  ansible.builtin.apt:
 | 
			
		||||
    pkg: redis
 | 
			
		||||
    state: present
 | 
			
		||||
 | 
			
		||||
- name: create postgres repo
 | 
			
		||||
  tags: postgres
 | 
			
		||||
  become: yes
 | 
			
		||||
  ansible.builtin.copy:
 | 
			
		||||
    content: "deb http://apt.postgresql.org/pub/repos/apt {{ ansible_distribution_release }}-pgdg main"
 | 
			
		||||
    dest: /etc/apt/sources.list.d/pgdg.list
 | 
			
		||||
    owner: root
 | 
			
		||||
    group: root
 | 
			
		||||
    mode: "0644"
 | 
			
		||||
 | 
			
		||||
- name: import postgres repo signing key
 | 
			
		||||
  tags: postgres
 | 
			
		||||
  become: yes
 | 
			
		||||
  ansible.builtin.apt_key:
 | 
			
		||||
    url: https://www.postgresql.org/media/keys/ACCC4CF8.asc
 | 
			
		||||
    state: present
 | 
			
		||||
 | 
			
		||||
- name: install postgresql
 | 
			
		||||
  tags: postgres
 | 
			
		||||
  become: yes
 | 
			
		||||
  ansible.builtin.apt:
 | 
			
		||||
    pkg: postgresql-15
 | 
			
		||||
    state: present
 | 
			
		||||
    update_cache: yes
 | 
			
		||||
 | 
			
		||||
- name: ensure postgres enabled and started
 | 
			
		||||
  tags: postgres
 | 
			
		||||
  become: yes
 | 
			
		||||
  ansible.builtin.service:
 | 
			
		||||
    name: postgresql
 | 
			
		||||
    enabled: yes
 | 
			
		||||
    state: started
 | 
			
		||||
 | 
			
		||||
- name: setup trmm database
 | 
			
		||||
  tags: postgres
 | 
			
		||||
  become: yes
 | 
			
		||||
  become_user: postgres
 | 
			
		||||
  ansible.builtin.shell:
 | 
			
		||||
    cmd: |
 | 
			
		||||
      psql -c "CREATE DATABASE tacticalrmm"
 | 
			
		||||
      psql -c "CREATE USER {{ db_user }} WITH PASSWORD '{{ db_passwd }}'"
 | 
			
		||||
      psql -c "ALTER ROLE {{ db_user }} SET client_encoding TO 'utf8'"
 | 
			
		||||
      psql -c "ALTER ROLE {{ db_user }} SET default_transaction_isolation TO 'read committed'"
 | 
			
		||||
      psql -c "ALTER ROLE {{ db_user }} SET timezone TO 'UTC'"
 | 
			
		||||
      psql -c "ALTER ROLE {{ db_user }} CREATEDB"
 | 
			
		||||
      psql -c "GRANT ALL PRIVILEGES ON DATABASE tacticalrmm TO {{ db_user }}"
 | 
			
		||||
      psql -c "ALTER DATABASE tacticalrmm OWNER TO {{ db_user }}"
 | 
			
		||||
      psql -c "GRANT USAGE, CREATE ON SCHEMA PUBLIC TO {{ db_user }}"
 | 
			
		||||
 | 
			
		||||
- name: setup mesh database
 | 
			
		||||
  tags: postgres
 | 
			
		||||
  become: yes
 | 
			
		||||
  become_user: postgres
 | 
			
		||||
  ansible.builtin.shell:
 | 
			
		||||
    cmd: |
 | 
			
		||||
      psql -c "CREATE DATABASE meshcentral"
 | 
			
		||||
      psql -c "CREATE USER {{ mesh_db_user }} WITH PASSWORD '{{ mesh_db_passwd }}'"
 | 
			
		||||
      psql -c "ALTER ROLE {{ mesh_db_user }} SET client_encoding TO 'utf8'"
 | 
			
		||||
      psql -c "ALTER ROLE {{ mesh_db_user }} SET default_transaction_isolation TO 'read committed'"
 | 
			
		||||
      psql -c "ALTER ROLE {{ mesh_db_user }} SET timezone TO 'UTC'"
 | 
			
		||||
      psql -c "GRANT ALL PRIVILEGES ON DATABASE meshcentral TO {{ mesh_db_user }}"
 | 
			
		||||
      psql -c "ALTER DATABASE meshcentral OWNER TO {{ mesh_db_user }}"
 | 
			
		||||
      psql -c "GRANT USAGE, CREATE ON SCHEMA PUBLIC TO {{ mesh_db_user }}"
 | 
			
		||||
 | 
			
		||||
- name: create repo dirs
 | 
			
		||||
  become: yes
 | 
			
		||||
  tags: git
 | 
			
		||||
  ansible.builtin.file:
 | 
			
		||||
    path: "{{ item }}"
 | 
			
		||||
    state: directory
 | 
			
		||||
    owner: "{{ user }}"
 | 
			
		||||
    group: "{{ user }}"
 | 
			
		||||
    mode: "0755"
 | 
			
		||||
  with_items:
 | 
			
		||||
    - "{{ backend_dir }}"
 | 
			
		||||
    - "{{ frontend_dir }}"
 | 
			
		||||
    - "{{ scripts_dir }}"
 | 
			
		||||
 | 
			
		||||
- name: git clone repos
 | 
			
		||||
  tags: git
 | 
			
		||||
  ansible.builtin.git:
 | 
			
		||||
    repo: "{{ item.repo }}"
 | 
			
		||||
    dest: "{{ item.dest }}"
 | 
			
		||||
    version: "{{ item.version }}"
 | 
			
		||||
  with_items:
 | 
			
		||||
    - {
 | 
			
		||||
        repo: "{{ backend_repo }}",
 | 
			
		||||
        dest: "{{ backend_dir }}",
 | 
			
		||||
        version: develop,
 | 
			
		||||
      }
 | 
			
		||||
    - {
 | 
			
		||||
        repo: "{{ frontend_repo }}",
 | 
			
		||||
        dest: "{{ frontend_dir }}",
 | 
			
		||||
        version: develop,
 | 
			
		||||
      }
 | 
			
		||||
    - { repo: "{{ scripts_repo }}", dest: "{{ scripts_dir }}", version: main }
 | 
			
		||||
 | 
			
		||||
- name: get nats_server_ver
 | 
			
		||||
  tags: nats
 | 
			
		||||
  ansible.builtin.shell: grep "^NATS_SERVER_VER" {{ settings_file }} | awk -F'[= "]' '{print $5}'
 | 
			
		||||
  register: nats_server_ver
 | 
			
		||||
 | 
			
		||||
- name: Create nats tmpdir
 | 
			
		||||
  tags: nats
 | 
			
		||||
  ansible.builtin.tempfile:
 | 
			
		||||
    state: directory
 | 
			
		||||
    suffix: nats
 | 
			
		||||
  register: nats_tmp
 | 
			
		||||
 | 
			
		||||
- name: download and extract nats
 | 
			
		||||
  tags: nats
 | 
			
		||||
  ansible.builtin.unarchive:
 | 
			
		||||
    src: "https://github.com/nats-io/nats-server/releases/download/v{{ nats_server_ver.stdout }}/nats-server-v{{ nats_server_ver.stdout }}-linux-{{ goarch }}.tar.gz"
 | 
			
		||||
    dest: "{{ nats_tmp.path }}"
 | 
			
		||||
    remote_src: yes
 | 
			
		||||
 | 
			
		||||
- name: install nats
 | 
			
		||||
  tags: nats
 | 
			
		||||
  become: yes
 | 
			
		||||
  ansible.builtin.copy:
 | 
			
		||||
    remote_src: yes
 | 
			
		||||
    src: "{{ nats_tmp.path }}/nats-server-v{{ nats_server_ver.stdout }}-linux-{{ goarch }}/nats-server"
 | 
			
		||||
    dest: /usr/local/bin/nats-server
 | 
			
		||||
    owner: "{{ user }}"
 | 
			
		||||
    group: "{{ user }}"
 | 
			
		||||
    mode: "0755"
 | 
			
		||||
 | 
			
		||||
- name: Create nodejs tmpdir
 | 
			
		||||
  tags: nodejs
 | 
			
		||||
  ansible.builtin.tempfile:
 | 
			
		||||
    state: directory
 | 
			
		||||
    suffix: nodejs
 | 
			
		||||
  register: nodejs_tmp
 | 
			
		||||
 | 
			
		||||
- name: download nodejs setup
 | 
			
		||||
  tags: nodejs
 | 
			
		||||
  ansible.builtin.get_url:
 | 
			
		||||
    url: https://deb.nodesource.com/setup_18.x
 | 
			
		||||
    dest: "{{ nodejs_tmp.path }}/setup_node.sh"
 | 
			
		||||
    mode: "0755"
 | 
			
		||||
 | 
			
		||||
- name: run node setup script
 | 
			
		||||
  tags: nodejs
 | 
			
		||||
  become: yes
 | 
			
		||||
  ansible.builtin.command:
 | 
			
		||||
    cmd: "{{ nodejs_tmp.path }}/setup_node.sh"
 | 
			
		||||
 | 
			
		||||
- name: install nodejs
 | 
			
		||||
  tags: nodejs
 | 
			
		||||
  become: yes
 | 
			
		||||
  ansible.builtin.apt:
 | 
			
		||||
    pkg: nodejs
 | 
			
		||||
    state: present
 | 
			
		||||
    update_cache: yes
 | 
			
		||||
 | 
			
		||||
- name: update npm
 | 
			
		||||
  tags: nodejs
 | 
			
		||||
  become: yes
 | 
			
		||||
  ansible.builtin.shell:
 | 
			
		||||
    cmd: npm install -g npm
 | 
			
		||||
 | 
			
		||||
- name: install quasar cli
 | 
			
		||||
  tags: quasar
 | 
			
		||||
  become: yes
 | 
			
		||||
  ansible.builtin.shell:
 | 
			
		||||
    cmd: npm install -g @quasar/cli
 | 
			
		||||
 | 
			
		||||
- name: install frontend
 | 
			
		||||
  tags: quasar
 | 
			
		||||
  ansible.builtin.shell:
 | 
			
		||||
    chdir: "{{ frontend_dir }}"
 | 
			
		||||
    cmd: npm install
 | 
			
		||||
 | 
			
		||||
- name: add quasar env
 | 
			
		||||
  tags: quasar
 | 
			
		||||
  ansible.builtin.template:
 | 
			
		||||
    src: quasar.env.j2
 | 
			
		||||
    dest: "{{ frontend_dir }}/.env"
 | 
			
		||||
    owner: "{{ user }}"
 | 
			
		||||
    group: "{{ user }}"
 | 
			
		||||
    mode: "0644"
 | 
			
		||||
 | 
			
		||||
- name: remove tempdirs
 | 
			
		||||
  tags: cleanup
 | 
			
		||||
  become: yes
 | 
			
		||||
  ignore_errors: yes
 | 
			
		||||
  ansible.builtin.file:
 | 
			
		||||
    path: "{{ item }}"
 | 
			
		||||
    state: absent
 | 
			
		||||
  with_items:
 | 
			
		||||
    - "{{ nats_tmp.path }}"
 | 
			
		||||
    - "{{ python_tmp.path }}"
 | 
			
		||||
    - "{{ nodejs_tmp.path }}"
 | 
			
		||||
 | 
			
		||||
- name: deploy fullchain
 | 
			
		||||
  tags: certs
 | 
			
		||||
  become: yes
 | 
			
		||||
  ansible.builtin.copy:
 | 
			
		||||
    src: "{{ fullchain_src }}"
 | 
			
		||||
    dest: "{{ fullchain_dest }}"
 | 
			
		||||
    owner: "{{ user }}"
 | 
			
		||||
    group: "{{ user }}"
 | 
			
		||||
    mode: "0440"
 | 
			
		||||
 | 
			
		||||
- name: deploy privkey
 | 
			
		||||
  tags: certs
 | 
			
		||||
  become: yes
 | 
			
		||||
  ansible.builtin.copy:
 | 
			
		||||
    src: "{{ privkey_src }}"
 | 
			
		||||
    dest: "{{ privkey_dest }}"
 | 
			
		||||
    owner: "{{ user }}"
 | 
			
		||||
    group: "{{ user }}"
 | 
			
		||||
    mode: "0440"
 | 
			
		||||
 | 
			
		||||
- name: import nginx signing key
 | 
			
		||||
  tags: nginx
 | 
			
		||||
  become: yes
 | 
			
		||||
  ansible.builtin.apt_key:
 | 
			
		||||
    url: https://nginx.org/packages/keys/nginx_signing.key
 | 
			
		||||
    state: present
 | 
			
		||||
 | 
			
		||||
- name: add nginx repo
 | 
			
		||||
  tags: nginx
 | 
			
		||||
  become: yes
 | 
			
		||||
  ansible.builtin.template:
 | 
			
		||||
    src: nginx.repo.j2
 | 
			
		||||
    dest: /etc/apt/sources.list.d/nginx.list
 | 
			
		||||
    owner: "root"
 | 
			
		||||
    group: "root"
 | 
			
		||||
    mode: "0644"
 | 
			
		||||
 | 
			
		||||
- name: install nginx
 | 
			
		||||
  tags: nginx
 | 
			
		||||
  become: yes
 | 
			
		||||
  ansible.builtin.apt:
 | 
			
		||||
    pkg: nginx
 | 
			
		||||
    state: present
 | 
			
		||||
    update_cache: yes
 | 
			
		||||
 | 
			
		||||
- name: set nginx default conf
 | 
			
		||||
  tags: nginx
 | 
			
		||||
  become: yes
 | 
			
		||||
  ansible.builtin.copy:
 | 
			
		||||
    src: nginx-default.conf
 | 
			
		||||
    dest: /etc/nginx/nginx.conf
 | 
			
		||||
    owner: "root"
 | 
			
		||||
    group: "root"
 | 
			
		||||
    mode: "0644"
 | 
			
		||||
 | 
			
		||||
- name: create nginx dirs
 | 
			
		||||
  become: yes
 | 
			
		||||
  tags: nginx
 | 
			
		||||
  ansible.builtin.file:
 | 
			
		||||
    state: directory
 | 
			
		||||
    path: "{{ item }}"
 | 
			
		||||
    mode: "0755"
 | 
			
		||||
  with_items:
 | 
			
		||||
    - /etc/nginx/sites-available
 | 
			
		||||
    - /etc/nginx/sites-enabled
 | 
			
		||||
 | 
			
		||||
- name: deploy nginx sites
 | 
			
		||||
  become: yes
 | 
			
		||||
  tags: nginx
 | 
			
		||||
  ansible.builtin.template:
 | 
			
		||||
    src: "{{ item.src }}"
 | 
			
		||||
    dest: "{{ item.dest }}"
 | 
			
		||||
    mode: "0644"
 | 
			
		||||
    owner: root
 | 
			
		||||
    group: root
 | 
			
		||||
  with_items:
 | 
			
		||||
    - { src: backend.nginx.j2, dest: /etc/nginx/sites-available/backend.conf }
 | 
			
		||||
    - { src: mesh.nginx.j2, dest: /etc/nginx/sites-available/mesh.conf }
 | 
			
		||||
 | 
			
		||||
- name: enable nginx sites
 | 
			
		||||
  become: yes
 | 
			
		||||
  tags: nginx
 | 
			
		||||
  ansible.builtin.file:
 | 
			
		||||
    src: "{{ item.src }}"
 | 
			
		||||
    dest: "{{ item.dest }}"
 | 
			
		||||
    mode: "0644"
 | 
			
		||||
    owner: root
 | 
			
		||||
    group: root
 | 
			
		||||
    state: link
 | 
			
		||||
  with_items:
 | 
			
		||||
    - {
 | 
			
		||||
        src: /etc/nginx/sites-available/backend.conf,
 | 
			
		||||
        dest: /etc/nginx/sites-enabled/backend.conf,
 | 
			
		||||
      }
 | 
			
		||||
    - {
 | 
			
		||||
        src: /etc/nginx/sites-available/mesh.conf,
 | 
			
		||||
        dest: /etc/nginx/sites-enabled/mesh.conf,
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
- name: ensure nginx enabled and restarted
 | 
			
		||||
  tags: nginx
 | 
			
		||||
  become: yes
 | 
			
		||||
  ansible.builtin.service:
 | 
			
		||||
    name: nginx
 | 
			
		||||
    enabled: yes
 | 
			
		||||
    state: restarted
 | 
			
		||||
 | 
			
		||||
- name: set natsapi fact
 | 
			
		||||
  ansible.builtin.set_fact:
 | 
			
		||||
    natsapi: "{{ 'nats-api' if ansible_architecture == 'x86_64' else 'nats-api-arm64' }}"
 | 
			
		||||
 | 
			
		||||
- name: copy nats-api bin
 | 
			
		||||
  tags: nats-api
 | 
			
		||||
  become: yes
 | 
			
		||||
  ansible.builtin.copy:
 | 
			
		||||
    remote_src: yes
 | 
			
		||||
    src: "{{ backend_dir }}/natsapi/bin/{{ natsapi }}"
 | 
			
		||||
    dest: /usr/local/bin/nats-api
 | 
			
		||||
    owner: "{{ user }}"
 | 
			
		||||
    group: "{{ user }}"
 | 
			
		||||
    mode: "0755"
 | 
			
		||||
 | 
			
		||||
- name: get setuptools_ver
 | 
			
		||||
  tags: pip
 | 
			
		||||
  ansible.builtin.shell: grep "^SETUPTOOLS_VER" {{ settings_file }} | awk -F'[= "]' '{print $5}'
 | 
			
		||||
  register: setuptools_ver
 | 
			
		||||
 | 
			
		||||
- name: get wheel_ver
 | 
			
		||||
  tags: pip
 | 
			
		||||
  ansible.builtin.shell: grep "^WHEEL_VER" {{ settings_file }} | awk -F'[= "]' '{print $5}'
 | 
			
		||||
  register: wheel_ver
 | 
			
		||||
 | 
			
		||||
- name: setup virtual env
 | 
			
		||||
  tags: pip
 | 
			
		||||
  ansible.builtin.shell:
 | 
			
		||||
    chdir: "{{ backend_dir }}/api"
 | 
			
		||||
    cmd: python3.11 -m venv env
 | 
			
		||||
 | 
			
		||||
- name: update pip to latest
 | 
			
		||||
  tags: pip
 | 
			
		||||
  ansible.builtin.pip:
 | 
			
		||||
    virtualenv: "{{ backend_dir }}/api/env"
 | 
			
		||||
    name: pip
 | 
			
		||||
    state: latest
 | 
			
		||||
 | 
			
		||||
- name: install setuptools and wheel
 | 
			
		||||
  tags: pip
 | 
			
		||||
  ansible.builtin.pip:
 | 
			
		||||
    virtualenv: "{{ backend_dir }}/api/env"
 | 
			
		||||
    name: "{{ item }}"
 | 
			
		||||
  with_items:
 | 
			
		||||
    - "setuptools=={{ setuptools_ver.stdout }}"
 | 
			
		||||
    - "wheel=={{ wheel_ver.stdout }}"
 | 
			
		||||
 | 
			
		||||
- name: install python packages
 | 
			
		||||
  tags: pip
 | 
			
		||||
  ansible.builtin.pip:
 | 
			
		||||
    virtualenv: "{{ backend_dir }}/api/env"
 | 
			
		||||
    chdir: "{{ backend_dir }}/api/tacticalrmm"
 | 
			
		||||
    requirements: "{{ item }}"
 | 
			
		||||
  with_items:
 | 
			
		||||
    - requirements.txt
 | 
			
		||||
    - requirements-dev.txt
 | 
			
		||||
    - requirements-test.txt
 | 
			
		||||
 | 
			
		||||
- name: deploy django local settings
 | 
			
		||||
  tags: django
 | 
			
		||||
  ansible.builtin.template:
 | 
			
		||||
    src: local_settings.j2
 | 
			
		||||
    dest: "{{ local_settings_file }}"
 | 
			
		||||
    mode: "0644"
 | 
			
		||||
    owner: "{{ user }}"
 | 
			
		||||
    group: "{{ user }}"
 | 
			
		||||
 | 
			
		||||
- name: setup django
 | 
			
		||||
  tags: django
 | 
			
		||||
  ansible.builtin.shell:
 | 
			
		||||
    chdir: "{{ backend_dir }}/api/tacticalrmm"
 | 
			
		||||
    cmd: |
 | 
			
		||||
      . ../env/bin/activate
 | 
			
		||||
      python manage.py migrate --no-input
 | 
			
		||||
      python manage.py collectstatic --no-input
 | 
			
		||||
      python manage.py create_natsapi_conf
 | 
			
		||||
      python manage.py load_chocos
 | 
			
		||||
      python manage.py load_community_scripts
 | 
			
		||||
      echo "from accounts.models import User; User.objects.create_superuser('{{ django_user }}', '{{ github_email }}', '{{ django_password }}') if not User.objects.filter(username='{{ django_user }}').exists() else 0;" | python manage.py shell
 | 
			
		||||
      python manage.py create_installer_user
 | 
			
		||||
 | 
			
		||||
- name: deploy services
 | 
			
		||||
  tags: services
 | 
			
		||||
  become: yes
 | 
			
		||||
  ansible.builtin.template:
 | 
			
		||||
    src: "{{ item.src }}"
 | 
			
		||||
    dest: "{{ item.dest }}"
 | 
			
		||||
    mode: "0644"
 | 
			
		||||
    owner: "root"
 | 
			
		||||
    group: "root"
 | 
			
		||||
  with_items:
 | 
			
		||||
    - { src: nats-api.systemd.j2, dest: /etc/systemd/system/nats-api.service }
 | 
			
		||||
    - { src: nats-server.systemd.j2, dest: /etc/systemd/system/nats.service }
 | 
			
		||||
    - { src: mesh.systemd.j2, dest: /etc/systemd/system/meshcentral.service }
 | 
			
		||||
 | 
			
		||||
- name: get mesh_ver
 | 
			
		||||
  tags: mesh
 | 
			
		||||
  ansible.builtin.shell: grep "^MESH_VER" {{ settings_file }} | awk -F'[= "]' '{print $5}'
 | 
			
		||||
  register: mesh_ver
 | 
			
		||||
 | 
			
		||||
- name: create meshcentral data directory
 | 
			
		||||
  tags: mesh
 | 
			
		||||
  become: yes
 | 
			
		||||
  ansible.builtin.file:
 | 
			
		||||
    path: "{{ mesh_dir }}/meshcentral-data"
 | 
			
		||||
    state: directory
 | 
			
		||||
    owner: "{{ user }}"
 | 
			
		||||
    group: "{{ user }}"
 | 
			
		||||
    mode: "0755"
 | 
			
		||||
 | 
			
		||||
- name: install meshcentral
 | 
			
		||||
  tags: mesh
 | 
			
		||||
  ansible.builtin.command:
 | 
			
		||||
    chdir: "{{ mesh_dir }}"
 | 
			
		||||
    cmd: "npm install meshcentral@{{ mesh_ver.stdout }}"
 | 
			
		||||
 | 
			
		||||
- name: deploy mesh config
 | 
			
		||||
  tags: mesh
 | 
			
		||||
  ansible.builtin.template:
 | 
			
		||||
    src: mesh.cfg.j2
 | 
			
		||||
    dest: "{{ mesh_dir }}/meshcentral-data/config.json"
 | 
			
		||||
    mode: "0644"
 | 
			
		||||
    owner: "{{ user }}"
 | 
			
		||||
    group: "{{ user }}"
 | 
			
		||||
 | 
			
		||||
- name: start meshcentral
 | 
			
		||||
  tags: mesh
 | 
			
		||||
  become: yes
 | 
			
		||||
  ansible.builtin.systemd:
 | 
			
		||||
    name: meshcentral.service
 | 
			
		||||
    state: started
 | 
			
		||||
    enabled: yes
 | 
			
		||||
    daemon_reload: yes
 | 
			
		||||
 | 
			
		||||
- name: wait for meshcentral to be ready
 | 
			
		||||
  tags: mesh
 | 
			
		||||
  uri:
 | 
			
		||||
    url: "https://{{ mesh }}"
 | 
			
		||||
    return_content: yes
 | 
			
		||||
    validate_certs: yes
 | 
			
		||||
    status_code: 200
 | 
			
		||||
  register: mesh_status
 | 
			
		||||
  until: mesh_status.status == 200
 | 
			
		||||
  retries: 20
 | 
			
		||||
  delay: 3
 | 
			
		||||
 | 
			
		||||
- name: get meshcentral login token key
 | 
			
		||||
  tags: mesh_key
 | 
			
		||||
  ansible.builtin.command:
 | 
			
		||||
    chdir: "{{ mesh_dir }}"
 | 
			
		||||
    cmd: node node_modules/meshcentral --logintokenkey
 | 
			
		||||
  register: mesh_token_key
 | 
			
		||||
 | 
			
		||||
- name: add mesh key to django settings file
 | 
			
		||||
  tags: mesh_key
 | 
			
		||||
  ansible.builtin.lineinfile:
 | 
			
		||||
    path: "{{ local_settings_file }}"
 | 
			
		||||
    line: 'MESH_TOKEN_KEY = "{{ mesh_token_key.stdout }}"'
 | 
			
		||||
 | 
			
		||||
- name: stop meshcentral service
 | 
			
		||||
  tags: mesh_user
 | 
			
		||||
  become: yes
 | 
			
		||||
  ansible.builtin.service:
 | 
			
		||||
    name: meshcentral.service
 | 
			
		||||
    state: stopped
 | 
			
		||||
 | 
			
		||||
- name: create mesh user
 | 
			
		||||
  tags: mesh_user
 | 
			
		||||
  ansible.builtin.shell:
 | 
			
		||||
    chdir: "{{ mesh_dir }}"
 | 
			
		||||
    cmd: |
 | 
			
		||||
      node node_modules/meshcentral --createaccount {{ mesh_user }} --pass {{ mesh_password }} --email {{ github_email }}
 | 
			
		||||
      node node_modules/meshcentral --adminaccount {{ mesh_user }}
 | 
			
		||||
 | 
			
		||||
- name: start meshcentral service
 | 
			
		||||
  tags: mesh_user
 | 
			
		||||
  become: yes
 | 
			
		||||
  ansible.builtin.service:
 | 
			
		||||
    name: meshcentral.service
 | 
			
		||||
    state: started
 | 
			
		||||
 | 
			
		||||
- name: wait for meshcentral to be ready
 | 
			
		||||
  tags: mesh_user
 | 
			
		||||
  uri:
 | 
			
		||||
    url: "https://{{ mesh }}"
 | 
			
		||||
    return_content: yes
 | 
			
		||||
    validate_certs: yes
 | 
			
		||||
    status_code: 200
 | 
			
		||||
  register: mesh_status
 | 
			
		||||
  until: mesh_status.status == 200
 | 
			
		||||
  retries: 20
 | 
			
		||||
  delay: 3
 | 
			
		||||
 | 
			
		||||
- name: create mesh device group
 | 
			
		||||
  tags: mesh_user
 | 
			
		||||
  ansible.builtin.shell:
 | 
			
		||||
    chdir: "{{ mesh_dir }}"
 | 
			
		||||
    cmd: |
 | 
			
		||||
      node node_modules/meshcentral/meshctrl.js --url wss://{{ mesh }}:443 --loginuser {{ mesh_user }} --loginpass {{ mesh_password }} AddDeviceGroup --name TacticalRMM
 | 
			
		||||
 | 
			
		||||
- name: finish up django
 | 
			
		||||
  tags: mesh_user
 | 
			
		||||
  ansible.builtin.shell:
 | 
			
		||||
    chdir: "{{ backend_dir }}/api/tacticalrmm"
 | 
			
		||||
    cmd: |
 | 
			
		||||
      . ../env/bin/activate
 | 
			
		||||
      python manage.py initial_db_setup
 | 
			
		||||
      python manage.py reload_nats
 | 
			
		||||
 | 
			
		||||
- name: restart services
 | 
			
		||||
  tags: services
 | 
			
		||||
  become: yes
 | 
			
		||||
  ansible.builtin.systemd:
 | 
			
		||||
    daemon_reload: yes
 | 
			
		||||
    enabled: yes
 | 
			
		||||
    state: restarted
 | 
			
		||||
    name: "{{ item }}.service"
 | 
			
		||||
  with_items:
 | 
			
		||||
    - nats
 | 
			
		||||
    - nats-api
 | 
			
		||||
							
								
								
									
										20
									
								
								ansible/roles/trmm_dev/templates/backend.nginx.j2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								ansible/roles/trmm_dev/templates/backend.nginx.j2
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
server {
 | 
			
		||||
    listen 443 ssl reuseport;
 | 
			
		||||
    listen [::]:443 ssl;
 | 
			
		||||
    server_name {{ api }};
 | 
			
		||||
    client_max_body_size 300M;
 | 
			
		||||
    ssl_certificate {{ fullchain_dest }};
 | 
			
		||||
    ssl_certificate_key {{ privkey_dest }};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    location ~ ^/natsws {
 | 
			
		||||
        proxy_pass http://127.0.0.1:9235;
 | 
			
		||||
        proxy_http_version 1.1;
 | 
			
		||||
        proxy_set_header Host $host;
 | 
			
		||||
        proxy_set_header Upgrade $http_upgrade;
 | 
			
		||||
        proxy_set_header Connection "upgrade";
 | 
			
		||||
        proxy_set_header X-Forwarded-Host $host:$server_port;
 | 
			
		||||
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 | 
			
		||||
        proxy_set_header X-Forwarded-Proto $scheme;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										20
									
								
								ansible/roles/trmm_dev/templates/local_settings.j2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								ansible/roles/trmm_dev/templates/local_settings.j2
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
SECRET_KEY = "{{ django_secret }}"
 | 
			
		||||
DEBUG = True
 | 
			
		||||
ALLOWED_HOSTS = ['{{ api }}']
 | 
			
		||||
ADMIN_URL = "admin/"
 | 
			
		||||
CORS_ORIGIN_ALLOW_ALL = True
 | 
			
		||||
DATABASES = {
 | 
			
		||||
    'default': {
 | 
			
		||||
        'ENGINE': 'django.db.backends.postgresql',
 | 
			
		||||
        'NAME': 'tacticalrmm',
 | 
			
		||||
        'USER': '{{ db_user }}',
 | 
			
		||||
        'PASSWORD': '{{ db_passwd }}',
 | 
			
		||||
        'HOST': 'localhost',
 | 
			
		||||
        'PORT': '5432',
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
ADMIN_ENABLED = True
 | 
			
		||||
CERT_FILE = "{{ fullchain_dest }}"
 | 
			
		||||
KEY_FILE = "{{ privkey_dest }}"
 | 
			
		||||
MESH_USERNAME = "{{ mesh_user }}"
 | 
			
		||||
MESH_SITE = "https://{{ mesh }}"
 | 
			
		||||
							
								
								
									
										37
									
								
								ansible/roles/trmm_dev/templates/mesh.cfg.j2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								ansible/roles/trmm_dev/templates/mesh.cfg.j2
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
			
		||||
{
 | 
			
		||||
  "settings": {
 | 
			
		||||
    "Cert": "{{ mesh }}",
 | 
			
		||||
    "WANonly": true,
 | 
			
		||||
    "Minify": 1,
 | 
			
		||||
    "Port": 4430,
 | 
			
		||||
    "AliasPort": 443,
 | 
			
		||||
    "RedirPort": 800,
 | 
			
		||||
    "AllowLoginToken": true,
 | 
			
		||||
    "AllowFraming": true,
 | 
			
		||||
    "AgentPing": 35,
 | 
			
		||||
    "AllowHighQualityDesktop": true,
 | 
			
		||||
    "TlsOffload": "127.0.0.1",
 | 
			
		||||
    "agentCoreDump": false,
 | 
			
		||||
    "Compression": true,
 | 
			
		||||
    "WsCompression": true,
 | 
			
		||||
    "AgentWsCompression": true,
 | 
			
		||||
    "MaxInvalidLogin": { "time": 5, "count": 5, "coolofftime": 30 },
 | 
			
		||||
    "postgres": {
 | 
			
		||||
      "user": "{{ mesh_db_user }}",
 | 
			
		||||
      "password": "{{ mesh_db_passwd }}",
 | 
			
		||||
      "port": "5432",
 | 
			
		||||
      "host": "localhost"
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "domains": {
 | 
			
		||||
    "": {
 | 
			
		||||
      "Title": "Tactical RMM Dev",
 | 
			
		||||
      "Title2": "Tactical RMM Dev",
 | 
			
		||||
      "NewAccounts": false,
 | 
			
		||||
      "CertUrl": "https://{{ mesh }}:443/",
 | 
			
		||||
      "GeoLocation": true,
 | 
			
		||||
      "CookieIpCheck": false,
 | 
			
		||||
      "mstsc": true
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										22
									
								
								ansible/roles/trmm_dev/templates/mesh.nginx.j2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								ansible/roles/trmm_dev/templates/mesh.nginx.j2
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
server {
 | 
			
		||||
    listen 443 ssl;
 | 
			
		||||
    listen [::]:443 ssl;
 | 
			
		||||
    proxy_send_timeout 330s;
 | 
			
		||||
    proxy_read_timeout 330s;
 | 
			
		||||
    server_name {{ mesh }};
 | 
			
		||||
    ssl_certificate {{ fullchain_dest }};
 | 
			
		||||
    ssl_certificate_key {{ privkey_dest }};
 | 
			
		||||
 | 
			
		||||
    ssl_session_cache shared:WEBSSL:10m;
 | 
			
		||||
 | 
			
		||||
    location / {
 | 
			
		||||
        proxy_pass http://127.0.0.1:4430/;
 | 
			
		||||
        proxy_http_version 1.1;
 | 
			
		||||
        proxy_set_header Host $host;
 | 
			
		||||
        proxy_set_header Upgrade $http_upgrade;
 | 
			
		||||
        proxy_set_header Connection "upgrade";
 | 
			
		||||
        proxy_set_header X-Forwarded-Host $host:$server_port;
 | 
			
		||||
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 | 
			
		||||
        proxy_set_header X-Forwarded-Proto $scheme;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										17
									
								
								ansible/roles/trmm_dev/templates/mesh.systemd.j2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								ansible/roles/trmm_dev/templates/mesh.systemd.j2
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
[Unit]
 | 
			
		||||
Description=MeshCentral Server
 | 
			
		||||
After=network.target postgresql.service nginx.service
 | 
			
		||||
 | 
			
		||||
[Service]
 | 
			
		||||
Type=simple
 | 
			
		||||
LimitNOFILE=1000000
 | 
			
		||||
ExecStart=/usr/bin/node node_modules/meshcentral
 | 
			
		||||
Environment=NODE_ENV=production
 | 
			
		||||
WorkingDirectory={{ mesh_dir }}
 | 
			
		||||
User={{ user }}
 | 
			
		||||
Group={{ user }}
 | 
			
		||||
Restart=always
 | 
			
		||||
RestartSec=10s
 | 
			
		||||
 | 
			
		||||
[Install]
 | 
			
		||||
WantedBy=multi-user.target
 | 
			
		||||
							
								
								
									
										14
									
								
								ansible/roles/trmm_dev/templates/nats-api.systemd.j2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								ansible/roles/trmm_dev/templates/nats-api.systemd.j2
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
			
		||||
[Unit]
 | 
			
		||||
Description=TacticalRMM Nats Api
 | 
			
		||||
After=nats.service
 | 
			
		||||
 | 
			
		||||
[Service]
 | 
			
		||||
Type=simple
 | 
			
		||||
ExecStart=/usr/local/bin/nats-api -config {{ backend_dir }}/api/tacticalrmm/nats-api.conf
 | 
			
		||||
User={{ user }}
 | 
			
		||||
Group={{ user }}
 | 
			
		||||
Restart=always
 | 
			
		||||
RestartSec=5s
 | 
			
		||||
 | 
			
		||||
[Install]
 | 
			
		||||
WantedBy=multi-user.target
 | 
			
		||||
							
								
								
									
										18
									
								
								ansible/roles/trmm_dev/templates/nats-server.systemd.j2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								ansible/roles/trmm_dev/templates/nats-server.systemd.j2
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
[Unit]
 | 
			
		||||
Description=NATS Server
 | 
			
		||||
After=network.target
 | 
			
		||||
 | 
			
		||||
[Service]
 | 
			
		||||
PrivateTmp=true
 | 
			
		||||
Type=simple
 | 
			
		||||
ExecStart=/usr/local/bin/nats-server -c {{ backend_dir }}/api/tacticalrmm/nats-rmm.conf
 | 
			
		||||
ExecReload=/usr/bin/kill -s HUP $MAINPID
 | 
			
		||||
ExecStop=/usr/bin/kill -s SIGINT $MAINPID
 | 
			
		||||
User={{ user }}
 | 
			
		||||
Group={{ user }}
 | 
			
		||||
Restart=always
 | 
			
		||||
RestartSec=5s
 | 
			
		||||
LimitNOFILE=1000000
 | 
			
		||||
 | 
			
		||||
[Install]
 | 
			
		||||
WantedBy=multi-user.target
 | 
			
		||||
							
								
								
									
										2
									
								
								ansible/roles/trmm_dev/templates/nginx.repo.j2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								ansible/roles/trmm_dev/templates/nginx.repo.j2
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
			
		||||
deb https://nginx.org/packages/debian/ {{ ansible_distribution_release }} nginx
 | 
			
		||||
deb-src https://nginx.org/packages/debian/ {{ ansible_distribution_release }} nginx
 | 
			
		||||
							
								
								
									
										4
									
								
								ansible/roles/trmm_dev/templates/quasar.env.j2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								ansible/roles/trmm_dev/templates/quasar.env.j2
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
DEV_URL = "http://{{ api }}:8000"
 | 
			
		||||
DEV_HOST = "0.0.0.0"
 | 
			
		||||
DEV_PORT = "8080"
 | 
			
		||||
USE_HTTPS = false
 | 
			
		||||
							
								
								
									
										22
									
								
								ansible/setup_dev.yml.example
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								ansible/setup_dev.yml.example
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
---
 | 
			
		||||
- hosts: "{{ target }}"
 | 
			
		||||
  vars:
 | 
			
		||||
    ansible_user: tactical
 | 
			
		||||
    fullchain_src: /path/to/fullchain.pem
 | 
			
		||||
    privkey_src: /path/to/privkey.pem
 | 
			
		||||
    api: "api.example.com"
 | 
			
		||||
    rmm: "rmm.example.com"
 | 
			
		||||
    mesh: "mesh.example.com"
 | 
			
		||||
    github_username: "changeme"
 | 
			
		||||
    github_email: "changeme@example.com"
 | 
			
		||||
    mesh_user: "changeme"
 | 
			
		||||
    mesh_password: "changeme"
 | 
			
		||||
    db_user: "changeme"
 | 
			
		||||
    db_passwd: "changeme"
 | 
			
		||||
    mesh_db_user: "changeme"
 | 
			
		||||
    mesh_db_passwd: "changeme"
 | 
			
		||||
    django_secret: "changeme"
 | 
			
		||||
    django_user: "changeme"
 | 
			
		||||
    django_password: "changeme"
 | 
			
		||||
  roles:
 | 
			
		||||
    - trmm_dev
 | 
			
		||||
@@ -1,26 +1,15 @@
 | 
			
		||||
[run]
 | 
			
		||||
source = .
 | 
			
		||||
[report]
 | 
			
		||||
show_missing = True
 | 
			
		||||
include = *.py
 | 
			
		||||
omit =
 | 
			
		||||
    tacticalrmm/asgi.py
 | 
			
		||||
    tacticalrmm/wsgi.py
 | 
			
		||||
    manage.py
 | 
			
		||||
    */__pycache__/*
 | 
			
		||||
    */env/*
 | 
			
		||||
    */management/*
 | 
			
		||||
    */migrations/*
 | 
			
		||||
    */static/*
 | 
			
		||||
    manage.py
 | 
			
		||||
    */local_settings.py
 | 
			
		||||
    */apps.py
 | 
			
		||||
    */admin.py
 | 
			
		||||
    */celery.py
 | 
			
		||||
    */wsgi.py
 | 
			
		||||
    */settings.py
 | 
			
		||||
    */baker_recipes.py
 | 
			
		||||
    */urls.py
 | 
			
		||||
    */tests.py
 | 
			
		||||
    */test.py
 | 
			
		||||
    checks/utils.py
 | 
			
		||||
    */asgi.py
 | 
			
		||||
    */demo_views.py
 | 
			
		||||
    /usr/local/lib/*
 | 
			
		||||
    **/migrations/*
 | 
			
		||||
    **/test*.py
 | 
			
		||||
    
 | 
			
		||||
[report]
 | 
			
		||||
show_missing = True
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										12
									
								
								api/tacticalrmm/.flake8
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								api/tacticalrmm/.flake8
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
[flake8]
 | 
			
		||||
ignore = E501,W503,E722,E203
 | 
			
		||||
exclude =
 | 
			
		||||
    .mypy*
 | 
			
		||||
    .pytest*
 | 
			
		||||
    .git
 | 
			
		||||
    demo_data.py
 | 
			
		||||
    manage.py
 | 
			
		||||
    */__pycache__/*
 | 
			
		||||
    */env/*
 | 
			
		||||
    /usr/local/lib/*
 | 
			
		||||
    **/migrations/*
 | 
			
		||||
@@ -1,22 +1,24 @@
 | 
			
		||||
import uuid
 | 
			
		||||
 | 
			
		||||
from accounts.models import User
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
 | 
			
		||||
from accounts.models import User
 | 
			
		||||
from tacticalrmm.helpers import make_random_password
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = "Creates the installer user"
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args, **kwargs):
 | 
			
		||||
    def handle(self, *args, **kwargs):  # type: ignore
 | 
			
		||||
        self.stdout.write("Checking if installer user has been created...")
 | 
			
		||||
        if User.objects.filter(is_installer_user=True).exists():
 | 
			
		||||
            self.stdout.write("Installer user already exists")
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        User.objects.create_user(  # type: ignore
 | 
			
		||||
        User.objects.create_user(
 | 
			
		||||
            username=uuid.uuid4().hex,
 | 
			
		||||
            is_installer_user=True,
 | 
			
		||||
            password=User.objects.make_random_password(60),  # type: ignore
 | 
			
		||||
            password=make_random_password(len=60),
 | 
			
		||||
            block_dashboard_login=True,
 | 
			
		||||
        )
 | 
			
		||||
        self.stdout.write("Installer user has been created")
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ from knox.models import AuthToken
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = "Deletes all knox web tokens"
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args, **kwargs):
 | 
			
		||||
    def handle(self, *args, **kwargs):  # type: ignore
 | 
			
		||||
        # only delete web tokens, not any generated by the installer or deployments
 | 
			
		||||
        dont_delete = djangotime.now() + djangotime.timedelta(hours=23)
 | 
			
		||||
        tokens = AuthToken.objects.exclude(deploytokens__isnull=False).filter(
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,10 @@
 | 
			
		||||
import subprocess
 | 
			
		||||
 | 
			
		||||
import pyotp
 | 
			
		||||
from accounts.models import User
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
 | 
			
		||||
from accounts.models import User
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = "Generates barcode for Authenticator and creates totp for user"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,11 @@
 | 
			
		||||
import os
 | 
			
		||||
import subprocess
 | 
			
		||||
 | 
			
		||||
import pyotp
 | 
			
		||||
from accounts.models import User
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
 | 
			
		||||
from accounts.models import User
 | 
			
		||||
from tacticalrmm.helpers import get_webdomain
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = "Reset 2fa"
 | 
			
		||||
@@ -20,28 +21,13 @@ class Command(BaseCommand):
 | 
			
		||||
            self.stdout.write(self.style.ERROR(f"User {username} doesn't exist"))
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        domain = "Tactical RMM"
 | 
			
		||||
        nginx = "/etc/nginx/sites-available/frontend.conf"
 | 
			
		||||
        found = None
 | 
			
		||||
        if os.path.exists(nginx):
 | 
			
		||||
            try:
 | 
			
		||||
                with open(nginx, "r") as f:
 | 
			
		||||
                    for line in f:
 | 
			
		||||
                        if "server_name" in line:
 | 
			
		||||
                            found = line
 | 
			
		||||
                            break
 | 
			
		||||
 | 
			
		||||
                if found:
 | 
			
		||||
                    rep = found.replace("server_name", "").replace(";", "")
 | 
			
		||||
                    domain = "".join(rep.split())
 | 
			
		||||
            except:
 | 
			
		||||
                pass
 | 
			
		||||
 | 
			
		||||
        code = pyotp.random_base32()
 | 
			
		||||
        user.totp_key = code
 | 
			
		||||
        user.save(update_fields=["totp_key"])
 | 
			
		||||
 | 
			
		||||
        url = pyotp.totp.TOTP(code).provisioning_uri(username, issuer_name=domain)
 | 
			
		||||
        url = pyotp.totp.TOTP(code).provisioning_uri(
 | 
			
		||||
            username, issuer_name=get_webdomain()
 | 
			
		||||
        )
 | 
			
		||||
        subprocess.run(f'qr "{url}"', shell=True)
 | 
			
		||||
        self.stdout.write(
 | 
			
		||||
            self.style.WARNING("Scan the barcode above with your authenticator app")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,9 @@
 | 
			
		||||
from accounts.models import User
 | 
			
		||||
from getpass import getpass
 | 
			
		||||
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
 | 
			
		||||
from accounts.models import User
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = "Reset password for user"
 | 
			
		||||
@@ -16,7 +19,13 @@ class Command(BaseCommand):
 | 
			
		||||
            self.stdout.write(self.style.ERROR(f"User {username} doesn't exist"))
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        passwd = input("Enter new password: ")
 | 
			
		||||
        user.set_password(passwd)
 | 
			
		||||
        pass1, pass2 = "foo", "bar"
 | 
			
		||||
        while pass1 != pass2:
 | 
			
		||||
            pass1 = getpass()
 | 
			
		||||
            pass2 = getpass(prompt="Confirm Password:")
 | 
			
		||||
            if pass1 != pass2:
 | 
			
		||||
                self.stdout.write(self.style.ERROR("Passwords don't match"))
 | 
			
		||||
 | 
			
		||||
        user.set_password(pass1)
 | 
			
		||||
        user.save()
 | 
			
		||||
        self.stdout.write(self.style.SUCCESS(f"Password for {username} was reset!"))
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,25 @@
 | 
			
		||||
# Generated by Django 4.2.1 on 2023-05-17 07:11
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("accounts", "0031_user_date_format"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="user",
 | 
			
		||||
            name="default_agent_tbl_tab",
 | 
			
		||||
            field=models.CharField(
 | 
			
		||||
                choices=[
 | 
			
		||||
                    ("server", "Servers"),
 | 
			
		||||
                    ("workstation", "Workstations"),
 | 
			
		||||
                    ("mixed", "Mixed"),
 | 
			
		||||
                ],
 | 
			
		||||
                default="mixed",
 | 
			
		||||
                max_length=50,
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,32 @@
 | 
			
		||||
# Generated by Django 4.2.1 on 2023-05-23 04:54
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("accounts", "0032_alter_user_default_agent_tbl_tab"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name="user",
 | 
			
		||||
            name="dash_info_color",
 | 
			
		||||
            field=models.CharField(default="info", max_length=255),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name="user",
 | 
			
		||||
            name="dash_negative_color",
 | 
			
		||||
            field=models.CharField(default="negative", max_length=255),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name="user",
 | 
			
		||||
            name="dash_positive_color",
 | 
			
		||||
            field=models.CharField(default="positive", max_length=255),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name="user",
 | 
			
		||||
            name="dash_warning_color",
 | 
			
		||||
            field=models.CharField(default="warning", max_length=255),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,17 @@
 | 
			
		||||
# Generated by Django 4.1.9 on 2023-05-26 23:59
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("accounts", "0033_user_dash_info_color_user_dash_negative_color_and_more"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name="role",
 | 
			
		||||
            name="can_send_wol",
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,22 @@
 | 
			
		||||
# Generated by Django 4.2.5 on 2023-10-08 22:24
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("accounts", "0034_role_can_send_wol"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name="role",
 | 
			
		||||
            name="can_manage_reports",
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name="role",
 | 
			
		||||
            name="can_view_reports",
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,16 @@
 | 
			
		||||
# Generated by Django 4.2.7 on 2023-11-09 19:57
 | 
			
		||||
 | 
			
		||||
from django.db import migrations
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("accounts", "0035_role_can_manage_reports_role_can_view_reports"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name="role",
 | 
			
		||||
            name="can_ping_agents",
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -1,30 +1,17 @@
 | 
			
		||||
from django.contrib.auth.models import AbstractUser
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.db.models.fields import CharField, DateTimeField
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from logs.models import BaseAuditModel
 | 
			
		||||
 | 
			
		||||
from typing import Optional
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.constants import ROLE_CACHE_PREFIX
 | 
			
		||||
from django.contrib.auth.models import AbstractUser
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.db.models.fields import CharField, DateTimeField
 | 
			
		||||
 | 
			
		||||
AGENT_DBLCLICK_CHOICES = [
 | 
			
		||||
    ("editagent", "Edit Agent"),
 | 
			
		||||
    ("takecontrol", "Take Control"),
 | 
			
		||||
    ("remotebg", "Remote Background"),
 | 
			
		||||
    ("urlaction", "URL Action"),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
AGENT_TBL_TAB_CHOICES = [
 | 
			
		||||
    ("server", "Servers"),
 | 
			
		||||
    ("workstation", "Workstations"),
 | 
			
		||||
    ("mixed", "Mixed"),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
CLIENT_TREE_SORT_CHOICES = [
 | 
			
		||||
    ("alphafail", "Move failing clients to the top"),
 | 
			
		||||
    ("alpha", "Sort alphabetically"),
 | 
			
		||||
]
 | 
			
		||||
from logs.models import BaseAuditModel
 | 
			
		||||
from tacticalrmm.constants import (
 | 
			
		||||
    ROLE_CACHE_PREFIX,
 | 
			
		||||
    AgentDblClick,
 | 
			
		||||
    AgentTableTabs,
 | 
			
		||||
    ClientTreeSort,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class User(AbstractUser, BaseAuditModel):
 | 
			
		||||
@@ -33,8 +20,8 @@ class User(AbstractUser, BaseAuditModel):
 | 
			
		||||
    totp_key = models.CharField(max_length=50, null=True, blank=True)
 | 
			
		||||
    dark_mode = models.BooleanField(default=True)
 | 
			
		||||
    show_community_scripts = models.BooleanField(default=True)
 | 
			
		||||
    agent_dblclick_action = models.CharField(
 | 
			
		||||
        max_length=50, choices=AGENT_DBLCLICK_CHOICES, default="editagent"
 | 
			
		||||
    agent_dblclick_action: "AgentDblClick" = models.CharField(
 | 
			
		||||
        max_length=50, choices=AgentDblClick.choices, default=AgentDblClick.EDIT_AGENT
 | 
			
		||||
    )
 | 
			
		||||
    url_action = models.ForeignKey(
 | 
			
		||||
        "core.URLAction",
 | 
			
		||||
@@ -44,14 +31,18 @@ class User(AbstractUser, BaseAuditModel):
 | 
			
		||||
        on_delete=models.SET_NULL,
 | 
			
		||||
    )
 | 
			
		||||
    default_agent_tbl_tab = models.CharField(
 | 
			
		||||
        max_length=50, choices=AGENT_TBL_TAB_CHOICES, default="server"
 | 
			
		||||
        max_length=50, choices=AgentTableTabs.choices, default=AgentTableTabs.MIXED
 | 
			
		||||
    )
 | 
			
		||||
    agents_per_page = models.PositiveIntegerField(default=50)  # not currently used
 | 
			
		||||
    client_tree_sort = models.CharField(
 | 
			
		||||
        max_length=50, choices=CLIENT_TREE_SORT_CHOICES, default="alphafail"
 | 
			
		||||
        max_length=50, choices=ClientTreeSort.choices, default=ClientTreeSort.ALPHA_FAIL
 | 
			
		||||
    )
 | 
			
		||||
    client_tree_splitter = models.PositiveIntegerField(default=11)
 | 
			
		||||
    loading_bar_color = models.CharField(max_length=255, default="red")
 | 
			
		||||
    dash_info_color = models.CharField(max_length=255, default="info")
 | 
			
		||||
    dash_positive_color = models.CharField(max_length=255, default="positive")
 | 
			
		||||
    dash_negative_color = models.CharField(max_length=255, default="negative")
 | 
			
		||||
    dash_warning_color = models.CharField(max_length=255, default="warning")
 | 
			
		||||
    clear_search_when_switching = models.BooleanField(default=True)
 | 
			
		||||
    date_format = models.CharField(max_length=30, blank=True, null=True)
 | 
			
		||||
    is_installer_user = models.BooleanField(default=False)
 | 
			
		||||
@@ -73,6 +64,15 @@ class User(AbstractUser, BaseAuditModel):
 | 
			
		||||
        on_delete=models.SET_NULL,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def mesh_user_id(self):
 | 
			
		||||
        return f"user//{self.mesh_username}"
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def mesh_username(self):
 | 
			
		||||
        # lower() needed for mesh api
 | 
			
		||||
        return f"{self.username.lower()}___{self.pk}"
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def serialize(user):
 | 
			
		||||
        # serializes the task and returns json
 | 
			
		||||
@@ -104,7 +104,6 @@ class Role(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
    # agents
 | 
			
		||||
    can_list_agents = models.BooleanField(default=False)
 | 
			
		||||
    can_ping_agents = models.BooleanField(default=False)
 | 
			
		||||
    can_use_mesh = models.BooleanField(default=False)
 | 
			
		||||
    can_uninstall_agents = models.BooleanField(default=False)
 | 
			
		||||
    can_update_agents = models.BooleanField(default=False)
 | 
			
		||||
@@ -118,6 +117,7 @@ class Role(BaseAuditModel):
 | 
			
		||||
    can_run_bulk = models.BooleanField(default=False)
 | 
			
		||||
    can_recover_agents = models.BooleanField(default=False)
 | 
			
		||||
    can_list_agent_history = models.BooleanField(default=False)
 | 
			
		||||
    can_send_wol = models.BooleanField(default=False)
 | 
			
		||||
 | 
			
		||||
    # core
 | 
			
		||||
    can_list_notes = models.BooleanField(default=False)
 | 
			
		||||
@@ -194,14 +194,17 @@ class Role(BaseAuditModel):
 | 
			
		||||
    can_list_api_keys = models.BooleanField(default=False)
 | 
			
		||||
    can_manage_api_keys = models.BooleanField(default=False)
 | 
			
		||||
 | 
			
		||||
    # reporting
 | 
			
		||||
    can_view_reports = models.BooleanField(default=False)
 | 
			
		||||
    can_manage_reports = models.BooleanField(default=False)
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.name
 | 
			
		||||
 | 
			
		||||
    def save(self, *args, **kwargs) -> None:
 | 
			
		||||
 | 
			
		||||
        # delete cache on save
 | 
			
		||||
        cache.delete(f"{ROLE_CACHE_PREFIX}{self.name}")
 | 
			
		||||
        super(BaseAuditModel, self).save(*args, **kwargs)
 | 
			
		||||
        super().save(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def serialize(role):
 | 
			
		||||
 
 | 
			
		||||
@@ -7,32 +7,31 @@ class AccountsPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view) -> bool:
 | 
			
		||||
        if r.method == "GET":
 | 
			
		||||
            return _has_perm(r, "can_list_accounts")
 | 
			
		||||
        else:
 | 
			
		||||
 | 
			
		||||
            # allow users to reset their own password/2fa see issue #686
 | 
			
		||||
            base_path = "/accounts/users/"
 | 
			
		||||
            paths = ["reset/", "reset_totp/"]
 | 
			
		||||
        # allow users to reset their own password/2fa see issue #686
 | 
			
		||||
        base_path = "/accounts/users/"
 | 
			
		||||
        paths = ("reset/", "reset_totp/")
 | 
			
		||||
 | 
			
		||||
            if r.path in [base_path + i for i in paths]:
 | 
			
		||||
                from accounts.models import User
 | 
			
		||||
        if r.path in [base_path + i for i in paths]:
 | 
			
		||||
            from accounts.models import User
 | 
			
		||||
 | 
			
		||||
                try:
 | 
			
		||||
                    user = User.objects.get(pk=r.data["id"])
 | 
			
		||||
                except User.DoesNotExist:
 | 
			
		||||
                    pass
 | 
			
		||||
                else:
 | 
			
		||||
                    if user == r.user:
 | 
			
		||||
                        return True
 | 
			
		||||
            try:
 | 
			
		||||
                user = User.objects.get(pk=r.data["id"])
 | 
			
		||||
            except User.DoesNotExist:
 | 
			
		||||
                pass
 | 
			
		||||
            else:
 | 
			
		||||
                if user == r.user:
 | 
			
		||||
                    return True
 | 
			
		||||
 | 
			
		||||
            return _has_perm(r, "can_manage_accounts")
 | 
			
		||||
        return _has_perm(r, "can_manage_accounts")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RolesPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view) -> bool:
 | 
			
		||||
        if r.method == "GET":
 | 
			
		||||
            return _has_perm(r, "can_list_roles")
 | 
			
		||||
        else:
 | 
			
		||||
            return _has_perm(r, "can_manage_roles")
 | 
			
		||||
 | 
			
		||||
        return _has_perm(r, "can_manage_roles")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class APIKeyPerms(permissions.BasePermission):
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,8 @@ from rest_framework.serializers import (
 | 
			
		||||
    SerializerMethodField,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.helpers import get_webdomain
 | 
			
		||||
 | 
			
		||||
from .models import APIKey, Role, User
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -20,6 +22,10 @@ class UserUISerializer(ModelSerializer):
 | 
			
		||||
            "client_tree_sort",
 | 
			
		||||
            "client_tree_splitter",
 | 
			
		||||
            "loading_bar_color",
 | 
			
		||||
            "dash_info_color",
 | 
			
		||||
            "dash_positive_color",
 | 
			
		||||
            "dash_negative_color",
 | 
			
		||||
            "dash_warning_color",
 | 
			
		||||
            "clear_search_when_switching",
 | 
			
		||||
            "block_dashboard_login",
 | 
			
		||||
            "date_format",
 | 
			
		||||
@@ -45,7 +51,6 @@ class UserSerializer(ModelSerializer):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TOTPSetupSerializer(ModelSerializer):
 | 
			
		||||
 | 
			
		||||
    qr_url = SerializerMethodField()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
@@ -58,7 +63,7 @@ class TOTPSetupSerializer(ModelSerializer):
 | 
			
		||||
 | 
			
		||||
    def get_qr_url(self, obj):
 | 
			
		||||
        return pyotp.totp.TOTP(obj.totp_key).provisioning_uri(
 | 
			
		||||
            obj.username, issuer_name="Tactical RMM"
 | 
			
		||||
            obj.username, issuer_name=get_webdomain()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -80,7 +85,6 @@ class RoleAuditSerializer(ModelSerializer):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class APIKeySerializer(ModelSerializer):
 | 
			
		||||
 | 
			
		||||
    username = ReadOnlyField(source="user.username")
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,11 @@
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
 | 
			
		||||
from accounts.models import APIKey, User
 | 
			
		||||
from accounts.serializers import APIKeySerializer
 | 
			
		||||
from django.test import override_settings
 | 
			
		||||
from model_bakery import baker, seq
 | 
			
		||||
 | 
			
		||||
from accounts.models import APIKey, User
 | 
			
		||||
from accounts.serializers import APIKeySerializer
 | 
			
		||||
from tacticalrmm.constants import AgentDblClick, AgentTableTabs, ClientTreeSort
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -196,7 +197,7 @@ class GetUpdateDeleteUser(TacticalTestCase):
 | 
			
		||||
        r = self.client.delete(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        url = f"/accounts/893452/users/"
 | 
			
		||||
        url = "/accounts/893452/users/"
 | 
			
		||||
        r = self.client.delete(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 404)
 | 
			
		||||
 | 
			
		||||
@@ -283,9 +284,9 @@ class TestUserAction(TacticalTestCase):
 | 
			
		||||
        data = {
 | 
			
		||||
            "dark_mode": True,
 | 
			
		||||
            "show_community_scripts": True,
 | 
			
		||||
            "agent_dblclick_action": "editagent",
 | 
			
		||||
            "default_agent_tbl_tab": "mixed",
 | 
			
		||||
            "client_tree_sort": "alpha",
 | 
			
		||||
            "agent_dblclick_action": AgentDblClick.EDIT_AGENT,
 | 
			
		||||
            "default_agent_tbl_tab": AgentTableTabs.MIXED,
 | 
			
		||||
            "client_tree_sort": ClientTreeSort.ALPHA,
 | 
			
		||||
            "client_tree_splitter": 14,
 | 
			
		||||
            "loading_bar_color": "green",
 | 
			
		||||
            "clear_search_when_switching": False,
 | 
			
		||||
@@ -296,6 +297,27 @@ class TestUserAction(TacticalTestCase):
 | 
			
		||||
        self.check_not_authenticated("patch", url)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestUserReset(TacticalTestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        self.setup_coresettings()
 | 
			
		||||
 | 
			
		||||
    def test_reset_pw(self):
 | 
			
		||||
        url = "/accounts/resetpw/"
 | 
			
		||||
        data = {"password": "superSekret123456"}
 | 
			
		||||
        r = self.client.put(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("put", url)
 | 
			
		||||
 | 
			
		||||
    def test_reset_2fa(self):
 | 
			
		||||
        url = "/accounts/reset2fa/"
 | 
			
		||||
        r = self.client.put(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("put", url)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestAPIKeyViews(TacticalTestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.setup_coresettings()
 | 
			
		||||
@@ -338,7 +360,7 @@ class TestAPIKeyViews(TacticalTestCase):
 | 
			
		||||
        resp = self.client.put(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        apikey = APIKey.objects.get(pk=apikey.pk)
 | 
			
		||||
        self.assertEquals(apikey.name, "New Name")
 | 
			
		||||
        self.assertEqual(apikey.name, "New Name")
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("put", url)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -13,4 +13,6 @@ urlpatterns = [
 | 
			
		||||
    path("roles/<int:pk>/", views.GetUpdateDeleteRole.as_view()),
 | 
			
		||||
    path("apikeys/", views.GetAddAPIKeys.as_view()),
 | 
			
		||||
    path("apikeys/<int:pk>/", views.GetUpdateDeleteAPIKey.as_view()),
 | 
			
		||||
    path("resetpw/", views.ResetPass.as_view()),
 | 
			
		||||
    path("reset2fa/", views.Reset2FA.as_view()),
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										24
									
								
								api/tacticalrmm/accounts/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								api/tacticalrmm/accounts/utils.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
			
		||||
from typing import TYPE_CHECKING
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
    from django.http import HttpRequest
 | 
			
		||||
 | 
			
		||||
    from accounts.models import User
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def is_root_user(*, request: "HttpRequest", user: "User") -> bool:
 | 
			
		||||
    root = (
 | 
			
		||||
        hasattr(settings, "ROOT_USER")
 | 
			
		||||
        and request.user != user
 | 
			
		||||
        and user.username == settings.ROOT_USER
 | 
			
		||||
    )
 | 
			
		||||
    demo = (
 | 
			
		||||
        getattr(settings, "DEMO", False) and request.user.username == settings.ROOT_USER
 | 
			
		||||
    )
 | 
			
		||||
    return root or demo
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def is_superuser(user: "User") -> bool:
 | 
			
		||||
    return user.role and getattr(user.role, "is_superuser")
 | 
			
		||||
@@ -3,15 +3,17 @@ from django.conf import settings
 | 
			
		||||
from django.contrib.auth import login
 | 
			
		||||
from django.db import IntegrityError
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from ipware import get_client_ip
 | 
			
		||||
from knox.views import LoginView as KnoxLoginView
 | 
			
		||||
from logs.models import AuditLog
 | 
			
		||||
from python_ipware import IpWare
 | 
			
		||||
from rest_framework.authtoken.serializers import AuthTokenSerializer
 | 
			
		||||
from rest_framework.permissions import AllowAny, IsAuthenticated
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.utils import notify_error
 | 
			
		||||
from accounts.utils import is_root_user
 | 
			
		||||
from core.tasks import sync_mesh_perms_task
 | 
			
		||||
from logs.models import AuditLog
 | 
			
		||||
from tacticalrmm.helpers import notify_error
 | 
			
		||||
 | 
			
		||||
from .models import APIKey, Role, User
 | 
			
		||||
from .permissions import AccountsPerms, APIKeyPerms, RolesPerms
 | 
			
		||||
@@ -24,24 +26,10 @@ from .serializers import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _is_root_user(request, user) -> bool:
 | 
			
		||||
    root = (
 | 
			
		||||
        hasattr(settings, "ROOT_USER")
 | 
			
		||||
        and request.user != user
 | 
			
		||||
        and user.username == settings.ROOT_USER
 | 
			
		||||
    )
 | 
			
		||||
    demo = (
 | 
			
		||||
        getattr(settings, "DEMO", False) and request.user.username == settings.ROOT_USER
 | 
			
		||||
    )
 | 
			
		||||
    return root or demo
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CheckCreds(KnoxLoginView):
 | 
			
		||||
 | 
			
		||||
    permission_classes = (AllowAny,)
 | 
			
		||||
 | 
			
		||||
    def post(self, request, format=None):
 | 
			
		||||
 | 
			
		||||
        # check credentials
 | 
			
		||||
        serializer = AuthTokenSerializer(data=request.data)
 | 
			
		||||
        if not serializer.is_valid():
 | 
			
		||||
@@ -66,7 +54,6 @@ class CheckCreds(KnoxLoginView):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LoginView(KnoxLoginView):
 | 
			
		||||
 | 
			
		||||
    permission_classes = (AllowAny,)
 | 
			
		||||
 | 
			
		||||
    def post(self, request, format=None):
 | 
			
		||||
@@ -93,9 +80,11 @@ class LoginView(KnoxLoginView):
 | 
			
		||||
            login(request, user)
 | 
			
		||||
 | 
			
		||||
            # save ip information
 | 
			
		||||
            client_ip, is_routable = get_client_ip(request)
 | 
			
		||||
            user.last_login_ip = client_ip
 | 
			
		||||
            user.save()
 | 
			
		||||
            ipw = IpWare()
 | 
			
		||||
            client_ip, _ = ipw.get_client_ip(request.META)
 | 
			
		||||
            if client_ip:
 | 
			
		||||
                user.last_login_ip = str(client_ip)
 | 
			
		||||
                user.save()
 | 
			
		||||
 | 
			
		||||
            AuditLog.audit_user_login_successful(
 | 
			
		||||
                request.data["username"], debug_info={"ip": request._client_ip}
 | 
			
		||||
@@ -145,6 +134,7 @@ class GetAddUsers(APIView):
 | 
			
		||||
            user.role = role
 | 
			
		||||
 | 
			
		||||
        user.save()
 | 
			
		||||
        sync_mesh_perms_task.delay()
 | 
			
		||||
        return Response(user.username)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -159,31 +149,33 @@ class GetUpdateDeleteUser(APIView):
 | 
			
		||||
    def put(self, request, pk):
 | 
			
		||||
        user = get_object_or_404(User, pk=pk)
 | 
			
		||||
 | 
			
		||||
        if _is_root_user(request, user):
 | 
			
		||||
        if is_root_user(request=request, user=user):
 | 
			
		||||
            return notify_error("The root user cannot be modified from the UI")
 | 
			
		||||
 | 
			
		||||
        serializer = UserSerializer(instance=user, data=request.data, partial=True)
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        serializer.save()
 | 
			
		||||
        sync_mesh_perms_task.delay()
 | 
			
		||||
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
    def delete(self, request, pk):
 | 
			
		||||
        user = get_object_or_404(User, pk=pk)
 | 
			
		||||
        if _is_root_user(request, user):
 | 
			
		||||
        if is_root_user(request=request, user=user):
 | 
			
		||||
            return notify_error("The root user cannot be deleted from the UI")
 | 
			
		||||
 | 
			
		||||
        user.delete()
 | 
			
		||||
 | 
			
		||||
        sync_mesh_perms_task.delay()
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserActions(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, AccountsPerms]
 | 
			
		||||
 | 
			
		||||
    # reset password
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        user = get_object_or_404(User, pk=request.data["id"])
 | 
			
		||||
        if _is_root_user(request, user):
 | 
			
		||||
        if is_root_user(request=request, user=user):
 | 
			
		||||
            return notify_error("The root user cannot be modified from the UI")
 | 
			
		||||
 | 
			
		||||
        user.set_password(request.data["password"])
 | 
			
		||||
@@ -194,7 +186,7 @@ class UserActions(APIView):
 | 
			
		||||
    # reset two factor token
 | 
			
		||||
    def put(self, request):
 | 
			
		||||
        user = get_object_or_404(User, pk=request.data["id"])
 | 
			
		||||
        if _is_root_user(request, user):
 | 
			
		||||
        if is_root_user(request=request, user=user):
 | 
			
		||||
            return notify_error("The root user cannot be modified from the UI")
 | 
			
		||||
 | 
			
		||||
        user.totp_key = ""
 | 
			
		||||
@@ -206,10 +198,8 @@ class UserActions(APIView):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TOTPSetup(APIView):
 | 
			
		||||
 | 
			
		||||
    # totp setup
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
 | 
			
		||||
        user = request.user
 | 
			
		||||
        if not user.totp_key:
 | 
			
		||||
            code = pyotp.random_base32()
 | 
			
		||||
@@ -256,11 +246,13 @@ class GetUpdateDeleteRole(APIView):
 | 
			
		||||
        serializer = RoleSerializer(instance=role, data=request.data)
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        serializer.save()
 | 
			
		||||
        sync_mesh_perms_task.delay()
 | 
			
		||||
        return Response("Role was edited")
 | 
			
		||||
 | 
			
		||||
    def delete(self, request, pk):
 | 
			
		||||
        role = get_object_or_404(Role, pk=pk)
 | 
			
		||||
        role.delete()
 | 
			
		||||
        sync_mesh_perms_task.delay()
 | 
			
		||||
        return Response("Role was removed")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -278,7 +270,7 @@ class GetAddAPIKeys(APIView):
 | 
			
		||||
        request.data["key"] = get_random_string(length=32).upper()
 | 
			
		||||
        serializer = APIKeySerializer(data=request.data)
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        obj = serializer.save()
 | 
			
		||||
        serializer.save()
 | 
			
		||||
        return Response("The API Key was added")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -301,3 +293,23 @@ class GetUpdateDeleteAPIKey(APIView):
 | 
			
		||||
        apikey = get_object_or_404(APIKey, pk=pk)
 | 
			
		||||
        apikey.delete()
 | 
			
		||||
        return Response("The API Key was deleted")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ResetPass(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
    def put(self, request):
 | 
			
		||||
        user = request.user
 | 
			
		||||
        user.set_password(request.data["password"])
 | 
			
		||||
        user.save()
 | 
			
		||||
        return Response("Password was reset.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Reset2FA(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
    def put(self, request):
 | 
			
		||||
        user = request.user
 | 
			
		||||
        user.totp_key = ""
 | 
			
		||||
        user.save()
 | 
			
		||||
        return Response("2FA was reset. Log out and back in to setup.")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import json
 | 
			
		||||
import os
 | 
			
		||||
import random
 | 
			
		||||
import secrets
 | 
			
		||||
import string
 | 
			
		||||
from itertools import cycle
 | 
			
		||||
 | 
			
		||||
@@ -8,10 +8,11 @@ from django.conf import settings
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from model_bakery.recipe import Recipe, foreign_key, seq
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.constants import AgentMonType, AgentPlat
 | 
			
		||||
 | 
			
		||||
def generate_agent_id(hostname):
 | 
			
		||||
    rand = "".join(random.choice(string.ascii_letters) for _ in range(35))
 | 
			
		||||
    return f"{rand}-{hostname}"
 | 
			
		||||
 | 
			
		||||
def generate_agent_id() -> str:
 | 
			
		||||
    return "".join(secrets.choice(string.ascii_letters) for i in range(39))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
site = Recipe("clients.Site")
 | 
			
		||||
@@ -24,26 +25,34 @@ def get_wmi_data():
 | 
			
		||||
        return json.load(f)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_win_svcs():
 | 
			
		||||
    svcs = settings.BASE_DIR.joinpath("tacticalrmm/test_data/winsvcs.json")
 | 
			
		||||
    with open(svcs) as f:
 | 
			
		||||
        return json.load(f)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
agent = Recipe(
 | 
			
		||||
    "agents.Agent",
 | 
			
		||||
    site=foreign_key(site),
 | 
			
		||||
    hostname="DESKTOP-TEST123",
 | 
			
		||||
    version="1.3.0",
 | 
			
		||||
    monitoring_type=cycle(["workstation", "server"]),
 | 
			
		||||
    agent_id=seq(generate_agent_id("DESKTOP-TEST123")),
 | 
			
		||||
    monitoring_type=cycle(AgentMonType.values),
 | 
			
		||||
    agent_id=seq(generate_agent_id()),
 | 
			
		||||
    last_seen=djangotime.now() - djangotime.timedelta(days=5),
 | 
			
		||||
    plat="windows",
 | 
			
		||||
    plat=AgentPlat.WINDOWS,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
server_agent = agent.extend(
 | 
			
		||||
    monitoring_type="server",
 | 
			
		||||
    monitoring_type=AgentMonType.SERVER,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
workstation_agent = agent.extend(
 | 
			
		||||
    monitoring_type="workstation",
 | 
			
		||||
    monitoring_type=AgentMonType.WORKSTATION,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
online_agent = agent.extend(last_seen=djangotime.now())
 | 
			
		||||
online_agent = agent.extend(
 | 
			
		||||
    last_seen=djangotime.now(), services=get_win_svcs(), wmi_detail=get_wmi_data()
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
offline_agent = agent.extend(
 | 
			
		||||
    last_seen=djangotime.now() - djangotime.timedelta(minutes=7)
 | 
			
		||||
@@ -78,4 +87,4 @@ agent_with_services = agent.extend(
 | 
			
		||||
    ],
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
agent_with_wmi = agent.extend(wmi=get_wmi_data())
 | 
			
		||||
agent_with_wmi = agent.extend(wmi_detail=get_wmi_data())
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										82
									
								
								api/tacticalrmm/agents/consumers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								api/tacticalrmm/agents/consumers.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,82 @@
 | 
			
		||||
from agents.models import Agent, AgentHistory
 | 
			
		||||
from channels.db import database_sync_to_async
 | 
			
		||||
from channels.generic.websocket import AsyncJsonWebsocketConsumer
 | 
			
		||||
from django.contrib.auth.models import AnonymousUser
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from tacticalrmm.constants import AGENT_DEFER, AgentHistoryType
 | 
			
		||||
from tacticalrmm.permissions import _has_perm_on_agent
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SendCMD(AsyncJsonWebsocketConsumer):
 | 
			
		||||
    async def connect(self):
 | 
			
		||||
        self.user = self.scope["user"]
 | 
			
		||||
 | 
			
		||||
        if isinstance(self.user, AnonymousUser):
 | 
			
		||||
            await self.close()
 | 
			
		||||
 | 
			
		||||
        await self.accept()
 | 
			
		||||
 | 
			
		||||
    async def receive_json(self, payload, **kwargs):
 | 
			
		||||
        auth = await self.has_perm(payload["agent_id"])
 | 
			
		||||
        if not auth:
 | 
			
		||||
            await self.send_json(
 | 
			
		||||
                {"ret": "You do not have permission to perform this action."}
 | 
			
		||||
            )
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        agent = await self.get_agent(payload["agent_id"])
 | 
			
		||||
        timeout = int(payload["timeout"])
 | 
			
		||||
        if payload["shell"] == "custom" and payload["custom_shell"]:
 | 
			
		||||
            shell = payload["custom_shell"]
 | 
			
		||||
        else:
 | 
			
		||||
            shell = payload["shell"]
 | 
			
		||||
 | 
			
		||||
        hist_pk = await self.get_history_id(agent, payload["cmd"])
 | 
			
		||||
 | 
			
		||||
        data = {
 | 
			
		||||
            "func": "rawcmd",
 | 
			
		||||
            "timeout": timeout,
 | 
			
		||||
            "payload": {
 | 
			
		||||
                "command": payload["cmd"],
 | 
			
		||||
                "shell": shell,
 | 
			
		||||
            },
 | 
			
		||||
            "id": hist_pk,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ret = await agent.nats_cmd(data, timeout=timeout + 2)
 | 
			
		||||
        await self.send_json({"ret": ret})
 | 
			
		||||
 | 
			
		||||
    async def disconnect(self, _):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def _has_perm(self, perm: str) -> bool:
 | 
			
		||||
        if self.user.is_superuser or (
 | 
			
		||||
            self.user.role and getattr(self.user.role, "is_superuser")
 | 
			
		||||
        ):
 | 
			
		||||
            return True
 | 
			
		||||
 | 
			
		||||
        # make sure non-superusers with empty roles aren't permitted
 | 
			
		||||
        elif not self.user.role:
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
        return self.user.role and getattr(self.user.role, perm)
 | 
			
		||||
 | 
			
		||||
    @database_sync_to_async  # type: ignore
 | 
			
		||||
    def get_agent(self, agent_id: str) -> "Agent":
 | 
			
		||||
        return get_object_or_404(Agent.objects.defer(*AGENT_DEFER), agent_id=agent_id)
 | 
			
		||||
 | 
			
		||||
    @database_sync_to_async  # type: ignore
 | 
			
		||||
    def get_history_id(self, agent: "Agent", cmd: str) -> int:
 | 
			
		||||
        hist = AgentHistory.objects.create(
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            type=AgentHistoryType.CMD_RUN,
 | 
			
		||||
            command=cmd,
 | 
			
		||||
            username=self.user.username[:50],
 | 
			
		||||
        )
 | 
			
		||||
        return hist.pk
 | 
			
		||||
 | 
			
		||||
    @database_sync_to_async  # type: ignore
 | 
			
		||||
    def has_perm(self, agent_id: str) -> bool:
 | 
			
		||||
        return self._has_perm("can_send_cmd") and _has_perm_on_agent(
 | 
			
		||||
            self.user, agent_id
 | 
			
		||||
        )
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from clients.models import Client, Site
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
 
 | 
			
		||||
@@ -1,16 +1,16 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from packaging import version as pyver
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from tacticalrmm.constants import AGENT_DEFER
 | 
			
		||||
from tacticalrmm.utils import reload_nats
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = "Delete old agents"
 | 
			
		||||
    help = "Delete multiple agents based on criteria"
 | 
			
		||||
 | 
			
		||||
    def add_arguments(self, parser):
 | 
			
		||||
        parser.add_argument(
 | 
			
		||||
@@ -23,6 +23,21 @@ class Command(BaseCommand):
 | 
			
		||||
            type=str,
 | 
			
		||||
            help="Delete agents that equal to or less than this version",
 | 
			
		||||
        )
 | 
			
		||||
        parser.add_argument(
 | 
			
		||||
            "--site",
 | 
			
		||||
            type=str,
 | 
			
		||||
            help="Delete agents that belong to the specified site",
 | 
			
		||||
        )
 | 
			
		||||
        parser.add_argument(
 | 
			
		||||
            "--client",
 | 
			
		||||
            type=str,
 | 
			
		||||
            help="Delete agents that belong to the specified client",
 | 
			
		||||
        )
 | 
			
		||||
        parser.add_argument(
 | 
			
		||||
            "--hostname",
 | 
			
		||||
            type=str,
 | 
			
		||||
            help="Delete agents with hostname starting with argument",
 | 
			
		||||
        )
 | 
			
		||||
        parser.add_argument(
 | 
			
		||||
            "--delete",
 | 
			
		||||
            action="store_true",
 | 
			
		||||
@@ -32,25 +47,40 @@ class Command(BaseCommand):
 | 
			
		||||
    def handle(self, *args, **kwargs):
 | 
			
		||||
        days = kwargs["days"]
 | 
			
		||||
        agentver = kwargs["agentver"]
 | 
			
		||||
        site = kwargs["site"]
 | 
			
		||||
        client = kwargs["client"]
 | 
			
		||||
        hostname = kwargs["hostname"]
 | 
			
		||||
        delete = kwargs["delete"]
 | 
			
		||||
 | 
			
		||||
        if not days and not agentver:
 | 
			
		||||
        if not days and not agentver and not site and not client and not hostname:
 | 
			
		||||
            self.stdout.write(
 | 
			
		||||
                self.style.ERROR("Must have at least one parameter: days or agentver")
 | 
			
		||||
                self.style.ERROR(
 | 
			
		||||
                    "Must have at least one parameter: days, agentver, site, client or hostname"
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        q = Agent.objects.defer(*AGENT_DEFER)
 | 
			
		||||
        agents = Agent.objects.select_related("site__client").defer(*AGENT_DEFER)
 | 
			
		||||
 | 
			
		||||
        agents = []
 | 
			
		||||
        if days:
 | 
			
		||||
            overdue = djangotime.now() - djangotime.timedelta(days=days)
 | 
			
		||||
            agents = [i for i in q if i.last_seen < overdue]
 | 
			
		||||
            agents = agents.filter(last_seen__lt=overdue)
 | 
			
		||||
 | 
			
		||||
        if site:
 | 
			
		||||
            agents = agents.filter(site__name=site)
 | 
			
		||||
 | 
			
		||||
        if client:
 | 
			
		||||
            agents = agents.filter(site__client__name=client)
 | 
			
		||||
 | 
			
		||||
        if hostname:
 | 
			
		||||
            agents = agents.filter(hostname__istartswith=hostname)
 | 
			
		||||
 | 
			
		||||
        if agentver:
 | 
			
		||||
            agents = [i for i in q if pyver.parse(i.version) <= pyver.parse(agentver)]
 | 
			
		||||
            agents = [
 | 
			
		||||
                i for i in agents if pyver.parse(i.version) <= pyver.parse(agentver)
 | 
			
		||||
            ]
 | 
			
		||||
 | 
			
		||||
        if not agents:
 | 
			
		||||
        if len(agents) == 0:
 | 
			
		||||
            self.stdout.write(self.style.ERROR("No agents matched"))
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
@@ -64,7 +94,7 @@ class Command(BaseCommand):
 | 
			
		||||
                try:
 | 
			
		||||
                    agent.delete()
 | 
			
		||||
                except Exception as e:
 | 
			
		||||
                    err = f"Failed to delete agent {agent.hostname}: {str(e)}"
 | 
			
		||||
                    err = f"Failed to delete agent {agent.hostname}: {e}"
 | 
			
		||||
                    self.stdout.write(self.style.ERROR(err))
 | 
			
		||||
                else:
 | 
			
		||||
                    deleted_count += 1
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +1,17 @@
 | 
			
		||||
# import datetime as dt
 | 
			
		||||
import random
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from core.tasks import cache_db_fields_task, handle_resolved_stuff
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from core.tasks import cache_db_fields_task
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = "stuff for demo site in cron"
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args, **kwargs):
 | 
			
		||||
 | 
			
		||||
        random_dates = []
 | 
			
		||||
        now = djangotime.now()
 | 
			
		||||
 | 
			
		||||
@@ -23,18 +23,9 @@ class Command(BaseCommand):
 | 
			
		||||
            rand = now - djangotime.timedelta(minutes=random.randint(10, 20))
 | 
			
		||||
            random_dates.append(rand)
 | 
			
		||||
 | 
			
		||||
        """ for _ in range(5):
 | 
			
		||||
            rand = djangotime.now() - djangotime.timedelta(hours=random.randint(1, 10))
 | 
			
		||||
            random_dates.append(rand)
 | 
			
		||||
 | 
			
		||||
        for _ in range(5):
 | 
			
		||||
            rand = djangotime.now() - djangotime.timedelta(days=random.randint(40, 90))
 | 
			
		||||
            random_dates.append(rand) """
 | 
			
		||||
 | 
			
		||||
        agents = Agent.objects.only("last_seen")
 | 
			
		||||
        for agent in agents:
 | 
			
		||||
            agent.last_seen = random.choice(random_dates)
 | 
			
		||||
            agent.save(update_fields=["last_seen"])
 | 
			
		||||
 | 
			
		||||
        cache_db_fields_task()
 | 
			
		||||
        handle_resolved_stuff()
 | 
			
		||||
 
 | 
			
		||||
@@ -3,28 +3,55 @@ import json
 | 
			
		||||
import random
 | 
			
		||||
import string
 | 
			
		||||
 | 
			
		||||
from accounts.models import User
 | 
			
		||||
from agents.models import Agent, AgentHistory
 | 
			
		||||
from automation.models import Policy
 | 
			
		||||
from autotasks.models import AutomatedTask, TaskResult
 | 
			
		||||
from checks.models import Check, CheckResult, CheckHistory
 | 
			
		||||
from clients.models import Client, Site
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.core.management import call_command
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
 | 
			
		||||
from accounts.models import User
 | 
			
		||||
from agents.models import Agent, AgentHistory
 | 
			
		||||
from automation.models import Policy
 | 
			
		||||
from autotasks.models import AutomatedTask, TaskResult
 | 
			
		||||
from checks.models import Check, CheckHistory, CheckResult
 | 
			
		||||
from clients.models import Client, Site
 | 
			
		||||
from logs.models import AuditLog, PendingAction
 | 
			
		||||
from scripts.models import Script
 | 
			
		||||
from software.models import InstalledSoftware
 | 
			
		||||
from winupdate.models import WinUpdate, WinUpdatePolicy
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.constants import (
 | 
			
		||||
    AgentHistoryType,
 | 
			
		||||
    AgentMonType,
 | 
			
		||||
    AgentPlat,
 | 
			
		||||
    AlertSeverity,
 | 
			
		||||
    CheckStatus,
 | 
			
		||||
    CheckType,
 | 
			
		||||
    EvtLogFailWhen,
 | 
			
		||||
    EvtLogNames,
 | 
			
		||||
    EvtLogTypes,
 | 
			
		||||
    GoArch,
 | 
			
		||||
    PAAction,
 | 
			
		||||
    ScriptShell,
 | 
			
		||||
    TaskSyncStatus,
 | 
			
		||||
    TaskType,
 | 
			
		||||
)
 | 
			
		||||
from tacticalrmm.demo_data import (
 | 
			
		||||
    check_network_loc_aware_ps1,
 | 
			
		||||
    check_storage_pool_health_ps1,
 | 
			
		||||
    clear_print_spool_bat,
 | 
			
		||||
    disks,
 | 
			
		||||
    disks_linux_deb,
 | 
			
		||||
    disks_linux_pi,
 | 
			
		||||
    ping_fail_output,
 | 
			
		||||
    ping_success_output,
 | 
			
		||||
    restart_nla_ps1,
 | 
			
		||||
    show_temp_dir_py,
 | 
			
		||||
    spooler_stdout,
 | 
			
		||||
    temp_dir_stdout,
 | 
			
		||||
    wmi_deb,
 | 
			
		||||
    wmi_pi,
 | 
			
		||||
    wmi_mac,
 | 
			
		||||
    disks_mac,
 | 
			
		||||
)
 | 
			
		||||
from winupdate.models import WinUpdate, WinUpdatePolicy
 | 
			
		||||
 | 
			
		||||
AGENTS_TO_GENERATE = 250
 | 
			
		||||
 | 
			
		||||
@@ -43,19 +70,18 @@ EVT_LOG_FAIL = settings.BASE_DIR.joinpath(
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = "populate database with fake agents"
 | 
			
		||||
 | 
			
		||||
    def rand_string(self, length):
 | 
			
		||||
    def rand_string(self, length: int) -> str:
 | 
			
		||||
        chars = string.ascii_letters
 | 
			
		||||
        return "".join(random.choice(chars) for _ in range(length))
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args, **kwargs):
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args, **kwargs) -> None:
 | 
			
		||||
        user = User.objects.first()
 | 
			
		||||
        if user:
 | 
			
		||||
            user.totp_key = "ABSA234234"
 | 
			
		||||
            user.save(update_fields=["totp_key"])
 | 
			
		||||
 | 
			
		||||
        Client.objects.all().delete()
 | 
			
		||||
        Agent.objects.all().delete()
 | 
			
		||||
        Client.objects.all().delete()
 | 
			
		||||
        Check.objects.all().delete()
 | 
			
		||||
        Script.objects.all().delete()
 | 
			
		||||
        AutomatedTask.objects.all().delete()
 | 
			
		||||
@@ -65,6 +91,9 @@ class Command(BaseCommand):
 | 
			
		||||
        PendingAction.objects.all().delete()
 | 
			
		||||
 | 
			
		||||
        call_command("load_community_scripts")
 | 
			
		||||
        call_command("initial_db_setup")
 | 
			
		||||
        call_command("load_chocos")
 | 
			
		||||
        call_command("create_installer_user")
 | 
			
		||||
 | 
			
		||||
        # policies
 | 
			
		||||
        check_policy = Policy()
 | 
			
		||||
@@ -95,27 +124,27 @@ class Command(BaseCommand):
 | 
			
		||||
        update_policy.email_if_fail = True
 | 
			
		||||
        update_policy.save()
 | 
			
		||||
 | 
			
		||||
        clients = [
 | 
			
		||||
        clients = (
 | 
			
		||||
            "Company 1",
 | 
			
		||||
            "Company 2",
 | 
			
		||||
            "Company 3",
 | 
			
		||||
            "Company 1",
 | 
			
		||||
            "Company 4",
 | 
			
		||||
            "Company 5",
 | 
			
		||||
            "Company 6",
 | 
			
		||||
        ]
 | 
			
		||||
        sites1 = ["HQ1", "LA Office 1", "NY Office 1"]
 | 
			
		||||
        sites2 = ["HQ2", "LA Office 2", "NY Office 2"]
 | 
			
		||||
        sites3 = ["HQ3", "LA Office 3", "NY Office 3"]
 | 
			
		||||
        sites4 = ["HQ4", "LA Office 4", "NY Office 4"]
 | 
			
		||||
        sites5 = ["HQ5", "LA Office 5", "NY Office 5"]
 | 
			
		||||
        sites6 = ["HQ6", "LA Office 6", "NY Office 6"]
 | 
			
		||||
        )
 | 
			
		||||
        sites1 = ("HQ1", "LA Office 1", "NY Office 1")
 | 
			
		||||
        sites2 = ("HQ2", "LA Office 2", "NY Office 2")
 | 
			
		||||
        sites3 = ("HQ3", "LA Office 3", "NY Office 3")
 | 
			
		||||
        sites4 = ("HQ4", "LA Office 4", "NY Office 4")
 | 
			
		||||
        sites5 = ("HQ5", "LA Office 5", "NY Office 5")
 | 
			
		||||
        sites6 = ("HQ6", "LA Office 6", "NY Office 6")
 | 
			
		||||
 | 
			
		||||
        client1 = Client(name="Company 1")
 | 
			
		||||
        client2 = Client(name="Company 2")
 | 
			
		||||
        client3 = Client(name="Company 3")
 | 
			
		||||
        client4 = Client(name="Company 4")
 | 
			
		||||
        client5 = Client(name="Company 5")
 | 
			
		||||
        client6 = Client(name="Company 6")
 | 
			
		||||
        client1 = Client(name=clients[0])
 | 
			
		||||
        client2 = Client(name=clients[1])
 | 
			
		||||
        client3 = Client(name=clients[2])
 | 
			
		||||
        client4 = Client(name=clients[3])
 | 
			
		||||
        client5 = Client(name=clients[4])
 | 
			
		||||
        client6 = Client(name=clients[5])
 | 
			
		||||
 | 
			
		||||
        client1.save()
 | 
			
		||||
        client2.save()
 | 
			
		||||
@@ -142,7 +171,7 @@ class Command(BaseCommand):
 | 
			
		||||
        for site in sites6:
 | 
			
		||||
            Site(client=client6, name=site).save()
 | 
			
		||||
 | 
			
		||||
        hostnames = [
 | 
			
		||||
        hostnames = (
 | 
			
		||||
            "DC-1",
 | 
			
		||||
            "DC-2",
 | 
			
		||||
            "FSV-1",
 | 
			
		||||
@@ -150,26 +179,33 @@ class Command(BaseCommand):
 | 
			
		||||
            "WSUS",
 | 
			
		||||
            "DESKTOP-12345",
 | 
			
		||||
            "LAPTOP-55443",
 | 
			
		||||
        ]
 | 
			
		||||
        descriptions = ["Bob's computer", "Primary DC", "File Server", "Karen's Laptop"]
 | 
			
		||||
        modes = ["server", "workstation"]
 | 
			
		||||
        op_systems_servers = [
 | 
			
		||||
            "db-aws-01",
 | 
			
		||||
            "Karens-MacBook-Air.local",
 | 
			
		||||
        )
 | 
			
		||||
        descriptions = ("Bob's computer", "Primary DC", "File Server", "Karen's Laptop")
 | 
			
		||||
        modes = AgentMonType.values
 | 
			
		||||
        op_systems_servers = (
 | 
			
		||||
            "Microsoft Windows Server 2016 Standard, 64bit (build 14393)",
 | 
			
		||||
            "Microsoft Windows Server 2012 R2 Standard, 64bit (build 9600)",
 | 
			
		||||
            "Microsoft Windows Server 2019 Standard, 64bit (build 17763)",
 | 
			
		||||
        ]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        op_systems_workstations = [
 | 
			
		||||
        op_systems_workstations = (
 | 
			
		||||
            "Microsoft Windows 8.1 Pro, 64bit (build 9600)",
 | 
			
		||||
            "Microsoft Windows 10 Pro for Workstations, 64bit (build 18363)",
 | 
			
		||||
            "Microsoft Windows 10 Pro, 64bit (build 18363)",
 | 
			
		||||
        ]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        public_ips = ["65.234.22.4", "74.123.43.5", "44.21.134.45"]
 | 
			
		||||
        linux_deb_os = "Debian 11.2 x86_64 5.10.0-11-amd64"
 | 
			
		||||
        linux_pi_os = "Raspbian 11.2 armv7l 5.10.92-v7+"
 | 
			
		||||
        mac_os = "Darwin 12.5.1 arm64 21.6.0"
 | 
			
		||||
 | 
			
		||||
        total_rams = [4, 8, 16, 32, 64, 128]
 | 
			
		||||
        public_ips = ("65.234.22.4", "74.123.43.5", "44.21.134.45")
 | 
			
		||||
 | 
			
		||||
        total_rams = (4, 8, 16, 32, 64, 128)
 | 
			
		||||
 | 
			
		||||
        now = dt.datetime.now()
 | 
			
		||||
        django_now = djangotime.now()
 | 
			
		||||
 | 
			
		||||
        boot_times = []
 | 
			
		||||
 | 
			
		||||
@@ -181,7 +217,7 @@ class Command(BaseCommand):
 | 
			
		||||
            rand_days = now - dt.timedelta(days=random.randint(2, 50))
 | 
			
		||||
            boot_times.append(str(rand_days.timestamp()))
 | 
			
		||||
 | 
			
		||||
        user_names = ["None", "Karen", "Steve", "jsmith", "jdoe"]
 | 
			
		||||
        user_names = ("None", "Karen", "Steve", "jsmith", "jdoe")
 | 
			
		||||
 | 
			
		||||
        with open(SVCS) as f:
 | 
			
		||||
            services = json.load(f)
 | 
			
		||||
@@ -196,10 +232,7 @@ class Command(BaseCommand):
 | 
			
		||||
        with open(WMI_3) as f:
 | 
			
		||||
            wmi3 = json.load(f)
 | 
			
		||||
 | 
			
		||||
        wmi_details = []
 | 
			
		||||
        wmi_details.append(wmi1)
 | 
			
		||||
        wmi_details.append(wmi2)
 | 
			
		||||
        wmi_details.append(wmi3)
 | 
			
		||||
        wmi_details = [i for i in (wmi1, wmi2, wmi3)]
 | 
			
		||||
 | 
			
		||||
        # software
 | 
			
		||||
        with open(SW_1) as f:
 | 
			
		||||
@@ -208,9 +241,7 @@ class Command(BaseCommand):
 | 
			
		||||
        with open(SW_2) as f:
 | 
			
		||||
            software2 = json.load(f)
 | 
			
		||||
 | 
			
		||||
        softwares = []
 | 
			
		||||
        softwares.append(software1)
 | 
			
		||||
        softwares.append(software2)
 | 
			
		||||
        softwares = [i for i in (software1, software2)]
 | 
			
		||||
 | 
			
		||||
        # windows updates
 | 
			
		||||
        with open(WIN_UPDATES) as f:
 | 
			
		||||
@@ -226,74 +257,103 @@ class Command(BaseCommand):
 | 
			
		||||
        clear_spool.name = "Clear Print Spooler"
 | 
			
		||||
        clear_spool.description = "clears the print spooler. Fuck printers"
 | 
			
		||||
        clear_spool.filename = "clear_print_spool.bat"
 | 
			
		||||
        clear_spool.shell = "cmd"
 | 
			
		||||
        clear_spool.shell = ScriptShell.CMD
 | 
			
		||||
        clear_spool.script_body = clear_print_spool_bat
 | 
			
		||||
        clear_spool.save()
 | 
			
		||||
 | 
			
		||||
        check_net_aware = Script()
 | 
			
		||||
        check_net_aware.name = "Check Network Location Awareness"
 | 
			
		||||
        check_net_aware.description = "Check's network location awareness on domain computers, should always be domain profile and not public or private. Sometimes happens when computer restarts before domain available. This script will return 0 if check passes or 1 if it fails."
 | 
			
		||||
        check_net_aware.filename = "check_network_loc_aware.ps1"
 | 
			
		||||
        check_net_aware.shell = "powershell"
 | 
			
		||||
        check_net_aware.shell = ScriptShell.POWERSHELL
 | 
			
		||||
        check_net_aware.script_body = check_network_loc_aware_ps1
 | 
			
		||||
        check_net_aware.save()
 | 
			
		||||
 | 
			
		||||
        check_pool_health = Script()
 | 
			
		||||
        check_pool_health.name = "Check storage spool health"
 | 
			
		||||
        check_pool_health.description = "loops through all storage pools and will fail if any of them are not healthy"
 | 
			
		||||
        check_pool_health.filename = "check_storage_pool_health.ps1"
 | 
			
		||||
        check_pool_health.shell = "powershell"
 | 
			
		||||
        check_pool_health.shell = ScriptShell.POWERSHELL
 | 
			
		||||
        check_pool_health.script_body = check_storage_pool_health_ps1
 | 
			
		||||
        check_pool_health.save()
 | 
			
		||||
 | 
			
		||||
        restart_nla = Script()
 | 
			
		||||
        restart_nla.name = "Restart NLA Service"
 | 
			
		||||
        restart_nla.description = "restarts the Network Location Awareness windows service to fix the nic profile. Run this after the check network service fails"
 | 
			
		||||
        restart_nla.filename = "restart_nla.ps1"
 | 
			
		||||
        restart_nla.shell = "powershell"
 | 
			
		||||
        restart_nla.shell = ScriptShell.POWERSHELL
 | 
			
		||||
        restart_nla.script_body = restart_nla_ps1
 | 
			
		||||
        restart_nla.save()
 | 
			
		||||
 | 
			
		||||
        show_tmp_dir_script = Script()
 | 
			
		||||
        show_tmp_dir_script.name = "Check temp dir"
 | 
			
		||||
        show_tmp_dir_script.description = "shows files in temp dir using python"
 | 
			
		||||
        show_tmp_dir_script.filename = "show_temp_dir.py"
 | 
			
		||||
        show_tmp_dir_script.shell = "python"
 | 
			
		||||
        show_tmp_dir_script.shell = ScriptShell.PYTHON
 | 
			
		||||
        show_tmp_dir_script.script_body = show_temp_dir_py
 | 
			
		||||
        show_tmp_dir_script.save()
 | 
			
		||||
 | 
			
		||||
        for count_agents in range(AGENTS_TO_GENERATE):
 | 
			
		||||
 | 
			
		||||
            client = random.choice(clients)
 | 
			
		||||
 | 
			
		||||
            if client == "Company 1":
 | 
			
		||||
            if client == clients[0]:
 | 
			
		||||
                site = random.choice(sites1)
 | 
			
		||||
            elif client == "Company 2":
 | 
			
		||||
            elif client == clients[1]:
 | 
			
		||||
                site = random.choice(sites2)
 | 
			
		||||
            elif client == "Company 3":
 | 
			
		||||
            elif client == clients[2]:
 | 
			
		||||
                site = random.choice(sites3)
 | 
			
		||||
            elif client == "Company 4":
 | 
			
		||||
            elif client == clients[3]:
 | 
			
		||||
                site = random.choice(sites4)
 | 
			
		||||
            elif client == "Company 5":
 | 
			
		||||
            elif client == clients[4]:
 | 
			
		||||
                site = random.choice(sites5)
 | 
			
		||||
            elif client == "Company 6":
 | 
			
		||||
            elif client == clients[5]:
 | 
			
		||||
                site = random.choice(sites6)
 | 
			
		||||
            else:
 | 
			
		||||
                site = None
 | 
			
		||||
 | 
			
		||||
            agent = Agent()
 | 
			
		||||
 | 
			
		||||
            mode = random.choice(modes)
 | 
			
		||||
            if mode == "server":
 | 
			
		||||
                agent.operating_system = random.choice(op_systems_servers)
 | 
			
		||||
            plat_pick = random.randint(1, 15)
 | 
			
		||||
            if plat_pick in (7, 11):
 | 
			
		||||
                agent.plat = AgentPlat.LINUX
 | 
			
		||||
                mode = AgentMonType.SERVER
 | 
			
		||||
                # pi arm
 | 
			
		||||
                if plat_pick == 7:
 | 
			
		||||
                    agent.goarch = GoArch.ARM32
 | 
			
		||||
                    agent.wmi_detail = wmi_pi
 | 
			
		||||
                    agent.disks = disks_linux_pi
 | 
			
		||||
                    agent.operating_system = linux_pi_os
 | 
			
		||||
                else:
 | 
			
		||||
                    agent.goarch = GoArch.AMD64
 | 
			
		||||
                    agent.wmi_detail = wmi_deb
 | 
			
		||||
                    agent.disks = disks_linux_deb
 | 
			
		||||
                    agent.operating_system = linux_deb_os
 | 
			
		||||
            elif plat_pick in (4, 14):
 | 
			
		||||
                agent.plat = AgentPlat.DARWIN
 | 
			
		||||
                mode = random.choice([AgentMonType.SERVER, AgentMonType.WORKSTATION])
 | 
			
		||||
                agent.goarch = GoArch.ARM64
 | 
			
		||||
                agent.wmi_detail = wmi_mac
 | 
			
		||||
                agent.disks = disks_mac
 | 
			
		||||
                agent.operating_system = mac_os
 | 
			
		||||
            else:
 | 
			
		||||
                agent.operating_system = random.choice(op_systems_workstations)
 | 
			
		||||
                agent.plat = AgentPlat.WINDOWS
 | 
			
		||||
                agent.goarch = GoArch.AMD64
 | 
			
		||||
                mode = random.choice(modes)
 | 
			
		||||
                agent.wmi_detail = random.choice(wmi_details)
 | 
			
		||||
                agent.services = services
 | 
			
		||||
                agent.disks = random.choice(disks)
 | 
			
		||||
                if mode == AgentMonType.SERVER:
 | 
			
		||||
                    agent.operating_system = random.choice(op_systems_servers)
 | 
			
		||||
                else:
 | 
			
		||||
                    agent.operating_system = random.choice(op_systems_workstations)
 | 
			
		||||
 | 
			
		||||
            agent.hostname = random.choice(hostnames)
 | 
			
		||||
            agent.version = settings.LATEST_AGENT_VER
 | 
			
		||||
            agent.hostname = random.choice(hostnames)
 | 
			
		||||
            agent.site = Site.objects.get(name=site)
 | 
			
		||||
            agent.agent_id = self.rand_string(25)
 | 
			
		||||
            agent.agent_id = self.rand_string(40)
 | 
			
		||||
            agent.description = random.choice(descriptions)
 | 
			
		||||
            agent.monitoring_type = mode
 | 
			
		||||
            agent.public_ip = random.choice(public_ips)
 | 
			
		||||
            agent.last_seen = djangotime.now()
 | 
			
		||||
            agent.plat = "windows"
 | 
			
		||||
            agent.plat_release = "windows-2019Server"
 | 
			
		||||
            agent.last_seen = django_now
 | 
			
		||||
 | 
			
		||||
            agent.total_ram = random.choice(total_rams)
 | 
			
		||||
            agent.boot_time = random.choice(boot_times)
 | 
			
		||||
            agent.logged_in_username = random.choice(user_names)
 | 
			
		||||
@@ -303,40 +363,36 @@ class Command(BaseCommand):
 | 
			
		||||
            agent.overdue_email_alert = random.choice([True, False])
 | 
			
		||||
            agent.overdue_text_alert = random.choice([True, False])
 | 
			
		||||
            agent.needs_reboot = random.choice([True, False])
 | 
			
		||||
            agent.wmi_detail = random.choice(wmi_details)
 | 
			
		||||
            agent.services = services
 | 
			
		||||
            agent.disks = random.choice(disks)
 | 
			
		||||
 | 
			
		||||
            agent.save()
 | 
			
		||||
 | 
			
		||||
            InstalledSoftware(agent=agent, software=random.choice(softwares)).save()
 | 
			
		||||
            if agent.plat == AgentPlat.WINDOWS:
 | 
			
		||||
                InstalledSoftware(agent=agent, software=random.choice(softwares)).save()
 | 
			
		||||
 | 
			
		||||
            if mode == "workstation":
 | 
			
		||||
            if mode == AgentMonType.WORKSTATION:
 | 
			
		||||
                WinUpdatePolicy(agent=agent, run_time_days=[5, 6]).save()
 | 
			
		||||
            else:
 | 
			
		||||
                WinUpdatePolicy(agent=agent).save()
 | 
			
		||||
 | 
			
		||||
            # windows updates load
 | 
			
		||||
            guids = []
 | 
			
		||||
            for k in windows_updates.keys():
 | 
			
		||||
                guids.append(k)
 | 
			
		||||
 | 
			
		||||
            for i in guids:
 | 
			
		||||
                WinUpdate(
 | 
			
		||||
                    agent=agent,
 | 
			
		||||
                    guid=i,
 | 
			
		||||
                    kb=windows_updates[i]["KBs"][0],
 | 
			
		||||
                    title=windows_updates[i]["Title"],
 | 
			
		||||
                    installed=windows_updates[i]["Installed"],
 | 
			
		||||
                    downloaded=windows_updates[i]["Downloaded"],
 | 
			
		||||
                    description=windows_updates[i]["Description"],
 | 
			
		||||
                    severity=windows_updates[i]["Severity"],
 | 
			
		||||
                ).save()
 | 
			
		||||
            if agent.plat == AgentPlat.WINDOWS:
 | 
			
		||||
                # windows updates load
 | 
			
		||||
                guids = [i for i in windows_updates.keys()]
 | 
			
		||||
                for i in guids:
 | 
			
		||||
                    WinUpdate(
 | 
			
		||||
                        agent=agent,
 | 
			
		||||
                        guid=i,
 | 
			
		||||
                        kb=windows_updates[i]["KBs"][0],
 | 
			
		||||
                        title=windows_updates[i]["Title"],
 | 
			
		||||
                        installed=windows_updates[i]["Installed"],
 | 
			
		||||
                        downloaded=windows_updates[i]["Downloaded"],
 | 
			
		||||
                        description=windows_updates[i]["Description"],
 | 
			
		||||
                        severity=windows_updates[i]["Severity"],
 | 
			
		||||
                    ).save()
 | 
			
		||||
 | 
			
		||||
            # agent histories
 | 
			
		||||
            hist = AgentHistory()
 | 
			
		||||
            hist.agent = agent
 | 
			
		||||
            hist.type = "cmd_run"
 | 
			
		||||
            hist.type = AgentHistoryType.CMD_RUN
 | 
			
		||||
            hist.command = "ping google.com"
 | 
			
		||||
            hist.username = "demo"
 | 
			
		||||
            hist.results = ping_success_output
 | 
			
		||||
@@ -344,7 +400,7 @@ class Command(BaseCommand):
 | 
			
		||||
 | 
			
		||||
            hist1 = AgentHistory()
 | 
			
		||||
            hist1.agent = agent
 | 
			
		||||
            hist1.type = "script_run"
 | 
			
		||||
            hist1.type = AgentHistoryType.SCRIPT_RUN
 | 
			
		||||
            hist1.script = clear_spool
 | 
			
		||||
            hist1.script_results = {
 | 
			
		||||
                "id": 1,
 | 
			
		||||
@@ -355,50 +411,57 @@ class Command(BaseCommand):
 | 
			
		||||
            }
 | 
			
		||||
            hist1.save()
 | 
			
		||||
 | 
			
		||||
            # disk space check
 | 
			
		||||
            check1 = Check()
 | 
			
		||||
            check_result1 = CheckResult(assigned_check=check1, agent=agent)
 | 
			
		||||
            check1.agent = agent
 | 
			
		||||
            check1.check_type = "diskspace"
 | 
			
		||||
            check_result1.status = "passing"
 | 
			
		||||
            check_result1.last_run = djangotime.now()
 | 
			
		||||
            check_result1.more_info = "Total: 498.7GB, Free: 287.4GB"
 | 
			
		||||
            check_result1.save()
 | 
			
		||||
            if agent.plat == AgentPlat.WINDOWS:
 | 
			
		||||
                # disk space check
 | 
			
		||||
                check1 = Check()
 | 
			
		||||
                check1.agent = agent
 | 
			
		||||
                check1.check_type = CheckType.DISK_SPACE
 | 
			
		||||
                check1.warning_threshold = 25
 | 
			
		||||
                check1.error_threshold = 10
 | 
			
		||||
                check1.disk = "C:"
 | 
			
		||||
                check1.email_alert = random.choice([True, False])
 | 
			
		||||
                check1.text_alert = random.choice([True, False])
 | 
			
		||||
                check1.save()
 | 
			
		||||
 | 
			
		||||
            check1.warning_threshold = 25
 | 
			
		||||
            check1.error_threshold = 10
 | 
			
		||||
            check1.disk = "C:"
 | 
			
		||||
            check1.email_alert = random.choice([True, False])
 | 
			
		||||
            check1.text_alert = random.choice([True, False])
 | 
			
		||||
            check1.save()
 | 
			
		||||
                check_result1 = CheckResult()
 | 
			
		||||
                check_result1.agent = agent
 | 
			
		||||
                check_result1.assigned_check = check1
 | 
			
		||||
                check_result1.status = CheckStatus.PASSING
 | 
			
		||||
                check_result1.last_run = django_now
 | 
			
		||||
                check_result1.more_info = "Total: 498.7GB, Free: 287.4GB"
 | 
			
		||||
                check_result1.save()
 | 
			
		||||
 | 
			
		||||
            for i in range(30):
 | 
			
		||||
                check1_history = CheckHistory()
 | 
			
		||||
                check1_history.check_id = check1.pk
 | 
			
		||||
                check1_history.agent_id = agent.agent_id
 | 
			
		||||
                check1_history.x = djangotime.now() - djangotime.timedelta(
 | 
			
		||||
                    minutes=i * 2
 | 
			
		||||
                )
 | 
			
		||||
                check1_history.y = random.randint(13, 40)
 | 
			
		||||
                check1_history.save()
 | 
			
		||||
                for i in range(30):
 | 
			
		||||
                    check1_history = CheckHistory()
 | 
			
		||||
                    check1_history.check_id = check1.pk
 | 
			
		||||
                    check1_history.agent_id = agent.agent_id
 | 
			
		||||
                    check1_history.x = django_now - djangotime.timedelta(minutes=i * 2)
 | 
			
		||||
                    check1_history.y = random.randint(13, 40)
 | 
			
		||||
                    check1_history.save()
 | 
			
		||||
 | 
			
		||||
            # ping check
 | 
			
		||||
            check2 = Check()
 | 
			
		||||
            check_result2 = CheckResult(assigned_check=check2, agent=agent)
 | 
			
		||||
            check_result2 = CheckResult()
 | 
			
		||||
 | 
			
		||||
            check2.agent = agent
 | 
			
		||||
            check2.check_type = "ping"
 | 
			
		||||
            check_result2.last_run = djangotime.now()
 | 
			
		||||
            check2.check_type = CheckType.PING
 | 
			
		||||
 | 
			
		||||
            check2.email_alert = random.choice([True, False])
 | 
			
		||||
            check2.text_alert = random.choice([True, False])
 | 
			
		||||
 | 
			
		||||
            check_result2.agent = agent
 | 
			
		||||
            check_result2.assigned_check = check2
 | 
			
		||||
            check_result2.last_run = django_now
 | 
			
		||||
 | 
			
		||||
            if site in sites5:
 | 
			
		||||
                check2.name = "Synology NAS"
 | 
			
		||||
                check_result2.status = "failing"
 | 
			
		||||
                check2.alert_severity = AlertSeverity.ERROR
 | 
			
		||||
                check_result2.status = CheckStatus.FAILING
 | 
			
		||||
                check2.ip = "172.17.14.26"
 | 
			
		||||
                check_result2.more_info = ping_fail_output
 | 
			
		||||
            else:
 | 
			
		||||
                check2.name = "Google"
 | 
			
		||||
                check_result2.status = "passing"
 | 
			
		||||
                check_result2.status = CheckStatus.PASSING
 | 
			
		||||
                check2.ip = "8.8.8.8"
 | 
			
		||||
                check_result2.more_info = ping_success_output
 | 
			
		||||
 | 
			
		||||
@@ -409,9 +472,7 @@ class Command(BaseCommand):
 | 
			
		||||
                check2_history = CheckHistory()
 | 
			
		||||
                check2_history.check_id = check2.pk
 | 
			
		||||
                check2_history.agent_id = agent.agent_id
 | 
			
		||||
                check2_history.x = djangotime.now() - djangotime.timedelta(
 | 
			
		||||
                    minutes=i * 2
 | 
			
		||||
                )
 | 
			
		||||
                check2_history.x = django_now - djangotime.timedelta(minutes=i * 2)
 | 
			
		||||
                if site in sites5:
 | 
			
		||||
                    check2_history.y = 1
 | 
			
		||||
                    check2_history.results = ping_fail_output
 | 
			
		||||
@@ -422,13 +483,19 @@ class Command(BaseCommand):
 | 
			
		||||
 | 
			
		||||
            # cpu load check
 | 
			
		||||
            check3 = Check()
 | 
			
		||||
            check_result3 = CheckResult(assigned_check=check3, agent=agent)
 | 
			
		||||
            check3.agent = agent
 | 
			
		||||
            check3.check_type = "cpuload"
 | 
			
		||||
            check_result3.status = "passing"
 | 
			
		||||
            check_result3.last_run = djangotime.now()
 | 
			
		||||
            check3.check_type = CheckType.CPU_LOAD
 | 
			
		||||
            check3.warning_threshold = 70
 | 
			
		||||
            check3.error_threshold = 90
 | 
			
		||||
            check3.email_alert = random.choice([True, False])
 | 
			
		||||
            check3.text_alert = random.choice([True, False])
 | 
			
		||||
            check3.save()
 | 
			
		||||
 | 
			
		||||
            check_result3 = CheckResult()
 | 
			
		||||
            check_result3.agent = agent
 | 
			
		||||
            check_result3.assigned_check = check3
 | 
			
		||||
            check_result3.status = CheckStatus.PASSING
 | 
			
		||||
            check_result3.last_run = django_now
 | 
			
		||||
            check_result3.history = [
 | 
			
		||||
                15,
 | 
			
		||||
                23,
 | 
			
		||||
@@ -445,101 +512,116 @@ class Command(BaseCommand):
 | 
			
		||||
                13,
 | 
			
		||||
                34,
 | 
			
		||||
            ]
 | 
			
		||||
            check3.email_alert = random.choice([True, False])
 | 
			
		||||
            check3.text_alert = random.choice([True, False])
 | 
			
		||||
            check3.save()
 | 
			
		||||
            check_result3.save()
 | 
			
		||||
 | 
			
		||||
            for i in range(30):
 | 
			
		||||
                check3_history = CheckHistory()
 | 
			
		||||
                check3_history.check_id = check3.pk
 | 
			
		||||
                check3_history.agent_id = agent.agent_id
 | 
			
		||||
                check3_history.x = djangotime.now() - djangotime.timedelta(
 | 
			
		||||
                    minutes=i * 2
 | 
			
		||||
                )
 | 
			
		||||
                check3_history.x = django_now - djangotime.timedelta(minutes=i * 2)
 | 
			
		||||
                check3_history.y = random.randint(2, 79)
 | 
			
		||||
                check3_history.save()
 | 
			
		||||
 | 
			
		||||
            # memory check
 | 
			
		||||
            check4 = Check()
 | 
			
		||||
            check_result4 = CheckResult(assigned_check=check4, agent=agent)
 | 
			
		||||
            check4.agent = agent
 | 
			
		||||
            check4.check_type = "memory"
 | 
			
		||||
            check_result4.status = "passing"
 | 
			
		||||
            check4.check_type = CheckType.MEMORY
 | 
			
		||||
            check4.warning_threshold = 70
 | 
			
		||||
            check4.error_threshold = 85
 | 
			
		||||
            check_result4.history = [34, 34, 35, 36, 34, 34, 34, 34, 34, 34]
 | 
			
		||||
            check4.email_alert = random.choice([True, False])
 | 
			
		||||
            check4.text_alert = random.choice([True, False])
 | 
			
		||||
            check4.save()
 | 
			
		||||
 | 
			
		||||
            check_result4 = CheckResult()
 | 
			
		||||
            check_result4.agent = agent
 | 
			
		||||
            check_result4.assigned_check = check4
 | 
			
		||||
            check_result4.status = CheckStatus.PASSING
 | 
			
		||||
            check_result4.last_run = django_now
 | 
			
		||||
            check_result4.history = [34, 34, 35, 36, 34, 34, 34, 34, 34, 34]
 | 
			
		||||
            check_result4.save()
 | 
			
		||||
 | 
			
		||||
            for i in range(30):
 | 
			
		||||
                check4_history = CheckHistory()
 | 
			
		||||
                check4_history.check_id = check4.pk
 | 
			
		||||
                check4_history.agent_id = agent.agent_id
 | 
			
		||||
                check4_history.x = djangotime.now() - djangotime.timedelta(
 | 
			
		||||
                    minutes=i * 2
 | 
			
		||||
                )
 | 
			
		||||
                check4_history.x = django_now - djangotime.timedelta(minutes=i * 2)
 | 
			
		||||
                check4_history.y = random.randint(2, 79)
 | 
			
		||||
                check4_history.save()
 | 
			
		||||
 | 
			
		||||
            # script check storage pool
 | 
			
		||||
            check5 = Check()
 | 
			
		||||
            check_result5 = CheckResult(assigned_check=check5, agent=agent)
 | 
			
		||||
 | 
			
		||||
            check5.agent = agent
 | 
			
		||||
            check5.check_type = "script"
 | 
			
		||||
            check_result5.status = "passing"
 | 
			
		||||
            check_result5.last_run = djangotime.now()
 | 
			
		||||
            check5.check_type = CheckType.SCRIPT
 | 
			
		||||
 | 
			
		||||
            check5.email_alert = random.choice([True, False])
 | 
			
		||||
            check5.text_alert = random.choice([True, False])
 | 
			
		||||
            check5.timeout = 120
 | 
			
		||||
            check_result5.retcode = 0
 | 
			
		||||
            check_result5.execution_time = "4.0000"
 | 
			
		||||
 | 
			
		||||
            check5.script = check_pool_health
 | 
			
		||||
            check5.save()
 | 
			
		||||
 | 
			
		||||
            check_result5 = CheckResult()
 | 
			
		||||
            check_result5.agent = agent
 | 
			
		||||
            check_result5.assigned_check = check5
 | 
			
		||||
            check_result5.status = CheckStatus.PASSING
 | 
			
		||||
            check_result5.last_run = django_now
 | 
			
		||||
            check_result5.retcode = 0
 | 
			
		||||
            check_result5.execution_time = "4.0000"
 | 
			
		||||
            check_result5.save()
 | 
			
		||||
 | 
			
		||||
            for i in range(30):
 | 
			
		||||
                check5_history = CheckHistory()
 | 
			
		||||
                check5_history.check_id = check5.pk
 | 
			
		||||
                check5_history.agent_id = agent.agent_id
 | 
			
		||||
                check5_history.x = djangotime.now() - djangotime.timedelta(
 | 
			
		||||
                    minutes=i * 2
 | 
			
		||||
                )
 | 
			
		||||
                check5_history.x = django_now - djangotime.timedelta(minutes=i * 2)
 | 
			
		||||
                if i == 10 or i == 18:
 | 
			
		||||
                    check5_history.y = 1
 | 
			
		||||
                else:
 | 
			
		||||
                    check5_history.y = 0
 | 
			
		||||
                check5_history.results = {
 | 
			
		||||
                    "retcode": 0,
 | 
			
		||||
                    "stdout": None,
 | 
			
		||||
                    "stderr": None,
 | 
			
		||||
                    "execution_time": "4.0000",
 | 
			
		||||
                }
 | 
			
		||||
                check5_history.save()
 | 
			
		||||
 | 
			
		||||
            check6 = Check()
 | 
			
		||||
            check_result6 = CheckResult(assigned_check=check6, agent=agent)
 | 
			
		||||
 | 
			
		||||
            check6.agent = agent
 | 
			
		||||
            check6.check_type = "script"
 | 
			
		||||
            check_result6.status = "passing"
 | 
			
		||||
            check_result6.last_run = djangotime.now()
 | 
			
		||||
            check6.check_type = CheckType.SCRIPT
 | 
			
		||||
            check6.email_alert = random.choice([True, False])
 | 
			
		||||
            check6.text_alert = random.choice([True, False])
 | 
			
		||||
            check6.timeout = 120
 | 
			
		||||
            check_result6.retcode = 0
 | 
			
		||||
            check_result6.execution_time = "4.0000"
 | 
			
		||||
            check6.script = check_net_aware
 | 
			
		||||
            check6.save()
 | 
			
		||||
 | 
			
		||||
            check_result6 = CheckResult()
 | 
			
		||||
            check_result6.agent = agent
 | 
			
		||||
            check_result6.assigned_check = check6
 | 
			
		||||
            check_result6.status = CheckStatus.PASSING
 | 
			
		||||
            check_result6.last_run = django_now
 | 
			
		||||
            check_result6.retcode = 0
 | 
			
		||||
            check_result6.execution_time = "4.0000"
 | 
			
		||||
            check_result6.save()
 | 
			
		||||
 | 
			
		||||
            for i in range(30):
 | 
			
		||||
                check6_history = CheckHistory()
 | 
			
		||||
                check6_history.check_id = check6.pk
 | 
			
		||||
                check6_history.agent_id = agent.agent_id
 | 
			
		||||
                check6_history.x = djangotime.now() - djangotime.timedelta(
 | 
			
		||||
                    minutes=i * 2
 | 
			
		||||
                )
 | 
			
		||||
                check6_history.x = django_now - djangotime.timedelta(minutes=i * 2)
 | 
			
		||||
                check6_history.y = 0
 | 
			
		||||
                check6_history.results = {
 | 
			
		||||
                    "retcode": 0,
 | 
			
		||||
                    "stdout": None,
 | 
			
		||||
                    "stderr": None,
 | 
			
		||||
                    "execution_time": "4.0000",
 | 
			
		||||
                }
 | 
			
		||||
                check6_history.save()
 | 
			
		||||
 | 
			
		||||
            nla_task = AutomatedTask()
 | 
			
		||||
            nla_task_result = TaskResult(task=nla_task, agent=agent)
 | 
			
		||||
 | 
			
		||||
            nla_task.agent = agent
 | 
			
		||||
            actions = [
 | 
			
		||||
                {
 | 
			
		||||
@@ -553,17 +635,21 @@ class Command(BaseCommand):
 | 
			
		||||
            nla_task.actions = actions
 | 
			
		||||
            nla_task.assigned_check = check6
 | 
			
		||||
            nla_task.name = "Restart NLA"
 | 
			
		||||
            nla_task.task_type = "checkfailure"
 | 
			
		||||
            nla_task.task_type = TaskType.CHECK_FAILURE
 | 
			
		||||
            nla_task.save()
 | 
			
		||||
 | 
			
		||||
            nla_task_result = TaskResult()
 | 
			
		||||
            nla_task_result.task = nla_task
 | 
			
		||||
            nla_task_result.agent = agent
 | 
			
		||||
            nla_task_result.execution_time = "1.8443"
 | 
			
		||||
            nla_task_result.last_run = djangotime.now()
 | 
			
		||||
            nla_task_result.last_run = django_now
 | 
			
		||||
            nla_task_result.stdout = "no stdout"
 | 
			
		||||
            nla_task_result.retcode = 0
 | 
			
		||||
            nla_task_result.sync_status = "synced"
 | 
			
		||||
            nla_task.save()
 | 
			
		||||
            nla_task_result.sync_status = TaskSyncStatus.SYNCED
 | 
			
		||||
            nla_task_result.save()
 | 
			
		||||
 | 
			
		||||
            spool_task = AutomatedTask()
 | 
			
		||||
            spool_task_result = TaskResult(task=spool_task, agent=agent)
 | 
			
		||||
 | 
			
		||||
            spool_task.agent = agent
 | 
			
		||||
            actions = [
 | 
			
		||||
                {
 | 
			
		||||
@@ -576,25 +662,26 @@ class Command(BaseCommand):
 | 
			
		||||
            ]
 | 
			
		||||
            spool_task.actions = actions
 | 
			
		||||
            spool_task.name = "Clear the print spooler"
 | 
			
		||||
            spool_task.task_type = "daily"
 | 
			
		||||
            spool_task.run_time_date = djangotime.now() + djangotime.timedelta(
 | 
			
		||||
                minutes=10
 | 
			
		||||
            )
 | 
			
		||||
            spool_task.expire_date = djangotime.now() + djangotime.timedelta(days=753)
 | 
			
		||||
            spool_task.task_type = TaskType.DAILY
 | 
			
		||||
            spool_task.run_time_date = django_now + djangotime.timedelta(minutes=10)
 | 
			
		||||
            spool_task.expire_date = django_now + djangotime.timedelta(days=753)
 | 
			
		||||
            spool_task.daily_interval = 1
 | 
			
		||||
            spool_task.weekly_interval = 1
 | 
			
		||||
            spool_task.task_repetition_duration = "2h"
 | 
			
		||||
            spool_task.task_repetition_interval = "25m"
 | 
			
		||||
            spool_task.random_task_delay = "3m"
 | 
			
		||||
            spool_task_result.last_run = djangotime.now()
 | 
			
		||||
            spool_task.save()
 | 
			
		||||
 | 
			
		||||
            spool_task_result = TaskResult()
 | 
			
		||||
            spool_task_result.task = spool_task
 | 
			
		||||
            spool_task_result.agent = agent
 | 
			
		||||
            spool_task_result.last_run = django_now
 | 
			
		||||
            spool_task_result.retcode = 0
 | 
			
		||||
            spool_task_result.stdout = spooler_stdout
 | 
			
		||||
            spool_task_result.sync_status = "synced"
 | 
			
		||||
            spool_task.save()
 | 
			
		||||
            spool_task_result.sync_status = TaskSyncStatus.SYNCED
 | 
			
		||||
            spool_task_result.save()
 | 
			
		||||
 | 
			
		||||
            tmp_dir_task = AutomatedTask()
 | 
			
		||||
            tmp_dir_task_result = TaskResult(task=tmp_dir_task, agent=agent)
 | 
			
		||||
            tmp_dir_task.agent = agent
 | 
			
		||||
            tmp_dir_task.name = "show temp dir files"
 | 
			
		||||
            actions = [
 | 
			
		||||
@@ -607,139 +694,153 @@ class Command(BaseCommand):
 | 
			
		||||
                }
 | 
			
		||||
            ]
 | 
			
		||||
            tmp_dir_task.actions = actions
 | 
			
		||||
            tmp_dir_task.task_type = "manual"
 | 
			
		||||
            tmp_dir_task_result.last_run = djangotime.now()
 | 
			
		||||
            tmp_dir_task.task_type = TaskType.MANUAL
 | 
			
		||||
            tmp_dir_task.save()
 | 
			
		||||
 | 
			
		||||
            tmp_dir_task_result = TaskResult()
 | 
			
		||||
            tmp_dir_task_result.task = tmp_dir_task
 | 
			
		||||
            tmp_dir_task_result.agent = agent
 | 
			
		||||
            tmp_dir_task_result.last_run = django_now
 | 
			
		||||
            tmp_dir_task_result.stdout = temp_dir_stdout
 | 
			
		||||
            tmp_dir_task_result.retcode = 0
 | 
			
		||||
            tmp_dir_task_result.sync_status = "synced"
 | 
			
		||||
            tmp_dir_task.save()
 | 
			
		||||
            tmp_dir_task_result.sync_status = TaskSyncStatus.SYNCED
 | 
			
		||||
            tmp_dir_task_result.save()
 | 
			
		||||
 | 
			
		||||
            check7 = Check()
 | 
			
		||||
            check_result7 = CheckResult(assigned_check=check7, agent=agent)
 | 
			
		||||
 | 
			
		||||
            check7.agent = agent
 | 
			
		||||
            check7.check_type = "script"
 | 
			
		||||
            check_result7.status = "passing"
 | 
			
		||||
            check_result7.last_run = djangotime.now()
 | 
			
		||||
            check7.check_type = CheckType.SCRIPT
 | 
			
		||||
 | 
			
		||||
            check7.email_alert = random.choice([True, False])
 | 
			
		||||
            check7.text_alert = random.choice([True, False])
 | 
			
		||||
            check7.timeout = 120
 | 
			
		||||
 | 
			
		||||
            check7.script = clear_spool
 | 
			
		||||
 | 
			
		||||
            check7.save()
 | 
			
		||||
 | 
			
		||||
            check_result7 = CheckResult()
 | 
			
		||||
            check_result7.assigned_check = check7
 | 
			
		||||
            check_result7.agent = agent
 | 
			
		||||
            check_result7.status = CheckStatus.PASSING
 | 
			
		||||
            check_result7.last_run = django_now
 | 
			
		||||
            check_result7.retcode = 0
 | 
			
		||||
            check_result7.execution_time = "3.1337"
 | 
			
		||||
            check7.script = clear_spool
 | 
			
		||||
            check_result7.stdout = spooler_stdout
 | 
			
		||||
            check7.save()
 | 
			
		||||
            check_result7.save()
 | 
			
		||||
 | 
			
		||||
            for i in range(30):
 | 
			
		||||
                check7_history = CheckHistory()
 | 
			
		||||
                check7_history.check_id = check7.pk
 | 
			
		||||
                check7_history.agent_id = agent.agent_id
 | 
			
		||||
                check7_history.x = djangotime.now() - djangotime.timedelta(
 | 
			
		||||
                    minutes=i * 2
 | 
			
		||||
                )
 | 
			
		||||
                check7_history.x = django_now - djangotime.timedelta(minutes=i * 2)
 | 
			
		||||
                check7_history.y = 0
 | 
			
		||||
                check7_history.results = {
 | 
			
		||||
                    "retcode": 0,
 | 
			
		||||
                    "stdout": spooler_stdout,
 | 
			
		||||
                    "stderr": None,
 | 
			
		||||
                    "execution_time": "3.1337",
 | 
			
		||||
                }
 | 
			
		||||
                check7_history.save()
 | 
			
		||||
 | 
			
		||||
            check8 = Check()
 | 
			
		||||
            check_result8 = CheckResult(assigned_check=check8, agent=agent)
 | 
			
		||||
            check8.agent = agent
 | 
			
		||||
            check8.check_type = "winsvc"
 | 
			
		||||
            check_result8.status = "passing"
 | 
			
		||||
            check_result8.last_run = djangotime.now()
 | 
			
		||||
            check8.email_alert = random.choice([True, False])
 | 
			
		||||
            check8.text_alert = random.choice([True, False])
 | 
			
		||||
            check_result8.more_info = "Status RUNNING"
 | 
			
		||||
            check8.fails_b4_alert = 4
 | 
			
		||||
            check8.svc_name = "Spooler"
 | 
			
		||||
            check8.svc_display_name = "Print Spooler"
 | 
			
		||||
            check8.pass_if_start_pending = False
 | 
			
		||||
            check8.restart_if_stopped = True
 | 
			
		||||
            check8.save()
 | 
			
		||||
            check_result8.save()
 | 
			
		||||
            if agent.plat == AgentPlat.WINDOWS:
 | 
			
		||||
                check8 = Check()
 | 
			
		||||
                check8.agent = agent
 | 
			
		||||
                check8.check_type = CheckType.WINSVC
 | 
			
		||||
                check8.email_alert = random.choice([True, False])
 | 
			
		||||
                check8.text_alert = random.choice([True, False])
 | 
			
		||||
                check8.fails_b4_alert = 4
 | 
			
		||||
                check8.svc_name = "Spooler"
 | 
			
		||||
                check8.svc_display_name = "Print Spooler"
 | 
			
		||||
                check8.pass_if_start_pending = False
 | 
			
		||||
                check8.restart_if_stopped = True
 | 
			
		||||
                check8.save()
 | 
			
		||||
 | 
			
		||||
            for i in range(30):
 | 
			
		||||
                check8_history = CheckHistory()
 | 
			
		||||
                check8_history.check_id = check8.pk
 | 
			
		||||
                check8_history.agent_id = agent.agent_id
 | 
			
		||||
                check8_history.x = djangotime.now() - djangotime.timedelta(
 | 
			
		||||
                    minutes=i * 2
 | 
			
		||||
                )
 | 
			
		||||
                if i == 10 or i == 18:
 | 
			
		||||
                    check8_history.y = 1
 | 
			
		||||
                    check8_history.results = "Status STOPPED"
 | 
			
		||||
                check_result8 = CheckResult()
 | 
			
		||||
                check_result8.assigned_check = check8
 | 
			
		||||
                check_result8.agent = agent
 | 
			
		||||
                check_result8.status = CheckStatus.PASSING
 | 
			
		||||
                check_result8.last_run = django_now
 | 
			
		||||
                check_result8.more_info = "Status RUNNING"
 | 
			
		||||
                check_result8.save()
 | 
			
		||||
 | 
			
		||||
                for i in range(30):
 | 
			
		||||
                    check8_history = CheckHistory()
 | 
			
		||||
                    check8_history.check_id = check8.pk
 | 
			
		||||
                    check8_history.agent_id = agent.agent_id
 | 
			
		||||
                    check8_history.x = django_now - djangotime.timedelta(minutes=i * 2)
 | 
			
		||||
                    if i == 10 or i == 18:
 | 
			
		||||
                        check8_history.y = 1
 | 
			
		||||
                        check8_history.results = "Status STOPPED"
 | 
			
		||||
                    else:
 | 
			
		||||
                        check8_history.y = 0
 | 
			
		||||
                        check8_history.results = "Status RUNNING"
 | 
			
		||||
                    check8_history.save()
 | 
			
		||||
 | 
			
		||||
                check9 = Check()
 | 
			
		||||
                check9.agent = agent
 | 
			
		||||
                check9.check_type = CheckType.EVENT_LOG
 | 
			
		||||
                check9.name = "unexpected shutdown"
 | 
			
		||||
                check9.email_alert = random.choice([True, False])
 | 
			
		||||
                check9.text_alert = random.choice([True, False])
 | 
			
		||||
                check9.fails_b4_alert = 2
 | 
			
		||||
                check9.log_name = EvtLogNames.APPLICATION
 | 
			
		||||
                check9.event_id = 1001
 | 
			
		||||
                check9.event_type = EvtLogTypes.INFO
 | 
			
		||||
                check9.fail_when = EvtLogFailWhen.CONTAINS
 | 
			
		||||
                check9.search_last_days = 30
 | 
			
		||||
 | 
			
		||||
                check_result9 = CheckResult()
 | 
			
		||||
                check_result9.agent = agent
 | 
			
		||||
                check_result9.assigned_check = check9
 | 
			
		||||
 | 
			
		||||
                check_result9.last_run = django_now
 | 
			
		||||
                if site in sites5:
 | 
			
		||||
                    check_result9.extra_details = eventlog_check_fail_data
 | 
			
		||||
                    check_result9.status = CheckStatus.FAILING
 | 
			
		||||
                else:
 | 
			
		||||
                    check8_history.y = 0
 | 
			
		||||
                    check8_history.results = "Status RUNNING"
 | 
			
		||||
                check8_history.save()
 | 
			
		||||
                    check_result9.extra_details = {"log": []}
 | 
			
		||||
                    check_result9.status = CheckStatus.PASSING
 | 
			
		||||
 | 
			
		||||
            check9 = Check()
 | 
			
		||||
            check_result9 = CheckResult(assigned_check=check9, agent=agent)
 | 
			
		||||
            check9.agent = agent
 | 
			
		||||
            check9.check_type = "eventlog"
 | 
			
		||||
            check9.name = "unexpected shutdown"
 | 
			
		||||
                check9.save()
 | 
			
		||||
                check_result9.save()
 | 
			
		||||
 | 
			
		||||
            check_result9.last_run = djangotime.now()
 | 
			
		||||
            check9.email_alert = random.choice([True, False])
 | 
			
		||||
            check9.text_alert = random.choice([True, False])
 | 
			
		||||
            check9.fails_b4_alert = 2
 | 
			
		||||
                for i in range(30):
 | 
			
		||||
                    check9_history = CheckHistory()
 | 
			
		||||
                    check9_history.check_id = check9.pk
 | 
			
		||||
                    check9_history.agent_id = agent.agent_id
 | 
			
		||||
                    check9_history.x = django_now - djangotime.timedelta(minutes=i * 2)
 | 
			
		||||
                    if i == 10 or i == 18:
 | 
			
		||||
                        check9_history.y = 1
 | 
			
		||||
                        check9_history.results = "Events Found: 16"
 | 
			
		||||
                    else:
 | 
			
		||||
                        check9_history.y = 0
 | 
			
		||||
                        check9_history.results = "Events Found: 0"
 | 
			
		||||
                    check9_history.save()
 | 
			
		||||
 | 
			
		||||
            if site in sites5:
 | 
			
		||||
                check_result9.extra_details = eventlog_check_fail_data
 | 
			
		||||
                check_result9.status = "failing"
 | 
			
		||||
            else:
 | 
			
		||||
                check_result9.extra_details = {"log": []}
 | 
			
		||||
                check_result9.status = "passing"
 | 
			
		||||
                pick = random.randint(1, 10)
 | 
			
		||||
 | 
			
		||||
            check9.log_name = "Application"
 | 
			
		||||
            check9.event_id = 1001
 | 
			
		||||
            check9.event_type = "INFO"
 | 
			
		||||
            check9.fail_when = "contains"
 | 
			
		||||
            check9.search_last_days = 30
 | 
			
		||||
                if pick == 5 or pick == 3:
 | 
			
		||||
                    reboot_time = django_now + djangotime.timedelta(
 | 
			
		||||
                        minutes=random.randint(1000, 500000)
 | 
			
		||||
                    )
 | 
			
		||||
                    date_obj = dt.datetime.strftime(reboot_time, "%Y-%m-%d %H:%M")
 | 
			
		||||
 | 
			
		||||
            check9.save()
 | 
			
		||||
            check_result9.save()
 | 
			
		||||
                    obj = dt.datetime.strptime(date_obj, "%Y-%m-%d %H:%M")
 | 
			
		||||
 | 
			
		||||
            for i in range(30):
 | 
			
		||||
                check9_history = CheckHistory()
 | 
			
		||||
                check9_history.check_id = check9.pk
 | 
			
		||||
                check9_history.agent_id = agent.agent_id
 | 
			
		||||
                check9_history.x = djangotime.now() - djangotime.timedelta(
 | 
			
		||||
                    minutes=i * 2
 | 
			
		||||
                )
 | 
			
		||||
                if i == 10 or i == 18:
 | 
			
		||||
                    check9_history.y = 1
 | 
			
		||||
                    check9_history.results = "Events Found: 16"
 | 
			
		||||
                else:
 | 
			
		||||
                    check9_history.y = 0
 | 
			
		||||
                    check9_history.results = "Events Found: 0"
 | 
			
		||||
                check9_history.save()
 | 
			
		||||
                    task_name = "TacticalRMM_SchedReboot_" + "".join(
 | 
			
		||||
                        random.choice(string.ascii_letters) for _ in range(10)
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
            pick = random.randint(1, 10)
 | 
			
		||||
 | 
			
		||||
            if pick == 5 or pick == 3:
 | 
			
		||||
 | 
			
		||||
                reboot_time = djangotime.now() + djangotime.timedelta(
 | 
			
		||||
                    minutes=random.randint(1000, 500000)
 | 
			
		||||
                )
 | 
			
		||||
                date_obj = dt.datetime.strftime(reboot_time, "%Y-%m-%d %H:%M")
 | 
			
		||||
 | 
			
		||||
                obj = dt.datetime.strptime(date_obj, "%Y-%m-%d %H:%M")
 | 
			
		||||
 | 
			
		||||
                task_name = "TacticalRMM_SchedReboot_" + "".join(
 | 
			
		||||
                    random.choice(string.ascii_letters) for _ in range(10)
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
                sched_reboot = PendingAction()
 | 
			
		||||
                sched_reboot.agent = agent
 | 
			
		||||
                sched_reboot.action_type = "schedreboot"
 | 
			
		||||
                sched_reboot.details = {
 | 
			
		||||
                    "time": str(obj),
 | 
			
		||||
                    "taskname": task_name,
 | 
			
		||||
                }
 | 
			
		||||
                sched_reboot.save()
 | 
			
		||||
                    sched_reboot = PendingAction()
 | 
			
		||||
                    sched_reboot.agent = agent
 | 
			
		||||
                    sched_reboot.action_type = PAAction.SCHED_REBOOT
 | 
			
		||||
                    sched_reboot.details = {
 | 
			
		||||
                        "time": str(obj),
 | 
			
		||||
                        "taskname": task_name,
 | 
			
		||||
                    }
 | 
			
		||||
                    sched_reboot.save()
 | 
			
		||||
 | 
			
		||||
            self.stdout.write(self.style.SUCCESS(f"Added agent # {count_agents + 1}"))
 | 
			
		||||
 | 
			
		||||
        call_command("load_demo_scripts")
 | 
			
		||||
        self.stdout.write("done")
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										30
									
								
								api/tacticalrmm/agents/management/commands/find_services.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								api/tacticalrmm/agents/management/commands/find_services.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from tacticalrmm.constants import AGENT_DEFER
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = "Find all agents that have a certain service installed"
 | 
			
		||||
 | 
			
		||||
    def add_arguments(self, parser):
 | 
			
		||||
        parser.add_argument("name", type=str)
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args, **kwargs):
 | 
			
		||||
        search = kwargs["name"].lower()
 | 
			
		||||
 | 
			
		||||
        agents = Agent.objects.defer(*AGENT_DEFER)
 | 
			
		||||
        for agent in agents:
 | 
			
		||||
            try:
 | 
			
		||||
                for svc in agent.services:
 | 
			
		||||
                    if (
 | 
			
		||||
                        search in svc["name"].lower()
 | 
			
		||||
                        or search in svc["display_name"].lower()
 | 
			
		||||
                    ):
 | 
			
		||||
                        self.stdout.write(
 | 
			
		||||
                            self.style.SUCCESS(
 | 
			
		||||
                                f"{agent.hostname} - {svc['name']} ({svc['display_name']}) - {svc['status']}"
 | 
			
		||||
                            )
 | 
			
		||||
                        )
 | 
			
		||||
            except:
 | 
			
		||||
                continue
 | 
			
		||||
@@ -0,0 +1,24 @@
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from tacticalrmm.constants import AGENT_DEFER
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    def find_duplicates(self, lst):
 | 
			
		||||
        return list(set([item for item in lst if lst.count(item) > 1]))
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args, **kwargs):
 | 
			
		||||
        for agent in Agent.objects.defer(*AGENT_DEFER).prefetch_related(
 | 
			
		||||
            "custom_fields__field"
 | 
			
		||||
        ):
 | 
			
		||||
            if dupes := self.find_duplicates(
 | 
			
		||||
                [i.field.name for i in agent.custom_fields.all()]
 | 
			
		||||
            ):
 | 
			
		||||
                for dupe in dupes:
 | 
			
		||||
                    cf = list(
 | 
			
		||||
                        agent.custom_fields.filter(field__name=dupe).order_by("id")
 | 
			
		||||
                    )
 | 
			
		||||
                    to_delete = cf[:-1]
 | 
			
		||||
                    for i in to_delete:
 | 
			
		||||
                        i.delete()
 | 
			
		||||
@@ -1,16 +1,17 @@
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from tacticalrmm.constants import AGENT_STATUS_ONLINE, ONLINE_AGENTS
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = "Shows online agents that are not on the latest version"
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args, **kwargs):
 | 
			
		||||
        q = Agent.objects.exclude(version=settings.LATEST_AGENT_VER).only(
 | 
			
		||||
            "pk", "version", "last_seen", "overdue_time", "offline_time"
 | 
			
		||||
        )
 | 
			
		||||
        agents = [i for i in q if i.status == "online"]
 | 
			
		||||
        only = ONLINE_AGENTS + ("hostname",)
 | 
			
		||||
        q = Agent.objects.exclude(version=settings.LATEST_AGENT_VER).only(*only)
 | 
			
		||||
        agents = [i for i in q if i.status == AGENT_STATUS_ONLINE]
 | 
			
		||||
        for agent in agents:
 | 
			
		||||
            self.stdout.write(
 | 
			
		||||
                self.style.SUCCESS(f"{agent.hostname} - v{agent.version}")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,10 @@
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from agents.tasks import send_agent_update_task
 | 
			
		||||
from core.utils import get_core_settings
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
from packaging import version as pyver
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from agents.tasks import send_agent_update_task
 | 
			
		||||
from core.utils import get_core_settings, token_is_valid
 | 
			
		||||
from tacticalrmm.constants import AGENT_DEFER
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -22,4 +22,5 @@ class Command(BaseCommand):
 | 
			
		||||
            for i in q
 | 
			
		||||
            if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
 | 
			
		||||
        ]
 | 
			
		||||
        send_agent_update_task.delay(agent_ids=agent_ids)
 | 
			
		||||
        token, _ = token_is_valid()
 | 
			
		||||
        send_agent_update_task.delay(agent_ids=agent_ids, token=token, force=False)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
# Generated by Django 4.0.3 on 2022-04-07 17:28
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,17 @@
 | 
			
		||||
# Generated by Django 4.0.4 on 2022-04-25 06:51
 | 
			
		||||
 | 
			
		||||
from django.db import migrations
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('agents', '0049_agent_agents_agen_monitor_df8816_idx'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='agent',
 | 
			
		||||
            name='plat_release',
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										18
									
								
								api/tacticalrmm/agents/migrations/0051_alter_agent_plat.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								api/tacticalrmm/agents/migrations/0051_alter_agent_plat.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
# Generated by Django 4.0.4 on 2022-05-18 03:50
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('agents', '0050_remove_agent_plat_release'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='agent',
 | 
			
		||||
            name='plat',
 | 
			
		||||
            field=models.CharField(choices=[('windows', 'Windows'), ('linux', 'Linux'), ('darwin', 'macOS')], default='windows', max_length=255),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
# Generated by Django 4.0.4 on 2022-05-18 05:28
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('agents', '0051_alter_agent_plat'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='agent',
 | 
			
		||||
            name='monitoring_type',
 | 
			
		||||
            field=models.CharField(choices=[('server', 'Server'), ('workstation', 'Workstation')], default='server', max_length=30),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,17 @@
 | 
			
		||||
# Generated by Django 4.0.4 on 2022-05-18 06:10
 | 
			
		||||
 | 
			
		||||
from django.db import migrations
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('agents', '0052_alter_agent_monitoring_type'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='agenthistory',
 | 
			
		||||
            name='status',
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										18
									
								
								api/tacticalrmm/agents/migrations/0054_alter_agent_goarch.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								api/tacticalrmm/agents/migrations/0054_alter_agent_goarch.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
# Generated by Django 4.0.4 on 2022-06-06 04:03
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('agents', '0053_remove_agenthistory_status'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='agent',
 | 
			
		||||
            name='goarch',
 | 
			
		||||
            field=models.CharField(blank=True, choices=[('amd64', 'amd64'), ('386', '386'), ('arm64', 'arm64'), ('arm', 'arm')], max_length=255, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										631
									
								
								api/tacticalrmm/agents/migrations/0055_alter_agent_time_zone.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										631
									
								
								api/tacticalrmm/agents/migrations/0055_alter_agent_time_zone.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,631 @@
 | 
			
		||||
# Generated by Django 4.1 on 2022-08-24 07:32
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("agents", "0054_alter_agent_goarch"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="agent",
 | 
			
		||||
            name="time_zone",
 | 
			
		||||
            field=models.CharField(
 | 
			
		||||
                blank=True,
 | 
			
		||||
                choices=[
 | 
			
		||||
                    ("Africa/Abidjan", "Africa/Abidjan"),
 | 
			
		||||
                    ("Africa/Accra", "Africa/Accra"),
 | 
			
		||||
                    ("Africa/Addis_Ababa", "Africa/Addis_Ababa"),
 | 
			
		||||
                    ("Africa/Algiers", "Africa/Algiers"),
 | 
			
		||||
                    ("Africa/Asmara", "Africa/Asmara"),
 | 
			
		||||
                    ("Africa/Asmera", "Africa/Asmera"),
 | 
			
		||||
                    ("Africa/Bamako", "Africa/Bamako"),
 | 
			
		||||
                    ("Africa/Bangui", "Africa/Bangui"),
 | 
			
		||||
                    ("Africa/Banjul", "Africa/Banjul"),
 | 
			
		||||
                    ("Africa/Bissau", "Africa/Bissau"),
 | 
			
		||||
                    ("Africa/Blantyre", "Africa/Blantyre"),
 | 
			
		||||
                    ("Africa/Brazzaville", "Africa/Brazzaville"),
 | 
			
		||||
                    ("Africa/Bujumbura", "Africa/Bujumbura"),
 | 
			
		||||
                    ("Africa/Cairo", "Africa/Cairo"),
 | 
			
		||||
                    ("Africa/Casablanca", "Africa/Casablanca"),
 | 
			
		||||
                    ("Africa/Ceuta", "Africa/Ceuta"),
 | 
			
		||||
                    ("Africa/Conakry", "Africa/Conakry"),
 | 
			
		||||
                    ("Africa/Dakar", "Africa/Dakar"),
 | 
			
		||||
                    ("Africa/Dar_es_Salaam", "Africa/Dar_es_Salaam"),
 | 
			
		||||
                    ("Africa/Djibouti", "Africa/Djibouti"),
 | 
			
		||||
                    ("Africa/Douala", "Africa/Douala"),
 | 
			
		||||
                    ("Africa/El_Aaiun", "Africa/El_Aaiun"),
 | 
			
		||||
                    ("Africa/Freetown", "Africa/Freetown"),
 | 
			
		||||
                    ("Africa/Gaborone", "Africa/Gaborone"),
 | 
			
		||||
                    ("Africa/Harare", "Africa/Harare"),
 | 
			
		||||
                    ("Africa/Johannesburg", "Africa/Johannesburg"),
 | 
			
		||||
                    ("Africa/Juba", "Africa/Juba"),
 | 
			
		||||
                    ("Africa/Kampala", "Africa/Kampala"),
 | 
			
		||||
                    ("Africa/Khartoum", "Africa/Khartoum"),
 | 
			
		||||
                    ("Africa/Kigali", "Africa/Kigali"),
 | 
			
		||||
                    ("Africa/Kinshasa", "Africa/Kinshasa"),
 | 
			
		||||
                    ("Africa/Lagos", "Africa/Lagos"),
 | 
			
		||||
                    ("Africa/Libreville", "Africa/Libreville"),
 | 
			
		||||
                    ("Africa/Lome", "Africa/Lome"),
 | 
			
		||||
                    ("Africa/Luanda", "Africa/Luanda"),
 | 
			
		||||
                    ("Africa/Lubumbashi", "Africa/Lubumbashi"),
 | 
			
		||||
                    ("Africa/Lusaka", "Africa/Lusaka"),
 | 
			
		||||
                    ("Africa/Malabo", "Africa/Malabo"),
 | 
			
		||||
                    ("Africa/Maputo", "Africa/Maputo"),
 | 
			
		||||
                    ("Africa/Maseru", "Africa/Maseru"),
 | 
			
		||||
                    ("Africa/Mbabane", "Africa/Mbabane"),
 | 
			
		||||
                    ("Africa/Mogadishu", "Africa/Mogadishu"),
 | 
			
		||||
                    ("Africa/Monrovia", "Africa/Monrovia"),
 | 
			
		||||
                    ("Africa/Nairobi", "Africa/Nairobi"),
 | 
			
		||||
                    ("Africa/Ndjamena", "Africa/Ndjamena"),
 | 
			
		||||
                    ("Africa/Niamey", "Africa/Niamey"),
 | 
			
		||||
                    ("Africa/Nouakchott", "Africa/Nouakchott"),
 | 
			
		||||
                    ("Africa/Ouagadougou", "Africa/Ouagadougou"),
 | 
			
		||||
                    ("Africa/Porto-Novo", "Africa/Porto-Novo"),
 | 
			
		||||
                    ("Africa/Sao_Tome", "Africa/Sao_Tome"),
 | 
			
		||||
                    ("Africa/Timbuktu", "Africa/Timbuktu"),
 | 
			
		||||
                    ("Africa/Tripoli", "Africa/Tripoli"),
 | 
			
		||||
                    ("Africa/Tunis", "Africa/Tunis"),
 | 
			
		||||
                    ("Africa/Windhoek", "Africa/Windhoek"),
 | 
			
		||||
                    ("America/Adak", "America/Adak"),
 | 
			
		||||
                    ("America/Anchorage", "America/Anchorage"),
 | 
			
		||||
                    ("America/Anguilla", "America/Anguilla"),
 | 
			
		||||
                    ("America/Antigua", "America/Antigua"),
 | 
			
		||||
                    ("America/Araguaina", "America/Araguaina"),
 | 
			
		||||
                    (
 | 
			
		||||
                        "America/Argentina/Buenos_Aires",
 | 
			
		||||
                        "America/Argentina/Buenos_Aires",
 | 
			
		||||
                    ),
 | 
			
		||||
                    ("America/Argentina/Catamarca", "America/Argentina/Catamarca"),
 | 
			
		||||
                    (
 | 
			
		||||
                        "America/Argentina/ComodRivadavia",
 | 
			
		||||
                        "America/Argentina/ComodRivadavia",
 | 
			
		||||
                    ),
 | 
			
		||||
                    ("America/Argentina/Cordoba", "America/Argentina/Cordoba"),
 | 
			
		||||
                    ("America/Argentina/Jujuy", "America/Argentina/Jujuy"),
 | 
			
		||||
                    ("America/Argentina/La_Rioja", "America/Argentina/La_Rioja"),
 | 
			
		||||
                    ("America/Argentina/Mendoza", "America/Argentina/Mendoza"),
 | 
			
		||||
                    (
 | 
			
		||||
                        "America/Argentina/Rio_Gallegos",
 | 
			
		||||
                        "America/Argentina/Rio_Gallegos",
 | 
			
		||||
                    ),
 | 
			
		||||
                    ("America/Argentina/Salta", "America/Argentina/Salta"),
 | 
			
		||||
                    ("America/Argentina/San_Juan", "America/Argentina/San_Juan"),
 | 
			
		||||
                    ("America/Argentina/San_Luis", "America/Argentina/San_Luis"),
 | 
			
		||||
                    ("America/Argentina/Tucuman", "America/Argentina/Tucuman"),
 | 
			
		||||
                    ("America/Argentina/Ushuaia", "America/Argentina/Ushuaia"),
 | 
			
		||||
                    ("America/Aruba", "America/Aruba"),
 | 
			
		||||
                    ("America/Asuncion", "America/Asuncion"),
 | 
			
		||||
                    ("America/Atikokan", "America/Atikokan"),
 | 
			
		||||
                    ("America/Atka", "America/Atka"),
 | 
			
		||||
                    ("America/Bahia", "America/Bahia"),
 | 
			
		||||
                    ("America/Bahia_Banderas", "America/Bahia_Banderas"),
 | 
			
		||||
                    ("America/Barbados", "America/Barbados"),
 | 
			
		||||
                    ("America/Belem", "America/Belem"),
 | 
			
		||||
                    ("America/Belize", "America/Belize"),
 | 
			
		||||
                    ("America/Blanc-Sablon", "America/Blanc-Sablon"),
 | 
			
		||||
                    ("America/Boa_Vista", "America/Boa_Vista"),
 | 
			
		||||
                    ("America/Bogota", "America/Bogota"),
 | 
			
		||||
                    ("America/Boise", "America/Boise"),
 | 
			
		||||
                    ("America/Buenos_Aires", "America/Buenos_Aires"),
 | 
			
		||||
                    ("America/Cambridge_Bay", "America/Cambridge_Bay"),
 | 
			
		||||
                    ("America/Campo_Grande", "America/Campo_Grande"),
 | 
			
		||||
                    ("America/Cancun", "America/Cancun"),
 | 
			
		||||
                    ("America/Caracas", "America/Caracas"),
 | 
			
		||||
                    ("America/Catamarca", "America/Catamarca"),
 | 
			
		||||
                    ("America/Cayenne", "America/Cayenne"),
 | 
			
		||||
                    ("America/Cayman", "America/Cayman"),
 | 
			
		||||
                    ("America/Chicago", "America/Chicago"),
 | 
			
		||||
                    ("America/Chihuahua", "America/Chihuahua"),
 | 
			
		||||
                    ("America/Coral_Harbour", "America/Coral_Harbour"),
 | 
			
		||||
                    ("America/Cordoba", "America/Cordoba"),
 | 
			
		||||
                    ("America/Costa_Rica", "America/Costa_Rica"),
 | 
			
		||||
                    ("America/Creston", "America/Creston"),
 | 
			
		||||
                    ("America/Cuiaba", "America/Cuiaba"),
 | 
			
		||||
                    ("America/Curacao", "America/Curacao"),
 | 
			
		||||
                    ("America/Danmarkshavn", "America/Danmarkshavn"),
 | 
			
		||||
                    ("America/Dawson", "America/Dawson"),
 | 
			
		||||
                    ("America/Dawson_Creek", "America/Dawson_Creek"),
 | 
			
		||||
                    ("America/Denver", "America/Denver"),
 | 
			
		||||
                    ("America/Detroit", "America/Detroit"),
 | 
			
		||||
                    ("America/Dominica", "America/Dominica"),
 | 
			
		||||
                    ("America/Edmonton", "America/Edmonton"),
 | 
			
		||||
                    ("America/Eirunepe", "America/Eirunepe"),
 | 
			
		||||
                    ("America/El_Salvador", "America/El_Salvador"),
 | 
			
		||||
                    ("America/Ensenada", "America/Ensenada"),
 | 
			
		||||
                    ("America/Fort_Nelson", "America/Fort_Nelson"),
 | 
			
		||||
                    ("America/Fort_Wayne", "America/Fort_Wayne"),
 | 
			
		||||
                    ("America/Fortaleza", "America/Fortaleza"),
 | 
			
		||||
                    ("America/Glace_Bay", "America/Glace_Bay"),
 | 
			
		||||
                    ("America/Godthab", "America/Godthab"),
 | 
			
		||||
                    ("America/Goose_Bay", "America/Goose_Bay"),
 | 
			
		||||
                    ("America/Grand_Turk", "America/Grand_Turk"),
 | 
			
		||||
                    ("America/Grenada", "America/Grenada"),
 | 
			
		||||
                    ("America/Guadeloupe", "America/Guadeloupe"),
 | 
			
		||||
                    ("America/Guatemala", "America/Guatemala"),
 | 
			
		||||
                    ("America/Guayaquil", "America/Guayaquil"),
 | 
			
		||||
                    ("America/Guyana", "America/Guyana"),
 | 
			
		||||
                    ("America/Halifax", "America/Halifax"),
 | 
			
		||||
                    ("America/Havana", "America/Havana"),
 | 
			
		||||
                    ("America/Hermosillo", "America/Hermosillo"),
 | 
			
		||||
                    ("America/Indiana/Indianapolis", "America/Indiana/Indianapolis"),
 | 
			
		||||
                    ("America/Indiana/Knox", "America/Indiana/Knox"),
 | 
			
		||||
                    ("America/Indiana/Marengo", "America/Indiana/Marengo"),
 | 
			
		||||
                    ("America/Indiana/Petersburg", "America/Indiana/Petersburg"),
 | 
			
		||||
                    ("America/Indiana/Tell_City", "America/Indiana/Tell_City"),
 | 
			
		||||
                    ("America/Indiana/Vevay", "America/Indiana/Vevay"),
 | 
			
		||||
                    ("America/Indiana/Vincennes", "America/Indiana/Vincennes"),
 | 
			
		||||
                    ("America/Indiana/Winamac", "America/Indiana/Winamac"),
 | 
			
		||||
                    ("America/Indianapolis", "America/Indianapolis"),
 | 
			
		||||
                    ("America/Inuvik", "America/Inuvik"),
 | 
			
		||||
                    ("America/Iqaluit", "America/Iqaluit"),
 | 
			
		||||
                    ("America/Jamaica", "America/Jamaica"),
 | 
			
		||||
                    ("America/Jujuy", "America/Jujuy"),
 | 
			
		||||
                    ("America/Juneau", "America/Juneau"),
 | 
			
		||||
                    ("America/Kentucky/Louisville", "America/Kentucky/Louisville"),
 | 
			
		||||
                    ("America/Kentucky/Monticello", "America/Kentucky/Monticello"),
 | 
			
		||||
                    ("America/Knox_IN", "America/Knox_IN"),
 | 
			
		||||
                    ("America/Kralendijk", "America/Kralendijk"),
 | 
			
		||||
                    ("America/La_Paz", "America/La_Paz"),
 | 
			
		||||
                    ("America/Lima", "America/Lima"),
 | 
			
		||||
                    ("America/Los_Angeles", "America/Los_Angeles"),
 | 
			
		||||
                    ("America/Louisville", "America/Louisville"),
 | 
			
		||||
                    ("America/Lower_Princes", "America/Lower_Princes"),
 | 
			
		||||
                    ("America/Maceio", "America/Maceio"),
 | 
			
		||||
                    ("America/Managua", "America/Managua"),
 | 
			
		||||
                    ("America/Manaus", "America/Manaus"),
 | 
			
		||||
                    ("America/Marigot", "America/Marigot"),
 | 
			
		||||
                    ("America/Martinique", "America/Martinique"),
 | 
			
		||||
                    ("America/Matamoros", "America/Matamoros"),
 | 
			
		||||
                    ("America/Mazatlan", "America/Mazatlan"),
 | 
			
		||||
                    ("America/Mendoza", "America/Mendoza"),
 | 
			
		||||
                    ("America/Menominee", "America/Menominee"),
 | 
			
		||||
                    ("America/Merida", "America/Merida"),
 | 
			
		||||
                    ("America/Metlakatla", "America/Metlakatla"),
 | 
			
		||||
                    ("America/Mexico_City", "America/Mexico_City"),
 | 
			
		||||
                    ("America/Miquelon", "America/Miquelon"),
 | 
			
		||||
                    ("America/Moncton", "America/Moncton"),
 | 
			
		||||
                    ("America/Monterrey", "America/Monterrey"),
 | 
			
		||||
                    ("America/Montevideo", "America/Montevideo"),
 | 
			
		||||
                    ("America/Montreal", "America/Montreal"),
 | 
			
		||||
                    ("America/Montserrat", "America/Montserrat"),
 | 
			
		||||
                    ("America/Nassau", "America/Nassau"),
 | 
			
		||||
                    ("America/New_York", "America/New_York"),
 | 
			
		||||
                    ("America/Nipigon", "America/Nipigon"),
 | 
			
		||||
                    ("America/Nome", "America/Nome"),
 | 
			
		||||
                    ("America/Noronha", "America/Noronha"),
 | 
			
		||||
                    ("America/North_Dakota/Beulah", "America/North_Dakota/Beulah"),
 | 
			
		||||
                    ("America/North_Dakota/Center", "America/North_Dakota/Center"),
 | 
			
		||||
                    (
 | 
			
		||||
                        "America/North_Dakota/New_Salem",
 | 
			
		||||
                        "America/North_Dakota/New_Salem",
 | 
			
		||||
                    ),
 | 
			
		||||
                    ("America/Nuuk", "America/Nuuk"),
 | 
			
		||||
                    ("America/Ojinaga", "America/Ojinaga"),
 | 
			
		||||
                    ("America/Panama", "America/Panama"),
 | 
			
		||||
                    ("America/Pangnirtung", "America/Pangnirtung"),
 | 
			
		||||
                    ("America/Paramaribo", "America/Paramaribo"),
 | 
			
		||||
                    ("America/Phoenix", "America/Phoenix"),
 | 
			
		||||
                    ("America/Port-au-Prince", "America/Port-au-Prince"),
 | 
			
		||||
                    ("America/Port_of_Spain", "America/Port_of_Spain"),
 | 
			
		||||
                    ("America/Porto_Acre", "America/Porto_Acre"),
 | 
			
		||||
                    ("America/Porto_Velho", "America/Porto_Velho"),
 | 
			
		||||
                    ("America/Puerto_Rico", "America/Puerto_Rico"),
 | 
			
		||||
                    ("America/Punta_Arenas", "America/Punta_Arenas"),
 | 
			
		||||
                    ("America/Rainy_River", "America/Rainy_River"),
 | 
			
		||||
                    ("America/Rankin_Inlet", "America/Rankin_Inlet"),
 | 
			
		||||
                    ("America/Recife", "America/Recife"),
 | 
			
		||||
                    ("America/Regina", "America/Regina"),
 | 
			
		||||
                    ("America/Resolute", "America/Resolute"),
 | 
			
		||||
                    ("America/Rio_Branco", "America/Rio_Branco"),
 | 
			
		||||
                    ("America/Rosario", "America/Rosario"),
 | 
			
		||||
                    ("America/Santa_Isabel", "America/Santa_Isabel"),
 | 
			
		||||
                    ("America/Santarem", "America/Santarem"),
 | 
			
		||||
                    ("America/Santiago", "America/Santiago"),
 | 
			
		||||
                    ("America/Santo_Domingo", "America/Santo_Domingo"),
 | 
			
		||||
                    ("America/Sao_Paulo", "America/Sao_Paulo"),
 | 
			
		||||
                    ("America/Scoresbysund", "America/Scoresbysund"),
 | 
			
		||||
                    ("America/Shiprock", "America/Shiprock"),
 | 
			
		||||
                    ("America/Sitka", "America/Sitka"),
 | 
			
		||||
                    ("America/St_Barthelemy", "America/St_Barthelemy"),
 | 
			
		||||
                    ("America/St_Johns", "America/St_Johns"),
 | 
			
		||||
                    ("America/St_Kitts", "America/St_Kitts"),
 | 
			
		||||
                    ("America/St_Lucia", "America/St_Lucia"),
 | 
			
		||||
                    ("America/St_Thomas", "America/St_Thomas"),
 | 
			
		||||
                    ("America/St_Vincent", "America/St_Vincent"),
 | 
			
		||||
                    ("America/Swift_Current", "America/Swift_Current"),
 | 
			
		||||
                    ("America/Tegucigalpa", "America/Tegucigalpa"),
 | 
			
		||||
                    ("America/Thule", "America/Thule"),
 | 
			
		||||
                    ("America/Thunder_Bay", "America/Thunder_Bay"),
 | 
			
		||||
                    ("America/Tijuana", "America/Tijuana"),
 | 
			
		||||
                    ("America/Toronto", "America/Toronto"),
 | 
			
		||||
                    ("America/Tortola", "America/Tortola"),
 | 
			
		||||
                    ("America/Vancouver", "America/Vancouver"),
 | 
			
		||||
                    ("America/Virgin", "America/Virgin"),
 | 
			
		||||
                    ("America/Whitehorse", "America/Whitehorse"),
 | 
			
		||||
                    ("America/Winnipeg", "America/Winnipeg"),
 | 
			
		||||
                    ("America/Yakutat", "America/Yakutat"),
 | 
			
		||||
                    ("America/Yellowknife", "America/Yellowknife"),
 | 
			
		||||
                    ("Antarctica/Casey", "Antarctica/Casey"),
 | 
			
		||||
                    ("Antarctica/Davis", "Antarctica/Davis"),
 | 
			
		||||
                    ("Antarctica/DumontDUrville", "Antarctica/DumontDUrville"),
 | 
			
		||||
                    ("Antarctica/Macquarie", "Antarctica/Macquarie"),
 | 
			
		||||
                    ("Antarctica/Mawson", "Antarctica/Mawson"),
 | 
			
		||||
                    ("Antarctica/McMurdo", "Antarctica/McMurdo"),
 | 
			
		||||
                    ("Antarctica/Palmer", "Antarctica/Palmer"),
 | 
			
		||||
                    ("Antarctica/Rothera", "Antarctica/Rothera"),
 | 
			
		||||
                    ("Antarctica/South_Pole", "Antarctica/South_Pole"),
 | 
			
		||||
                    ("Antarctica/Syowa", "Antarctica/Syowa"),
 | 
			
		||||
                    ("Antarctica/Troll", "Antarctica/Troll"),
 | 
			
		||||
                    ("Antarctica/Vostok", "Antarctica/Vostok"),
 | 
			
		||||
                    ("Arctic/Longyearbyen", "Arctic/Longyearbyen"),
 | 
			
		||||
                    ("Asia/Aden", "Asia/Aden"),
 | 
			
		||||
                    ("Asia/Almaty", "Asia/Almaty"),
 | 
			
		||||
                    ("Asia/Amman", "Asia/Amman"),
 | 
			
		||||
                    ("Asia/Anadyr", "Asia/Anadyr"),
 | 
			
		||||
                    ("Asia/Aqtau", "Asia/Aqtau"),
 | 
			
		||||
                    ("Asia/Aqtobe", "Asia/Aqtobe"),
 | 
			
		||||
                    ("Asia/Ashgabat", "Asia/Ashgabat"),
 | 
			
		||||
                    ("Asia/Ashkhabad", "Asia/Ashkhabad"),
 | 
			
		||||
                    ("Asia/Atyrau", "Asia/Atyrau"),
 | 
			
		||||
                    ("Asia/Baghdad", "Asia/Baghdad"),
 | 
			
		||||
                    ("Asia/Bahrain", "Asia/Bahrain"),
 | 
			
		||||
                    ("Asia/Baku", "Asia/Baku"),
 | 
			
		||||
                    ("Asia/Bangkok", "Asia/Bangkok"),
 | 
			
		||||
                    ("Asia/Barnaul", "Asia/Barnaul"),
 | 
			
		||||
                    ("Asia/Beirut", "Asia/Beirut"),
 | 
			
		||||
                    ("Asia/Bishkek", "Asia/Bishkek"),
 | 
			
		||||
                    ("Asia/Brunei", "Asia/Brunei"),
 | 
			
		||||
                    ("Asia/Calcutta", "Asia/Calcutta"),
 | 
			
		||||
                    ("Asia/Chita", "Asia/Chita"),
 | 
			
		||||
                    ("Asia/Choibalsan", "Asia/Choibalsan"),
 | 
			
		||||
                    ("Asia/Chongqing", "Asia/Chongqing"),
 | 
			
		||||
                    ("Asia/Chungking", "Asia/Chungking"),
 | 
			
		||||
                    ("Asia/Colombo", "Asia/Colombo"),
 | 
			
		||||
                    ("Asia/Dacca", "Asia/Dacca"),
 | 
			
		||||
                    ("Asia/Damascus", "Asia/Damascus"),
 | 
			
		||||
                    ("Asia/Dhaka", "Asia/Dhaka"),
 | 
			
		||||
                    ("Asia/Dili", "Asia/Dili"),
 | 
			
		||||
                    ("Asia/Dubai", "Asia/Dubai"),
 | 
			
		||||
                    ("Asia/Dushanbe", "Asia/Dushanbe"),
 | 
			
		||||
                    ("Asia/Famagusta", "Asia/Famagusta"),
 | 
			
		||||
                    ("Asia/Gaza", "Asia/Gaza"),
 | 
			
		||||
                    ("Asia/Harbin", "Asia/Harbin"),
 | 
			
		||||
                    ("Asia/Hebron", "Asia/Hebron"),
 | 
			
		||||
                    ("Asia/Ho_Chi_Minh", "Asia/Ho_Chi_Minh"),
 | 
			
		||||
                    ("Asia/Hong_Kong", "Asia/Hong_Kong"),
 | 
			
		||||
                    ("Asia/Hovd", "Asia/Hovd"),
 | 
			
		||||
                    ("Asia/Irkutsk", "Asia/Irkutsk"),
 | 
			
		||||
                    ("Asia/Istanbul", "Asia/Istanbul"),
 | 
			
		||||
                    ("Asia/Jakarta", "Asia/Jakarta"),
 | 
			
		||||
                    ("Asia/Jayapura", "Asia/Jayapura"),
 | 
			
		||||
                    ("Asia/Jerusalem", "Asia/Jerusalem"),
 | 
			
		||||
                    ("Asia/Kabul", "Asia/Kabul"),
 | 
			
		||||
                    ("Asia/Kamchatka", "Asia/Kamchatka"),
 | 
			
		||||
                    ("Asia/Karachi", "Asia/Karachi"),
 | 
			
		||||
                    ("Asia/Kashgar", "Asia/Kashgar"),
 | 
			
		||||
                    ("Asia/Kathmandu", "Asia/Kathmandu"),
 | 
			
		||||
                    ("Asia/Katmandu", "Asia/Katmandu"),
 | 
			
		||||
                    ("Asia/Khandyga", "Asia/Khandyga"),
 | 
			
		||||
                    ("Asia/Kolkata", "Asia/Kolkata"),
 | 
			
		||||
                    ("Asia/Krasnoyarsk", "Asia/Krasnoyarsk"),
 | 
			
		||||
                    ("Asia/Kuala_Lumpur", "Asia/Kuala_Lumpur"),
 | 
			
		||||
                    ("Asia/Kuching", "Asia/Kuching"),
 | 
			
		||||
                    ("Asia/Kuwait", "Asia/Kuwait"),
 | 
			
		||||
                    ("Asia/Macao", "Asia/Macao"),
 | 
			
		||||
                    ("Asia/Macau", "Asia/Macau"),
 | 
			
		||||
                    ("Asia/Magadan", "Asia/Magadan"),
 | 
			
		||||
                    ("Asia/Makassar", "Asia/Makassar"),
 | 
			
		||||
                    ("Asia/Manila", "Asia/Manila"),
 | 
			
		||||
                    ("Asia/Muscat", "Asia/Muscat"),
 | 
			
		||||
                    ("Asia/Nicosia", "Asia/Nicosia"),
 | 
			
		||||
                    ("Asia/Novokuznetsk", "Asia/Novokuznetsk"),
 | 
			
		||||
                    ("Asia/Novosibirsk", "Asia/Novosibirsk"),
 | 
			
		||||
                    ("Asia/Omsk", "Asia/Omsk"),
 | 
			
		||||
                    ("Asia/Oral", "Asia/Oral"),
 | 
			
		||||
                    ("Asia/Phnom_Penh", "Asia/Phnom_Penh"),
 | 
			
		||||
                    ("Asia/Pontianak", "Asia/Pontianak"),
 | 
			
		||||
                    ("Asia/Pyongyang", "Asia/Pyongyang"),
 | 
			
		||||
                    ("Asia/Qatar", "Asia/Qatar"),
 | 
			
		||||
                    ("Asia/Qostanay", "Asia/Qostanay"),
 | 
			
		||||
                    ("Asia/Qyzylorda", "Asia/Qyzylorda"),
 | 
			
		||||
                    ("Asia/Rangoon", "Asia/Rangoon"),
 | 
			
		||||
                    ("Asia/Riyadh", "Asia/Riyadh"),
 | 
			
		||||
                    ("Asia/Saigon", "Asia/Saigon"),
 | 
			
		||||
                    ("Asia/Sakhalin", "Asia/Sakhalin"),
 | 
			
		||||
                    ("Asia/Samarkand", "Asia/Samarkand"),
 | 
			
		||||
                    ("Asia/Seoul", "Asia/Seoul"),
 | 
			
		||||
                    ("Asia/Shanghai", "Asia/Shanghai"),
 | 
			
		||||
                    ("Asia/Singapore", "Asia/Singapore"),
 | 
			
		||||
                    ("Asia/Srednekolymsk", "Asia/Srednekolymsk"),
 | 
			
		||||
                    ("Asia/Taipei", "Asia/Taipei"),
 | 
			
		||||
                    ("Asia/Tashkent", "Asia/Tashkent"),
 | 
			
		||||
                    ("Asia/Tbilisi", "Asia/Tbilisi"),
 | 
			
		||||
                    ("Asia/Tehran", "Asia/Tehran"),
 | 
			
		||||
                    ("Asia/Tel_Aviv", "Asia/Tel_Aviv"),
 | 
			
		||||
                    ("Asia/Thimbu", "Asia/Thimbu"),
 | 
			
		||||
                    ("Asia/Thimphu", "Asia/Thimphu"),
 | 
			
		||||
                    ("Asia/Tokyo", "Asia/Tokyo"),
 | 
			
		||||
                    ("Asia/Tomsk", "Asia/Tomsk"),
 | 
			
		||||
                    ("Asia/Ujung_Pandang", "Asia/Ujung_Pandang"),
 | 
			
		||||
                    ("Asia/Ulaanbaatar", "Asia/Ulaanbaatar"),
 | 
			
		||||
                    ("Asia/Ulan_Bator", "Asia/Ulan_Bator"),
 | 
			
		||||
                    ("Asia/Urumqi", "Asia/Urumqi"),
 | 
			
		||||
                    ("Asia/Ust-Nera", "Asia/Ust-Nera"),
 | 
			
		||||
                    ("Asia/Vientiane", "Asia/Vientiane"),
 | 
			
		||||
                    ("Asia/Vladivostok", "Asia/Vladivostok"),
 | 
			
		||||
                    ("Asia/Yakutsk", "Asia/Yakutsk"),
 | 
			
		||||
                    ("Asia/Yangon", "Asia/Yangon"),
 | 
			
		||||
                    ("Asia/Yekaterinburg", "Asia/Yekaterinburg"),
 | 
			
		||||
                    ("Asia/Yerevan", "Asia/Yerevan"),
 | 
			
		||||
                    ("Atlantic/Azores", "Atlantic/Azores"),
 | 
			
		||||
                    ("Atlantic/Bermuda", "Atlantic/Bermuda"),
 | 
			
		||||
                    ("Atlantic/Canary", "Atlantic/Canary"),
 | 
			
		||||
                    ("Atlantic/Cape_Verde", "Atlantic/Cape_Verde"),
 | 
			
		||||
                    ("Atlantic/Faeroe", "Atlantic/Faeroe"),
 | 
			
		||||
                    ("Atlantic/Faroe", "Atlantic/Faroe"),
 | 
			
		||||
                    ("Atlantic/Jan_Mayen", "Atlantic/Jan_Mayen"),
 | 
			
		||||
                    ("Atlantic/Madeira", "Atlantic/Madeira"),
 | 
			
		||||
                    ("Atlantic/Reykjavik", "Atlantic/Reykjavik"),
 | 
			
		||||
                    ("Atlantic/South_Georgia", "Atlantic/South_Georgia"),
 | 
			
		||||
                    ("Atlantic/St_Helena", "Atlantic/St_Helena"),
 | 
			
		||||
                    ("Atlantic/Stanley", "Atlantic/Stanley"),
 | 
			
		||||
                    ("Australia/ACT", "Australia/ACT"),
 | 
			
		||||
                    ("Australia/Adelaide", "Australia/Adelaide"),
 | 
			
		||||
                    ("Australia/Brisbane", "Australia/Brisbane"),
 | 
			
		||||
                    ("Australia/Broken_Hill", "Australia/Broken_Hill"),
 | 
			
		||||
                    ("Australia/Canberra", "Australia/Canberra"),
 | 
			
		||||
                    ("Australia/Currie", "Australia/Currie"),
 | 
			
		||||
                    ("Australia/Darwin", "Australia/Darwin"),
 | 
			
		||||
                    ("Australia/Eucla", "Australia/Eucla"),
 | 
			
		||||
                    ("Australia/Hobart", "Australia/Hobart"),
 | 
			
		||||
                    ("Australia/LHI", "Australia/LHI"),
 | 
			
		||||
                    ("Australia/Lindeman", "Australia/Lindeman"),
 | 
			
		||||
                    ("Australia/Lord_Howe", "Australia/Lord_Howe"),
 | 
			
		||||
                    ("Australia/Melbourne", "Australia/Melbourne"),
 | 
			
		||||
                    ("Australia/NSW", "Australia/NSW"),
 | 
			
		||||
                    ("Australia/North", "Australia/North"),
 | 
			
		||||
                    ("Australia/Perth", "Australia/Perth"),
 | 
			
		||||
                    ("Australia/Queensland", "Australia/Queensland"),
 | 
			
		||||
                    ("Australia/South", "Australia/South"),
 | 
			
		||||
                    ("Australia/Sydney", "Australia/Sydney"),
 | 
			
		||||
                    ("Australia/Tasmania", "Australia/Tasmania"),
 | 
			
		||||
                    ("Australia/Victoria", "Australia/Victoria"),
 | 
			
		||||
                    ("Australia/West", "Australia/West"),
 | 
			
		||||
                    ("Australia/Yancowinna", "Australia/Yancowinna"),
 | 
			
		||||
                    ("Brazil/Acre", "Brazil/Acre"),
 | 
			
		||||
                    ("Brazil/DeNoronha", "Brazil/DeNoronha"),
 | 
			
		||||
                    ("Brazil/East", "Brazil/East"),
 | 
			
		||||
                    ("Brazil/West", "Brazil/West"),
 | 
			
		||||
                    ("CET", "CET"),
 | 
			
		||||
                    ("CST6CDT", "CST6CDT"),
 | 
			
		||||
                    ("Canada/Atlantic", "Canada/Atlantic"),
 | 
			
		||||
                    ("Canada/Central", "Canada/Central"),
 | 
			
		||||
                    ("Canada/Eastern", "Canada/Eastern"),
 | 
			
		||||
                    ("Canada/Mountain", "Canada/Mountain"),
 | 
			
		||||
                    ("Canada/Newfoundland", "Canada/Newfoundland"),
 | 
			
		||||
                    ("Canada/Pacific", "Canada/Pacific"),
 | 
			
		||||
                    ("Canada/Saskatchewan", "Canada/Saskatchewan"),
 | 
			
		||||
                    ("Canada/Yukon", "Canada/Yukon"),
 | 
			
		||||
                    ("Chile/Continental", "Chile/Continental"),
 | 
			
		||||
                    ("Chile/EasterIsland", "Chile/EasterIsland"),
 | 
			
		||||
                    ("Cuba", "Cuba"),
 | 
			
		||||
                    ("EET", "EET"),
 | 
			
		||||
                    ("EST", "EST"),
 | 
			
		||||
                    ("EST5EDT", "EST5EDT"),
 | 
			
		||||
                    ("Egypt", "Egypt"),
 | 
			
		||||
                    ("Eire", "Eire"),
 | 
			
		||||
                    ("Etc/GMT", "Etc/GMT"),
 | 
			
		||||
                    ("Etc/GMT+0", "Etc/GMT+0"),
 | 
			
		||||
                    ("Etc/GMT+1", "Etc/GMT+1"),
 | 
			
		||||
                    ("Etc/GMT+10", "Etc/GMT+10"),
 | 
			
		||||
                    ("Etc/GMT+11", "Etc/GMT+11"),
 | 
			
		||||
                    ("Etc/GMT+12", "Etc/GMT+12"),
 | 
			
		||||
                    ("Etc/GMT+2", "Etc/GMT+2"),
 | 
			
		||||
                    ("Etc/GMT+3", "Etc/GMT+3"),
 | 
			
		||||
                    ("Etc/GMT+4", "Etc/GMT+4"),
 | 
			
		||||
                    ("Etc/GMT+5", "Etc/GMT+5"),
 | 
			
		||||
                    ("Etc/GMT+6", "Etc/GMT+6"),
 | 
			
		||||
                    ("Etc/GMT+7", "Etc/GMT+7"),
 | 
			
		||||
                    ("Etc/GMT+8", "Etc/GMT+8"),
 | 
			
		||||
                    ("Etc/GMT+9", "Etc/GMT+9"),
 | 
			
		||||
                    ("Etc/GMT-0", "Etc/GMT-0"),
 | 
			
		||||
                    ("Etc/GMT-1", "Etc/GMT-1"),
 | 
			
		||||
                    ("Etc/GMT-10", "Etc/GMT-10"),
 | 
			
		||||
                    ("Etc/GMT-11", "Etc/GMT-11"),
 | 
			
		||||
                    ("Etc/GMT-12", "Etc/GMT-12"),
 | 
			
		||||
                    ("Etc/GMT-13", "Etc/GMT-13"),
 | 
			
		||||
                    ("Etc/GMT-14", "Etc/GMT-14"),
 | 
			
		||||
                    ("Etc/GMT-2", "Etc/GMT-2"),
 | 
			
		||||
                    ("Etc/GMT-3", "Etc/GMT-3"),
 | 
			
		||||
                    ("Etc/GMT-4", "Etc/GMT-4"),
 | 
			
		||||
                    ("Etc/GMT-5", "Etc/GMT-5"),
 | 
			
		||||
                    ("Etc/GMT-6", "Etc/GMT-6"),
 | 
			
		||||
                    ("Etc/GMT-7", "Etc/GMT-7"),
 | 
			
		||||
                    ("Etc/GMT-8", "Etc/GMT-8"),
 | 
			
		||||
                    ("Etc/GMT-9", "Etc/GMT-9"),
 | 
			
		||||
                    ("Etc/GMT0", "Etc/GMT0"),
 | 
			
		||||
                    ("Etc/Greenwich", "Etc/Greenwich"),
 | 
			
		||||
                    ("Etc/UCT", "Etc/UCT"),
 | 
			
		||||
                    ("Etc/UTC", "Etc/UTC"),
 | 
			
		||||
                    ("Etc/Universal", "Etc/Universal"),
 | 
			
		||||
                    ("Etc/Zulu", "Etc/Zulu"),
 | 
			
		||||
                    ("Europe/Amsterdam", "Europe/Amsterdam"),
 | 
			
		||||
                    ("Europe/Andorra", "Europe/Andorra"),
 | 
			
		||||
                    ("Europe/Astrakhan", "Europe/Astrakhan"),
 | 
			
		||||
                    ("Europe/Athens", "Europe/Athens"),
 | 
			
		||||
                    ("Europe/Belfast", "Europe/Belfast"),
 | 
			
		||||
                    ("Europe/Belgrade", "Europe/Belgrade"),
 | 
			
		||||
                    ("Europe/Berlin", "Europe/Berlin"),
 | 
			
		||||
                    ("Europe/Bratislava", "Europe/Bratislava"),
 | 
			
		||||
                    ("Europe/Brussels", "Europe/Brussels"),
 | 
			
		||||
                    ("Europe/Bucharest", "Europe/Bucharest"),
 | 
			
		||||
                    ("Europe/Budapest", "Europe/Budapest"),
 | 
			
		||||
                    ("Europe/Busingen", "Europe/Busingen"),
 | 
			
		||||
                    ("Europe/Chisinau", "Europe/Chisinau"),
 | 
			
		||||
                    ("Europe/Copenhagen", "Europe/Copenhagen"),
 | 
			
		||||
                    ("Europe/Dublin", "Europe/Dublin"),
 | 
			
		||||
                    ("Europe/Gibraltar", "Europe/Gibraltar"),
 | 
			
		||||
                    ("Europe/Guernsey", "Europe/Guernsey"),
 | 
			
		||||
                    ("Europe/Helsinki", "Europe/Helsinki"),
 | 
			
		||||
                    ("Europe/Isle_of_Man", "Europe/Isle_of_Man"),
 | 
			
		||||
                    ("Europe/Istanbul", "Europe/Istanbul"),
 | 
			
		||||
                    ("Europe/Jersey", "Europe/Jersey"),
 | 
			
		||||
                    ("Europe/Kaliningrad", "Europe/Kaliningrad"),
 | 
			
		||||
                    ("Europe/Kiev", "Europe/Kiev"),
 | 
			
		||||
                    ("Europe/Kirov", "Europe/Kirov"),
 | 
			
		||||
                    ("Europe/Kyiv", "Europe/Kyiv"),
 | 
			
		||||
                    ("Europe/Lisbon", "Europe/Lisbon"),
 | 
			
		||||
                    ("Europe/Ljubljana", "Europe/Ljubljana"),
 | 
			
		||||
                    ("Europe/London", "Europe/London"),
 | 
			
		||||
                    ("Europe/Luxembourg", "Europe/Luxembourg"),
 | 
			
		||||
                    ("Europe/Madrid", "Europe/Madrid"),
 | 
			
		||||
                    ("Europe/Malta", "Europe/Malta"),
 | 
			
		||||
                    ("Europe/Mariehamn", "Europe/Mariehamn"),
 | 
			
		||||
                    ("Europe/Minsk", "Europe/Minsk"),
 | 
			
		||||
                    ("Europe/Monaco", "Europe/Monaco"),
 | 
			
		||||
                    ("Europe/Moscow", "Europe/Moscow"),
 | 
			
		||||
                    ("Europe/Nicosia", "Europe/Nicosia"),
 | 
			
		||||
                    ("Europe/Oslo", "Europe/Oslo"),
 | 
			
		||||
                    ("Europe/Paris", "Europe/Paris"),
 | 
			
		||||
                    ("Europe/Podgorica", "Europe/Podgorica"),
 | 
			
		||||
                    ("Europe/Prague", "Europe/Prague"),
 | 
			
		||||
                    ("Europe/Riga", "Europe/Riga"),
 | 
			
		||||
                    ("Europe/Rome", "Europe/Rome"),
 | 
			
		||||
                    ("Europe/Samara", "Europe/Samara"),
 | 
			
		||||
                    ("Europe/San_Marino", "Europe/San_Marino"),
 | 
			
		||||
                    ("Europe/Sarajevo", "Europe/Sarajevo"),
 | 
			
		||||
                    ("Europe/Saratov", "Europe/Saratov"),
 | 
			
		||||
                    ("Europe/Simferopol", "Europe/Simferopol"),
 | 
			
		||||
                    ("Europe/Skopje", "Europe/Skopje"),
 | 
			
		||||
                    ("Europe/Sofia", "Europe/Sofia"),
 | 
			
		||||
                    ("Europe/Stockholm", "Europe/Stockholm"),
 | 
			
		||||
                    ("Europe/Tallinn", "Europe/Tallinn"),
 | 
			
		||||
                    ("Europe/Tirane", "Europe/Tirane"),
 | 
			
		||||
                    ("Europe/Tiraspol", "Europe/Tiraspol"),
 | 
			
		||||
                    ("Europe/Ulyanovsk", "Europe/Ulyanovsk"),
 | 
			
		||||
                    ("Europe/Uzhgorod", "Europe/Uzhgorod"),
 | 
			
		||||
                    ("Europe/Vaduz", "Europe/Vaduz"),
 | 
			
		||||
                    ("Europe/Vatican", "Europe/Vatican"),
 | 
			
		||||
                    ("Europe/Vienna", "Europe/Vienna"),
 | 
			
		||||
                    ("Europe/Vilnius", "Europe/Vilnius"),
 | 
			
		||||
                    ("Europe/Volgograd", "Europe/Volgograd"),
 | 
			
		||||
                    ("Europe/Warsaw", "Europe/Warsaw"),
 | 
			
		||||
                    ("Europe/Zagreb", "Europe/Zagreb"),
 | 
			
		||||
                    ("Europe/Zaporozhye", "Europe/Zaporozhye"),
 | 
			
		||||
                    ("Europe/Zurich", "Europe/Zurich"),
 | 
			
		||||
                    ("GB", "GB"),
 | 
			
		||||
                    ("GB-Eire", "GB-Eire"),
 | 
			
		||||
                    ("GMT", "GMT"),
 | 
			
		||||
                    ("GMT+0", "GMT+0"),
 | 
			
		||||
                    ("GMT-0", "GMT-0"),
 | 
			
		||||
                    ("GMT0", "GMT0"),
 | 
			
		||||
                    ("Greenwich", "Greenwich"),
 | 
			
		||||
                    ("HST", "HST"),
 | 
			
		||||
                    ("Hongkong", "Hongkong"),
 | 
			
		||||
                    ("Iceland", "Iceland"),
 | 
			
		||||
                    ("Indian/Antananarivo", "Indian/Antananarivo"),
 | 
			
		||||
                    ("Indian/Chagos", "Indian/Chagos"),
 | 
			
		||||
                    ("Indian/Christmas", "Indian/Christmas"),
 | 
			
		||||
                    ("Indian/Cocos", "Indian/Cocos"),
 | 
			
		||||
                    ("Indian/Comoro", "Indian/Comoro"),
 | 
			
		||||
                    ("Indian/Kerguelen", "Indian/Kerguelen"),
 | 
			
		||||
                    ("Indian/Mahe", "Indian/Mahe"),
 | 
			
		||||
                    ("Indian/Maldives", "Indian/Maldives"),
 | 
			
		||||
                    ("Indian/Mauritius", "Indian/Mauritius"),
 | 
			
		||||
                    ("Indian/Mayotte", "Indian/Mayotte"),
 | 
			
		||||
                    ("Indian/Reunion", "Indian/Reunion"),
 | 
			
		||||
                    ("Iran", "Iran"),
 | 
			
		||||
                    ("Israel", "Israel"),
 | 
			
		||||
                    ("Jamaica", "Jamaica"),
 | 
			
		||||
                    ("Japan", "Japan"),
 | 
			
		||||
                    ("Kwajalein", "Kwajalein"),
 | 
			
		||||
                    ("Libya", "Libya"),
 | 
			
		||||
                    ("MET", "MET"),
 | 
			
		||||
                    ("MST", "MST"),
 | 
			
		||||
                    ("MST7MDT", "MST7MDT"),
 | 
			
		||||
                    ("Mexico/BajaNorte", "Mexico/BajaNorte"),
 | 
			
		||||
                    ("Mexico/BajaSur", "Mexico/BajaSur"),
 | 
			
		||||
                    ("Mexico/General", "Mexico/General"),
 | 
			
		||||
                    ("NZ", "NZ"),
 | 
			
		||||
                    ("NZ-CHAT", "NZ-CHAT"),
 | 
			
		||||
                    ("Navajo", "Navajo"),
 | 
			
		||||
                    ("PRC", "PRC"),
 | 
			
		||||
                    ("PST8PDT", "PST8PDT"),
 | 
			
		||||
                    ("Pacific/Apia", "Pacific/Apia"),
 | 
			
		||||
                    ("Pacific/Auckland", "Pacific/Auckland"),
 | 
			
		||||
                    ("Pacific/Bougainville", "Pacific/Bougainville"),
 | 
			
		||||
                    ("Pacific/Chatham", "Pacific/Chatham"),
 | 
			
		||||
                    ("Pacific/Chuuk", "Pacific/Chuuk"),
 | 
			
		||||
                    ("Pacific/Easter", "Pacific/Easter"),
 | 
			
		||||
                    ("Pacific/Efate", "Pacific/Efate"),
 | 
			
		||||
                    ("Pacific/Enderbury", "Pacific/Enderbury"),
 | 
			
		||||
                    ("Pacific/Fakaofo", "Pacific/Fakaofo"),
 | 
			
		||||
                    ("Pacific/Fiji", "Pacific/Fiji"),
 | 
			
		||||
                    ("Pacific/Funafuti", "Pacific/Funafuti"),
 | 
			
		||||
                    ("Pacific/Galapagos", "Pacific/Galapagos"),
 | 
			
		||||
                    ("Pacific/Gambier", "Pacific/Gambier"),
 | 
			
		||||
                    ("Pacific/Guadalcanal", "Pacific/Guadalcanal"),
 | 
			
		||||
                    ("Pacific/Guam", "Pacific/Guam"),
 | 
			
		||||
                    ("Pacific/Honolulu", "Pacific/Honolulu"),
 | 
			
		||||
                    ("Pacific/Johnston", "Pacific/Johnston"),
 | 
			
		||||
                    ("Pacific/Kanton", "Pacific/Kanton"),
 | 
			
		||||
                    ("Pacific/Kiritimati", "Pacific/Kiritimati"),
 | 
			
		||||
                    ("Pacific/Kosrae", "Pacific/Kosrae"),
 | 
			
		||||
                    ("Pacific/Kwajalein", "Pacific/Kwajalein"),
 | 
			
		||||
                    ("Pacific/Majuro", "Pacific/Majuro"),
 | 
			
		||||
                    ("Pacific/Marquesas", "Pacific/Marquesas"),
 | 
			
		||||
                    ("Pacific/Midway", "Pacific/Midway"),
 | 
			
		||||
                    ("Pacific/Nauru", "Pacific/Nauru"),
 | 
			
		||||
                    ("Pacific/Niue", "Pacific/Niue"),
 | 
			
		||||
                    ("Pacific/Norfolk", "Pacific/Norfolk"),
 | 
			
		||||
                    ("Pacific/Noumea", "Pacific/Noumea"),
 | 
			
		||||
                    ("Pacific/Pago_Pago", "Pacific/Pago_Pago"),
 | 
			
		||||
                    ("Pacific/Palau", "Pacific/Palau"),
 | 
			
		||||
                    ("Pacific/Pitcairn", "Pacific/Pitcairn"),
 | 
			
		||||
                    ("Pacific/Pohnpei", "Pacific/Pohnpei"),
 | 
			
		||||
                    ("Pacific/Ponape", "Pacific/Ponape"),
 | 
			
		||||
                    ("Pacific/Port_Moresby", "Pacific/Port_Moresby"),
 | 
			
		||||
                    ("Pacific/Rarotonga", "Pacific/Rarotonga"),
 | 
			
		||||
                    ("Pacific/Saipan", "Pacific/Saipan"),
 | 
			
		||||
                    ("Pacific/Samoa", "Pacific/Samoa"),
 | 
			
		||||
                    ("Pacific/Tahiti", "Pacific/Tahiti"),
 | 
			
		||||
                    ("Pacific/Tarawa", "Pacific/Tarawa"),
 | 
			
		||||
                    ("Pacific/Tongatapu", "Pacific/Tongatapu"),
 | 
			
		||||
                    ("Pacific/Truk", "Pacific/Truk"),
 | 
			
		||||
                    ("Pacific/Wake", "Pacific/Wake"),
 | 
			
		||||
                    ("Pacific/Wallis", "Pacific/Wallis"),
 | 
			
		||||
                    ("Pacific/Yap", "Pacific/Yap"),
 | 
			
		||||
                    ("Poland", "Poland"),
 | 
			
		||||
                    ("Portugal", "Portugal"),
 | 
			
		||||
                    ("ROC", "ROC"),
 | 
			
		||||
                    ("ROK", "ROK"),
 | 
			
		||||
                    ("Singapore", "Singapore"),
 | 
			
		||||
                    ("Turkey", "Turkey"),
 | 
			
		||||
                    ("UCT", "UCT"),
 | 
			
		||||
                    ("US/Alaska", "US/Alaska"),
 | 
			
		||||
                    ("US/Aleutian", "US/Aleutian"),
 | 
			
		||||
                    ("US/Arizona", "US/Arizona"),
 | 
			
		||||
                    ("US/Central", "US/Central"),
 | 
			
		||||
                    ("US/East-Indiana", "US/East-Indiana"),
 | 
			
		||||
                    ("US/Eastern", "US/Eastern"),
 | 
			
		||||
                    ("US/Hawaii", "US/Hawaii"),
 | 
			
		||||
                    ("US/Indiana-Starke", "US/Indiana-Starke"),
 | 
			
		||||
                    ("US/Michigan", "US/Michigan"),
 | 
			
		||||
                    ("US/Mountain", "US/Mountain"),
 | 
			
		||||
                    ("US/Pacific", "US/Pacific"),
 | 
			
		||||
                    ("US/Samoa", "US/Samoa"),
 | 
			
		||||
                    ("UTC", "UTC"),
 | 
			
		||||
                    ("Universal", "Universal"),
 | 
			
		||||
                    ("W-SU", "W-SU"),
 | 
			
		||||
                    ("WET", "WET"),
 | 
			
		||||
                    ("Zulu", "Zulu"),
 | 
			
		||||
                ],
 | 
			
		||||
                max_length=255,
 | 
			
		||||
                null=True,
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										631
									
								
								api/tacticalrmm/agents/migrations/0056_alter_agent_time_zone.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										631
									
								
								api/tacticalrmm/agents/migrations/0056_alter_agent_time_zone.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,631 @@
 | 
			
		||||
# Generated by Django 4.1.7 on 2023-02-28 22:14
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("agents", "0055_alter_agent_time_zone"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="agent",
 | 
			
		||||
            name="time_zone",
 | 
			
		||||
            field=models.CharField(
 | 
			
		||||
                blank=True,
 | 
			
		||||
                choices=[
 | 
			
		||||
                    ("Africa/Abidjan", "Africa/Abidjan"),
 | 
			
		||||
                    ("Africa/Accra", "Africa/Accra"),
 | 
			
		||||
                    ("Africa/Addis_Ababa", "Africa/Addis_Ababa"),
 | 
			
		||||
                    ("Africa/Algiers", "Africa/Algiers"),
 | 
			
		||||
                    ("Africa/Asmara", "Africa/Asmara"),
 | 
			
		||||
                    ("Africa/Asmera", "Africa/Asmera"),
 | 
			
		||||
                    ("Africa/Bamako", "Africa/Bamako"),
 | 
			
		||||
                    ("Africa/Bangui", "Africa/Bangui"),
 | 
			
		||||
                    ("Africa/Banjul", "Africa/Banjul"),
 | 
			
		||||
                    ("Africa/Bissau", "Africa/Bissau"),
 | 
			
		||||
                    ("Africa/Blantyre", "Africa/Blantyre"),
 | 
			
		||||
                    ("Africa/Brazzaville", "Africa/Brazzaville"),
 | 
			
		||||
                    ("Africa/Bujumbura", "Africa/Bujumbura"),
 | 
			
		||||
                    ("Africa/Cairo", "Africa/Cairo"),
 | 
			
		||||
                    ("Africa/Casablanca", "Africa/Casablanca"),
 | 
			
		||||
                    ("Africa/Ceuta", "Africa/Ceuta"),
 | 
			
		||||
                    ("Africa/Conakry", "Africa/Conakry"),
 | 
			
		||||
                    ("Africa/Dakar", "Africa/Dakar"),
 | 
			
		||||
                    ("Africa/Dar_es_Salaam", "Africa/Dar_es_Salaam"),
 | 
			
		||||
                    ("Africa/Djibouti", "Africa/Djibouti"),
 | 
			
		||||
                    ("Africa/Douala", "Africa/Douala"),
 | 
			
		||||
                    ("Africa/El_Aaiun", "Africa/El_Aaiun"),
 | 
			
		||||
                    ("Africa/Freetown", "Africa/Freetown"),
 | 
			
		||||
                    ("Africa/Gaborone", "Africa/Gaborone"),
 | 
			
		||||
                    ("Africa/Harare", "Africa/Harare"),
 | 
			
		||||
                    ("Africa/Johannesburg", "Africa/Johannesburg"),
 | 
			
		||||
                    ("Africa/Juba", "Africa/Juba"),
 | 
			
		||||
                    ("Africa/Kampala", "Africa/Kampala"),
 | 
			
		||||
                    ("Africa/Khartoum", "Africa/Khartoum"),
 | 
			
		||||
                    ("Africa/Kigali", "Africa/Kigali"),
 | 
			
		||||
                    ("Africa/Kinshasa", "Africa/Kinshasa"),
 | 
			
		||||
                    ("Africa/Lagos", "Africa/Lagos"),
 | 
			
		||||
                    ("Africa/Libreville", "Africa/Libreville"),
 | 
			
		||||
                    ("Africa/Lome", "Africa/Lome"),
 | 
			
		||||
                    ("Africa/Luanda", "Africa/Luanda"),
 | 
			
		||||
                    ("Africa/Lubumbashi", "Africa/Lubumbashi"),
 | 
			
		||||
                    ("Africa/Lusaka", "Africa/Lusaka"),
 | 
			
		||||
                    ("Africa/Malabo", "Africa/Malabo"),
 | 
			
		||||
                    ("Africa/Maputo", "Africa/Maputo"),
 | 
			
		||||
                    ("Africa/Maseru", "Africa/Maseru"),
 | 
			
		||||
                    ("Africa/Mbabane", "Africa/Mbabane"),
 | 
			
		||||
                    ("Africa/Mogadishu", "Africa/Mogadishu"),
 | 
			
		||||
                    ("Africa/Monrovia", "Africa/Monrovia"),
 | 
			
		||||
                    ("Africa/Nairobi", "Africa/Nairobi"),
 | 
			
		||||
                    ("Africa/Ndjamena", "Africa/Ndjamena"),
 | 
			
		||||
                    ("Africa/Niamey", "Africa/Niamey"),
 | 
			
		||||
                    ("Africa/Nouakchott", "Africa/Nouakchott"),
 | 
			
		||||
                    ("Africa/Ouagadougou", "Africa/Ouagadougou"),
 | 
			
		||||
                    ("Africa/Porto-Novo", "Africa/Porto-Novo"),
 | 
			
		||||
                    ("Africa/Sao_Tome", "Africa/Sao_Tome"),
 | 
			
		||||
                    ("Africa/Timbuktu", "Africa/Timbuktu"),
 | 
			
		||||
                    ("Africa/Tripoli", "Africa/Tripoli"),
 | 
			
		||||
                    ("Africa/Tunis", "Africa/Tunis"),
 | 
			
		||||
                    ("Africa/Windhoek", "Africa/Windhoek"),
 | 
			
		||||
                    ("America/Adak", "America/Adak"),
 | 
			
		||||
                    ("America/Anchorage", "America/Anchorage"),
 | 
			
		||||
                    ("America/Anguilla", "America/Anguilla"),
 | 
			
		||||
                    ("America/Antigua", "America/Antigua"),
 | 
			
		||||
                    ("America/Araguaina", "America/Araguaina"),
 | 
			
		||||
                    (
 | 
			
		||||
                        "America/Argentina/Buenos_Aires",
 | 
			
		||||
                        "America/Argentina/Buenos_Aires",
 | 
			
		||||
                    ),
 | 
			
		||||
                    ("America/Argentina/Catamarca", "America/Argentina/Catamarca"),
 | 
			
		||||
                    (
 | 
			
		||||
                        "America/Argentina/ComodRivadavia",
 | 
			
		||||
                        "America/Argentina/ComodRivadavia",
 | 
			
		||||
                    ),
 | 
			
		||||
                    ("America/Argentina/Cordoba", "America/Argentina/Cordoba"),
 | 
			
		||||
                    ("America/Argentina/Jujuy", "America/Argentina/Jujuy"),
 | 
			
		||||
                    ("America/Argentina/La_Rioja", "America/Argentina/La_Rioja"),
 | 
			
		||||
                    ("America/Argentina/Mendoza", "America/Argentina/Mendoza"),
 | 
			
		||||
                    (
 | 
			
		||||
                        "America/Argentina/Rio_Gallegos",
 | 
			
		||||
                        "America/Argentina/Rio_Gallegos",
 | 
			
		||||
                    ),
 | 
			
		||||
                    ("America/Argentina/Salta", "America/Argentina/Salta"),
 | 
			
		||||
                    ("America/Argentina/San_Juan", "America/Argentina/San_Juan"),
 | 
			
		||||
                    ("America/Argentina/San_Luis", "America/Argentina/San_Luis"),
 | 
			
		||||
                    ("America/Argentina/Tucuman", "America/Argentina/Tucuman"),
 | 
			
		||||
                    ("America/Argentina/Ushuaia", "America/Argentina/Ushuaia"),
 | 
			
		||||
                    ("America/Aruba", "America/Aruba"),
 | 
			
		||||
                    ("America/Asuncion", "America/Asuncion"),
 | 
			
		||||
                    ("America/Atikokan", "America/Atikokan"),
 | 
			
		||||
                    ("America/Atka", "America/Atka"),
 | 
			
		||||
                    ("America/Bahia", "America/Bahia"),
 | 
			
		||||
                    ("America/Bahia_Banderas", "America/Bahia_Banderas"),
 | 
			
		||||
                    ("America/Barbados", "America/Barbados"),
 | 
			
		||||
                    ("America/Belem", "America/Belem"),
 | 
			
		||||
                    ("America/Belize", "America/Belize"),
 | 
			
		||||
                    ("America/Blanc-Sablon", "America/Blanc-Sablon"),
 | 
			
		||||
                    ("America/Boa_Vista", "America/Boa_Vista"),
 | 
			
		||||
                    ("America/Bogota", "America/Bogota"),
 | 
			
		||||
                    ("America/Boise", "America/Boise"),
 | 
			
		||||
                    ("America/Buenos_Aires", "America/Buenos_Aires"),
 | 
			
		||||
                    ("America/Cambridge_Bay", "America/Cambridge_Bay"),
 | 
			
		||||
                    ("America/Campo_Grande", "America/Campo_Grande"),
 | 
			
		||||
                    ("America/Cancun", "America/Cancun"),
 | 
			
		||||
                    ("America/Caracas", "America/Caracas"),
 | 
			
		||||
                    ("America/Catamarca", "America/Catamarca"),
 | 
			
		||||
                    ("America/Cayenne", "America/Cayenne"),
 | 
			
		||||
                    ("America/Cayman", "America/Cayman"),
 | 
			
		||||
                    ("America/Chicago", "America/Chicago"),
 | 
			
		||||
                    ("America/Chihuahua", "America/Chihuahua"),
 | 
			
		||||
                    ("America/Ciudad_Juarez", "America/Ciudad_Juarez"),
 | 
			
		||||
                    ("America/Coral_Harbour", "America/Coral_Harbour"),
 | 
			
		||||
                    ("America/Cordoba", "America/Cordoba"),
 | 
			
		||||
                    ("America/Costa_Rica", "America/Costa_Rica"),
 | 
			
		||||
                    ("America/Creston", "America/Creston"),
 | 
			
		||||
                    ("America/Cuiaba", "America/Cuiaba"),
 | 
			
		||||
                    ("America/Curacao", "America/Curacao"),
 | 
			
		||||
                    ("America/Danmarkshavn", "America/Danmarkshavn"),
 | 
			
		||||
                    ("America/Dawson", "America/Dawson"),
 | 
			
		||||
                    ("America/Dawson_Creek", "America/Dawson_Creek"),
 | 
			
		||||
                    ("America/Denver", "America/Denver"),
 | 
			
		||||
                    ("America/Detroit", "America/Detroit"),
 | 
			
		||||
                    ("America/Dominica", "America/Dominica"),
 | 
			
		||||
                    ("America/Edmonton", "America/Edmonton"),
 | 
			
		||||
                    ("America/Eirunepe", "America/Eirunepe"),
 | 
			
		||||
                    ("America/El_Salvador", "America/El_Salvador"),
 | 
			
		||||
                    ("America/Ensenada", "America/Ensenada"),
 | 
			
		||||
                    ("America/Fort_Nelson", "America/Fort_Nelson"),
 | 
			
		||||
                    ("America/Fort_Wayne", "America/Fort_Wayne"),
 | 
			
		||||
                    ("America/Fortaleza", "America/Fortaleza"),
 | 
			
		||||
                    ("America/Glace_Bay", "America/Glace_Bay"),
 | 
			
		||||
                    ("America/Godthab", "America/Godthab"),
 | 
			
		||||
                    ("America/Goose_Bay", "America/Goose_Bay"),
 | 
			
		||||
                    ("America/Grand_Turk", "America/Grand_Turk"),
 | 
			
		||||
                    ("America/Grenada", "America/Grenada"),
 | 
			
		||||
                    ("America/Guadeloupe", "America/Guadeloupe"),
 | 
			
		||||
                    ("America/Guatemala", "America/Guatemala"),
 | 
			
		||||
                    ("America/Guayaquil", "America/Guayaquil"),
 | 
			
		||||
                    ("America/Guyana", "America/Guyana"),
 | 
			
		||||
                    ("America/Halifax", "America/Halifax"),
 | 
			
		||||
                    ("America/Havana", "America/Havana"),
 | 
			
		||||
                    ("America/Hermosillo", "America/Hermosillo"),
 | 
			
		||||
                    ("America/Indiana/Indianapolis", "America/Indiana/Indianapolis"),
 | 
			
		||||
                    ("America/Indiana/Knox", "America/Indiana/Knox"),
 | 
			
		||||
                    ("America/Indiana/Marengo", "America/Indiana/Marengo"),
 | 
			
		||||
                    ("America/Indiana/Petersburg", "America/Indiana/Petersburg"),
 | 
			
		||||
                    ("America/Indiana/Tell_City", "America/Indiana/Tell_City"),
 | 
			
		||||
                    ("America/Indiana/Vevay", "America/Indiana/Vevay"),
 | 
			
		||||
                    ("America/Indiana/Vincennes", "America/Indiana/Vincennes"),
 | 
			
		||||
                    ("America/Indiana/Winamac", "America/Indiana/Winamac"),
 | 
			
		||||
                    ("America/Indianapolis", "America/Indianapolis"),
 | 
			
		||||
                    ("America/Inuvik", "America/Inuvik"),
 | 
			
		||||
                    ("America/Iqaluit", "America/Iqaluit"),
 | 
			
		||||
                    ("America/Jamaica", "America/Jamaica"),
 | 
			
		||||
                    ("America/Jujuy", "America/Jujuy"),
 | 
			
		||||
                    ("America/Juneau", "America/Juneau"),
 | 
			
		||||
                    ("America/Kentucky/Louisville", "America/Kentucky/Louisville"),
 | 
			
		||||
                    ("America/Kentucky/Monticello", "America/Kentucky/Monticello"),
 | 
			
		||||
                    ("America/Knox_IN", "America/Knox_IN"),
 | 
			
		||||
                    ("America/Kralendijk", "America/Kralendijk"),
 | 
			
		||||
                    ("America/La_Paz", "America/La_Paz"),
 | 
			
		||||
                    ("America/Lima", "America/Lima"),
 | 
			
		||||
                    ("America/Los_Angeles", "America/Los_Angeles"),
 | 
			
		||||
                    ("America/Louisville", "America/Louisville"),
 | 
			
		||||
                    ("America/Lower_Princes", "America/Lower_Princes"),
 | 
			
		||||
                    ("America/Maceio", "America/Maceio"),
 | 
			
		||||
                    ("America/Managua", "America/Managua"),
 | 
			
		||||
                    ("America/Manaus", "America/Manaus"),
 | 
			
		||||
                    ("America/Marigot", "America/Marigot"),
 | 
			
		||||
                    ("America/Martinique", "America/Martinique"),
 | 
			
		||||
                    ("America/Matamoros", "America/Matamoros"),
 | 
			
		||||
                    ("America/Mazatlan", "America/Mazatlan"),
 | 
			
		||||
                    ("America/Mendoza", "America/Mendoza"),
 | 
			
		||||
                    ("America/Menominee", "America/Menominee"),
 | 
			
		||||
                    ("America/Merida", "America/Merida"),
 | 
			
		||||
                    ("America/Metlakatla", "America/Metlakatla"),
 | 
			
		||||
                    ("America/Mexico_City", "America/Mexico_City"),
 | 
			
		||||
                    ("America/Miquelon", "America/Miquelon"),
 | 
			
		||||
                    ("America/Moncton", "America/Moncton"),
 | 
			
		||||
                    ("America/Monterrey", "America/Monterrey"),
 | 
			
		||||
                    ("America/Montevideo", "America/Montevideo"),
 | 
			
		||||
                    ("America/Montreal", "America/Montreal"),
 | 
			
		||||
                    ("America/Montserrat", "America/Montserrat"),
 | 
			
		||||
                    ("America/Nassau", "America/Nassau"),
 | 
			
		||||
                    ("America/New_York", "America/New_York"),
 | 
			
		||||
                    ("America/Nipigon", "America/Nipigon"),
 | 
			
		||||
                    ("America/Nome", "America/Nome"),
 | 
			
		||||
                    ("America/Noronha", "America/Noronha"),
 | 
			
		||||
                    ("America/North_Dakota/Beulah", "America/North_Dakota/Beulah"),
 | 
			
		||||
                    ("America/North_Dakota/Center", "America/North_Dakota/Center"),
 | 
			
		||||
                    (
 | 
			
		||||
                        "America/North_Dakota/New_Salem",
 | 
			
		||||
                        "America/North_Dakota/New_Salem",
 | 
			
		||||
                    ),
 | 
			
		||||
                    ("America/Nuuk", "America/Nuuk"),
 | 
			
		||||
                    ("America/Ojinaga", "America/Ojinaga"),
 | 
			
		||||
                    ("America/Panama", "America/Panama"),
 | 
			
		||||
                    ("America/Pangnirtung", "America/Pangnirtung"),
 | 
			
		||||
                    ("America/Paramaribo", "America/Paramaribo"),
 | 
			
		||||
                    ("America/Phoenix", "America/Phoenix"),
 | 
			
		||||
                    ("America/Port-au-Prince", "America/Port-au-Prince"),
 | 
			
		||||
                    ("America/Port_of_Spain", "America/Port_of_Spain"),
 | 
			
		||||
                    ("America/Porto_Acre", "America/Porto_Acre"),
 | 
			
		||||
                    ("America/Porto_Velho", "America/Porto_Velho"),
 | 
			
		||||
                    ("America/Puerto_Rico", "America/Puerto_Rico"),
 | 
			
		||||
                    ("America/Punta_Arenas", "America/Punta_Arenas"),
 | 
			
		||||
                    ("America/Rainy_River", "America/Rainy_River"),
 | 
			
		||||
                    ("America/Rankin_Inlet", "America/Rankin_Inlet"),
 | 
			
		||||
                    ("America/Recife", "America/Recife"),
 | 
			
		||||
                    ("America/Regina", "America/Regina"),
 | 
			
		||||
                    ("America/Resolute", "America/Resolute"),
 | 
			
		||||
                    ("America/Rio_Branco", "America/Rio_Branco"),
 | 
			
		||||
                    ("America/Rosario", "America/Rosario"),
 | 
			
		||||
                    ("America/Santa_Isabel", "America/Santa_Isabel"),
 | 
			
		||||
                    ("America/Santarem", "America/Santarem"),
 | 
			
		||||
                    ("America/Santiago", "America/Santiago"),
 | 
			
		||||
                    ("America/Santo_Domingo", "America/Santo_Domingo"),
 | 
			
		||||
                    ("America/Sao_Paulo", "America/Sao_Paulo"),
 | 
			
		||||
                    ("America/Scoresbysund", "America/Scoresbysund"),
 | 
			
		||||
                    ("America/Shiprock", "America/Shiprock"),
 | 
			
		||||
                    ("America/Sitka", "America/Sitka"),
 | 
			
		||||
                    ("America/St_Barthelemy", "America/St_Barthelemy"),
 | 
			
		||||
                    ("America/St_Johns", "America/St_Johns"),
 | 
			
		||||
                    ("America/St_Kitts", "America/St_Kitts"),
 | 
			
		||||
                    ("America/St_Lucia", "America/St_Lucia"),
 | 
			
		||||
                    ("America/St_Thomas", "America/St_Thomas"),
 | 
			
		||||
                    ("America/St_Vincent", "America/St_Vincent"),
 | 
			
		||||
                    ("America/Swift_Current", "America/Swift_Current"),
 | 
			
		||||
                    ("America/Tegucigalpa", "America/Tegucigalpa"),
 | 
			
		||||
                    ("America/Thule", "America/Thule"),
 | 
			
		||||
                    ("America/Thunder_Bay", "America/Thunder_Bay"),
 | 
			
		||||
                    ("America/Tijuana", "America/Tijuana"),
 | 
			
		||||
                    ("America/Toronto", "America/Toronto"),
 | 
			
		||||
                    ("America/Tortola", "America/Tortola"),
 | 
			
		||||
                    ("America/Vancouver", "America/Vancouver"),
 | 
			
		||||
                    ("America/Virgin", "America/Virgin"),
 | 
			
		||||
                    ("America/Whitehorse", "America/Whitehorse"),
 | 
			
		||||
                    ("America/Winnipeg", "America/Winnipeg"),
 | 
			
		||||
                    ("America/Yakutat", "America/Yakutat"),
 | 
			
		||||
                    ("America/Yellowknife", "America/Yellowknife"),
 | 
			
		||||
                    ("Antarctica/Casey", "Antarctica/Casey"),
 | 
			
		||||
                    ("Antarctica/Davis", "Antarctica/Davis"),
 | 
			
		||||
                    ("Antarctica/DumontDUrville", "Antarctica/DumontDUrville"),
 | 
			
		||||
                    ("Antarctica/Macquarie", "Antarctica/Macquarie"),
 | 
			
		||||
                    ("Antarctica/Mawson", "Antarctica/Mawson"),
 | 
			
		||||
                    ("Antarctica/McMurdo", "Antarctica/McMurdo"),
 | 
			
		||||
                    ("Antarctica/Palmer", "Antarctica/Palmer"),
 | 
			
		||||
                    ("Antarctica/Rothera", "Antarctica/Rothera"),
 | 
			
		||||
                    ("Antarctica/South_Pole", "Antarctica/South_Pole"),
 | 
			
		||||
                    ("Antarctica/Syowa", "Antarctica/Syowa"),
 | 
			
		||||
                    ("Antarctica/Troll", "Antarctica/Troll"),
 | 
			
		||||
                    ("Antarctica/Vostok", "Antarctica/Vostok"),
 | 
			
		||||
                    ("Arctic/Longyearbyen", "Arctic/Longyearbyen"),
 | 
			
		||||
                    ("Asia/Aden", "Asia/Aden"),
 | 
			
		||||
                    ("Asia/Almaty", "Asia/Almaty"),
 | 
			
		||||
                    ("Asia/Amman", "Asia/Amman"),
 | 
			
		||||
                    ("Asia/Anadyr", "Asia/Anadyr"),
 | 
			
		||||
                    ("Asia/Aqtau", "Asia/Aqtau"),
 | 
			
		||||
                    ("Asia/Aqtobe", "Asia/Aqtobe"),
 | 
			
		||||
                    ("Asia/Ashgabat", "Asia/Ashgabat"),
 | 
			
		||||
                    ("Asia/Ashkhabad", "Asia/Ashkhabad"),
 | 
			
		||||
                    ("Asia/Atyrau", "Asia/Atyrau"),
 | 
			
		||||
                    ("Asia/Baghdad", "Asia/Baghdad"),
 | 
			
		||||
                    ("Asia/Bahrain", "Asia/Bahrain"),
 | 
			
		||||
                    ("Asia/Baku", "Asia/Baku"),
 | 
			
		||||
                    ("Asia/Bangkok", "Asia/Bangkok"),
 | 
			
		||||
                    ("Asia/Barnaul", "Asia/Barnaul"),
 | 
			
		||||
                    ("Asia/Beirut", "Asia/Beirut"),
 | 
			
		||||
                    ("Asia/Bishkek", "Asia/Bishkek"),
 | 
			
		||||
                    ("Asia/Brunei", "Asia/Brunei"),
 | 
			
		||||
                    ("Asia/Calcutta", "Asia/Calcutta"),
 | 
			
		||||
                    ("Asia/Chita", "Asia/Chita"),
 | 
			
		||||
                    ("Asia/Choibalsan", "Asia/Choibalsan"),
 | 
			
		||||
                    ("Asia/Chongqing", "Asia/Chongqing"),
 | 
			
		||||
                    ("Asia/Chungking", "Asia/Chungking"),
 | 
			
		||||
                    ("Asia/Colombo", "Asia/Colombo"),
 | 
			
		||||
                    ("Asia/Dacca", "Asia/Dacca"),
 | 
			
		||||
                    ("Asia/Damascus", "Asia/Damascus"),
 | 
			
		||||
                    ("Asia/Dhaka", "Asia/Dhaka"),
 | 
			
		||||
                    ("Asia/Dili", "Asia/Dili"),
 | 
			
		||||
                    ("Asia/Dubai", "Asia/Dubai"),
 | 
			
		||||
                    ("Asia/Dushanbe", "Asia/Dushanbe"),
 | 
			
		||||
                    ("Asia/Famagusta", "Asia/Famagusta"),
 | 
			
		||||
                    ("Asia/Gaza", "Asia/Gaza"),
 | 
			
		||||
                    ("Asia/Harbin", "Asia/Harbin"),
 | 
			
		||||
                    ("Asia/Hebron", "Asia/Hebron"),
 | 
			
		||||
                    ("Asia/Ho_Chi_Minh", "Asia/Ho_Chi_Minh"),
 | 
			
		||||
                    ("Asia/Hong_Kong", "Asia/Hong_Kong"),
 | 
			
		||||
                    ("Asia/Hovd", "Asia/Hovd"),
 | 
			
		||||
                    ("Asia/Irkutsk", "Asia/Irkutsk"),
 | 
			
		||||
                    ("Asia/Istanbul", "Asia/Istanbul"),
 | 
			
		||||
                    ("Asia/Jakarta", "Asia/Jakarta"),
 | 
			
		||||
                    ("Asia/Jayapura", "Asia/Jayapura"),
 | 
			
		||||
                    ("Asia/Jerusalem", "Asia/Jerusalem"),
 | 
			
		||||
                    ("Asia/Kabul", "Asia/Kabul"),
 | 
			
		||||
                    ("Asia/Kamchatka", "Asia/Kamchatka"),
 | 
			
		||||
                    ("Asia/Karachi", "Asia/Karachi"),
 | 
			
		||||
                    ("Asia/Kashgar", "Asia/Kashgar"),
 | 
			
		||||
                    ("Asia/Kathmandu", "Asia/Kathmandu"),
 | 
			
		||||
                    ("Asia/Katmandu", "Asia/Katmandu"),
 | 
			
		||||
                    ("Asia/Khandyga", "Asia/Khandyga"),
 | 
			
		||||
                    ("Asia/Kolkata", "Asia/Kolkata"),
 | 
			
		||||
                    ("Asia/Krasnoyarsk", "Asia/Krasnoyarsk"),
 | 
			
		||||
                    ("Asia/Kuala_Lumpur", "Asia/Kuala_Lumpur"),
 | 
			
		||||
                    ("Asia/Kuching", "Asia/Kuching"),
 | 
			
		||||
                    ("Asia/Kuwait", "Asia/Kuwait"),
 | 
			
		||||
                    ("Asia/Macao", "Asia/Macao"),
 | 
			
		||||
                    ("Asia/Macau", "Asia/Macau"),
 | 
			
		||||
                    ("Asia/Magadan", "Asia/Magadan"),
 | 
			
		||||
                    ("Asia/Makassar", "Asia/Makassar"),
 | 
			
		||||
                    ("Asia/Manila", "Asia/Manila"),
 | 
			
		||||
                    ("Asia/Muscat", "Asia/Muscat"),
 | 
			
		||||
                    ("Asia/Nicosia", "Asia/Nicosia"),
 | 
			
		||||
                    ("Asia/Novokuznetsk", "Asia/Novokuznetsk"),
 | 
			
		||||
                    ("Asia/Novosibirsk", "Asia/Novosibirsk"),
 | 
			
		||||
                    ("Asia/Omsk", "Asia/Omsk"),
 | 
			
		||||
                    ("Asia/Oral", "Asia/Oral"),
 | 
			
		||||
                    ("Asia/Phnom_Penh", "Asia/Phnom_Penh"),
 | 
			
		||||
                    ("Asia/Pontianak", "Asia/Pontianak"),
 | 
			
		||||
                    ("Asia/Pyongyang", "Asia/Pyongyang"),
 | 
			
		||||
                    ("Asia/Qatar", "Asia/Qatar"),
 | 
			
		||||
                    ("Asia/Qostanay", "Asia/Qostanay"),
 | 
			
		||||
                    ("Asia/Qyzylorda", "Asia/Qyzylorda"),
 | 
			
		||||
                    ("Asia/Rangoon", "Asia/Rangoon"),
 | 
			
		||||
                    ("Asia/Riyadh", "Asia/Riyadh"),
 | 
			
		||||
                    ("Asia/Saigon", "Asia/Saigon"),
 | 
			
		||||
                    ("Asia/Sakhalin", "Asia/Sakhalin"),
 | 
			
		||||
                    ("Asia/Samarkand", "Asia/Samarkand"),
 | 
			
		||||
                    ("Asia/Seoul", "Asia/Seoul"),
 | 
			
		||||
                    ("Asia/Shanghai", "Asia/Shanghai"),
 | 
			
		||||
                    ("Asia/Singapore", "Asia/Singapore"),
 | 
			
		||||
                    ("Asia/Srednekolymsk", "Asia/Srednekolymsk"),
 | 
			
		||||
                    ("Asia/Taipei", "Asia/Taipei"),
 | 
			
		||||
                    ("Asia/Tashkent", "Asia/Tashkent"),
 | 
			
		||||
                    ("Asia/Tbilisi", "Asia/Tbilisi"),
 | 
			
		||||
                    ("Asia/Tehran", "Asia/Tehran"),
 | 
			
		||||
                    ("Asia/Tel_Aviv", "Asia/Tel_Aviv"),
 | 
			
		||||
                    ("Asia/Thimbu", "Asia/Thimbu"),
 | 
			
		||||
                    ("Asia/Thimphu", "Asia/Thimphu"),
 | 
			
		||||
                    ("Asia/Tokyo", "Asia/Tokyo"),
 | 
			
		||||
                    ("Asia/Tomsk", "Asia/Tomsk"),
 | 
			
		||||
                    ("Asia/Ujung_Pandang", "Asia/Ujung_Pandang"),
 | 
			
		||||
                    ("Asia/Ulaanbaatar", "Asia/Ulaanbaatar"),
 | 
			
		||||
                    ("Asia/Ulan_Bator", "Asia/Ulan_Bator"),
 | 
			
		||||
                    ("Asia/Urumqi", "Asia/Urumqi"),
 | 
			
		||||
                    ("Asia/Ust-Nera", "Asia/Ust-Nera"),
 | 
			
		||||
                    ("Asia/Vientiane", "Asia/Vientiane"),
 | 
			
		||||
                    ("Asia/Vladivostok", "Asia/Vladivostok"),
 | 
			
		||||
                    ("Asia/Yakutsk", "Asia/Yakutsk"),
 | 
			
		||||
                    ("Asia/Yangon", "Asia/Yangon"),
 | 
			
		||||
                    ("Asia/Yekaterinburg", "Asia/Yekaterinburg"),
 | 
			
		||||
                    ("Asia/Yerevan", "Asia/Yerevan"),
 | 
			
		||||
                    ("Atlantic/Azores", "Atlantic/Azores"),
 | 
			
		||||
                    ("Atlantic/Bermuda", "Atlantic/Bermuda"),
 | 
			
		||||
                    ("Atlantic/Canary", "Atlantic/Canary"),
 | 
			
		||||
                    ("Atlantic/Cape_Verde", "Atlantic/Cape_Verde"),
 | 
			
		||||
                    ("Atlantic/Faeroe", "Atlantic/Faeroe"),
 | 
			
		||||
                    ("Atlantic/Faroe", "Atlantic/Faroe"),
 | 
			
		||||
                    ("Atlantic/Jan_Mayen", "Atlantic/Jan_Mayen"),
 | 
			
		||||
                    ("Atlantic/Madeira", "Atlantic/Madeira"),
 | 
			
		||||
                    ("Atlantic/Reykjavik", "Atlantic/Reykjavik"),
 | 
			
		||||
                    ("Atlantic/South_Georgia", "Atlantic/South_Georgia"),
 | 
			
		||||
                    ("Atlantic/St_Helena", "Atlantic/St_Helena"),
 | 
			
		||||
                    ("Atlantic/Stanley", "Atlantic/Stanley"),
 | 
			
		||||
                    ("Australia/ACT", "Australia/ACT"),
 | 
			
		||||
                    ("Australia/Adelaide", "Australia/Adelaide"),
 | 
			
		||||
                    ("Australia/Brisbane", "Australia/Brisbane"),
 | 
			
		||||
                    ("Australia/Broken_Hill", "Australia/Broken_Hill"),
 | 
			
		||||
                    ("Australia/Canberra", "Australia/Canberra"),
 | 
			
		||||
                    ("Australia/Currie", "Australia/Currie"),
 | 
			
		||||
                    ("Australia/Darwin", "Australia/Darwin"),
 | 
			
		||||
                    ("Australia/Eucla", "Australia/Eucla"),
 | 
			
		||||
                    ("Australia/Hobart", "Australia/Hobart"),
 | 
			
		||||
                    ("Australia/LHI", "Australia/LHI"),
 | 
			
		||||
                    ("Australia/Lindeman", "Australia/Lindeman"),
 | 
			
		||||
                    ("Australia/Lord_Howe", "Australia/Lord_Howe"),
 | 
			
		||||
                    ("Australia/Melbourne", "Australia/Melbourne"),
 | 
			
		||||
                    ("Australia/NSW", "Australia/NSW"),
 | 
			
		||||
                    ("Australia/North", "Australia/North"),
 | 
			
		||||
                    ("Australia/Perth", "Australia/Perth"),
 | 
			
		||||
                    ("Australia/Queensland", "Australia/Queensland"),
 | 
			
		||||
                    ("Australia/South", "Australia/South"),
 | 
			
		||||
                    ("Australia/Sydney", "Australia/Sydney"),
 | 
			
		||||
                    ("Australia/Tasmania", "Australia/Tasmania"),
 | 
			
		||||
                    ("Australia/Victoria", "Australia/Victoria"),
 | 
			
		||||
                    ("Australia/West", "Australia/West"),
 | 
			
		||||
                    ("Australia/Yancowinna", "Australia/Yancowinna"),
 | 
			
		||||
                    ("Brazil/Acre", "Brazil/Acre"),
 | 
			
		||||
                    ("Brazil/DeNoronha", "Brazil/DeNoronha"),
 | 
			
		||||
                    ("Brazil/East", "Brazil/East"),
 | 
			
		||||
                    ("Brazil/West", "Brazil/West"),
 | 
			
		||||
                    ("CET", "CET"),
 | 
			
		||||
                    ("CST6CDT", "CST6CDT"),
 | 
			
		||||
                    ("Canada/Atlantic", "Canada/Atlantic"),
 | 
			
		||||
                    ("Canada/Central", "Canada/Central"),
 | 
			
		||||
                    ("Canada/Eastern", "Canada/Eastern"),
 | 
			
		||||
                    ("Canada/Mountain", "Canada/Mountain"),
 | 
			
		||||
                    ("Canada/Newfoundland", "Canada/Newfoundland"),
 | 
			
		||||
                    ("Canada/Pacific", "Canada/Pacific"),
 | 
			
		||||
                    ("Canada/Saskatchewan", "Canada/Saskatchewan"),
 | 
			
		||||
                    ("Canada/Yukon", "Canada/Yukon"),
 | 
			
		||||
                    ("Chile/Continental", "Chile/Continental"),
 | 
			
		||||
                    ("Chile/EasterIsland", "Chile/EasterIsland"),
 | 
			
		||||
                    ("Cuba", "Cuba"),
 | 
			
		||||
                    ("EET", "EET"),
 | 
			
		||||
                    ("EST", "EST"),
 | 
			
		||||
                    ("EST5EDT", "EST5EDT"),
 | 
			
		||||
                    ("Egypt", "Egypt"),
 | 
			
		||||
                    ("Eire", "Eire"),
 | 
			
		||||
                    ("Etc/GMT", "Etc/GMT"),
 | 
			
		||||
                    ("Etc/GMT+0", "Etc/GMT+0"),
 | 
			
		||||
                    ("Etc/GMT+1", "Etc/GMT+1"),
 | 
			
		||||
                    ("Etc/GMT+10", "Etc/GMT+10"),
 | 
			
		||||
                    ("Etc/GMT+11", "Etc/GMT+11"),
 | 
			
		||||
                    ("Etc/GMT+12", "Etc/GMT+12"),
 | 
			
		||||
                    ("Etc/GMT+2", "Etc/GMT+2"),
 | 
			
		||||
                    ("Etc/GMT+3", "Etc/GMT+3"),
 | 
			
		||||
                    ("Etc/GMT+4", "Etc/GMT+4"),
 | 
			
		||||
                    ("Etc/GMT+5", "Etc/GMT+5"),
 | 
			
		||||
                    ("Etc/GMT+6", "Etc/GMT+6"),
 | 
			
		||||
                    ("Etc/GMT+7", "Etc/GMT+7"),
 | 
			
		||||
                    ("Etc/GMT+8", "Etc/GMT+8"),
 | 
			
		||||
                    ("Etc/GMT+9", "Etc/GMT+9"),
 | 
			
		||||
                    ("Etc/GMT-0", "Etc/GMT-0"),
 | 
			
		||||
                    ("Etc/GMT-1", "Etc/GMT-1"),
 | 
			
		||||
                    ("Etc/GMT-10", "Etc/GMT-10"),
 | 
			
		||||
                    ("Etc/GMT-11", "Etc/GMT-11"),
 | 
			
		||||
                    ("Etc/GMT-12", "Etc/GMT-12"),
 | 
			
		||||
                    ("Etc/GMT-13", "Etc/GMT-13"),
 | 
			
		||||
                    ("Etc/GMT-14", "Etc/GMT-14"),
 | 
			
		||||
                    ("Etc/GMT-2", "Etc/GMT-2"),
 | 
			
		||||
                    ("Etc/GMT-3", "Etc/GMT-3"),
 | 
			
		||||
                    ("Etc/GMT-4", "Etc/GMT-4"),
 | 
			
		||||
                    ("Etc/GMT-5", "Etc/GMT-5"),
 | 
			
		||||
                    ("Etc/GMT-6", "Etc/GMT-6"),
 | 
			
		||||
                    ("Etc/GMT-7", "Etc/GMT-7"),
 | 
			
		||||
                    ("Etc/GMT-8", "Etc/GMT-8"),
 | 
			
		||||
                    ("Etc/GMT-9", "Etc/GMT-9"),
 | 
			
		||||
                    ("Etc/GMT0", "Etc/GMT0"),
 | 
			
		||||
                    ("Etc/Greenwich", "Etc/Greenwich"),
 | 
			
		||||
                    ("Etc/UCT", "Etc/UCT"),
 | 
			
		||||
                    ("Etc/UTC", "Etc/UTC"),
 | 
			
		||||
                    ("Etc/Universal", "Etc/Universal"),
 | 
			
		||||
                    ("Etc/Zulu", "Etc/Zulu"),
 | 
			
		||||
                    ("Europe/Amsterdam", "Europe/Amsterdam"),
 | 
			
		||||
                    ("Europe/Andorra", "Europe/Andorra"),
 | 
			
		||||
                    ("Europe/Astrakhan", "Europe/Astrakhan"),
 | 
			
		||||
                    ("Europe/Athens", "Europe/Athens"),
 | 
			
		||||
                    ("Europe/Belfast", "Europe/Belfast"),
 | 
			
		||||
                    ("Europe/Belgrade", "Europe/Belgrade"),
 | 
			
		||||
                    ("Europe/Berlin", "Europe/Berlin"),
 | 
			
		||||
                    ("Europe/Bratislava", "Europe/Bratislava"),
 | 
			
		||||
                    ("Europe/Brussels", "Europe/Brussels"),
 | 
			
		||||
                    ("Europe/Bucharest", "Europe/Bucharest"),
 | 
			
		||||
                    ("Europe/Budapest", "Europe/Budapest"),
 | 
			
		||||
                    ("Europe/Busingen", "Europe/Busingen"),
 | 
			
		||||
                    ("Europe/Chisinau", "Europe/Chisinau"),
 | 
			
		||||
                    ("Europe/Copenhagen", "Europe/Copenhagen"),
 | 
			
		||||
                    ("Europe/Dublin", "Europe/Dublin"),
 | 
			
		||||
                    ("Europe/Gibraltar", "Europe/Gibraltar"),
 | 
			
		||||
                    ("Europe/Guernsey", "Europe/Guernsey"),
 | 
			
		||||
                    ("Europe/Helsinki", "Europe/Helsinki"),
 | 
			
		||||
                    ("Europe/Isle_of_Man", "Europe/Isle_of_Man"),
 | 
			
		||||
                    ("Europe/Istanbul", "Europe/Istanbul"),
 | 
			
		||||
                    ("Europe/Jersey", "Europe/Jersey"),
 | 
			
		||||
                    ("Europe/Kaliningrad", "Europe/Kaliningrad"),
 | 
			
		||||
                    ("Europe/Kiev", "Europe/Kiev"),
 | 
			
		||||
                    ("Europe/Kirov", "Europe/Kirov"),
 | 
			
		||||
                    ("Europe/Kyiv", "Europe/Kyiv"),
 | 
			
		||||
                    ("Europe/Lisbon", "Europe/Lisbon"),
 | 
			
		||||
                    ("Europe/Ljubljana", "Europe/Ljubljana"),
 | 
			
		||||
                    ("Europe/London", "Europe/London"),
 | 
			
		||||
                    ("Europe/Luxembourg", "Europe/Luxembourg"),
 | 
			
		||||
                    ("Europe/Madrid", "Europe/Madrid"),
 | 
			
		||||
                    ("Europe/Malta", "Europe/Malta"),
 | 
			
		||||
                    ("Europe/Mariehamn", "Europe/Mariehamn"),
 | 
			
		||||
                    ("Europe/Minsk", "Europe/Minsk"),
 | 
			
		||||
                    ("Europe/Monaco", "Europe/Monaco"),
 | 
			
		||||
                    ("Europe/Moscow", "Europe/Moscow"),
 | 
			
		||||
                    ("Europe/Nicosia", "Europe/Nicosia"),
 | 
			
		||||
                    ("Europe/Oslo", "Europe/Oslo"),
 | 
			
		||||
                    ("Europe/Paris", "Europe/Paris"),
 | 
			
		||||
                    ("Europe/Podgorica", "Europe/Podgorica"),
 | 
			
		||||
                    ("Europe/Prague", "Europe/Prague"),
 | 
			
		||||
                    ("Europe/Riga", "Europe/Riga"),
 | 
			
		||||
                    ("Europe/Rome", "Europe/Rome"),
 | 
			
		||||
                    ("Europe/Samara", "Europe/Samara"),
 | 
			
		||||
                    ("Europe/San_Marino", "Europe/San_Marino"),
 | 
			
		||||
                    ("Europe/Sarajevo", "Europe/Sarajevo"),
 | 
			
		||||
                    ("Europe/Saratov", "Europe/Saratov"),
 | 
			
		||||
                    ("Europe/Simferopol", "Europe/Simferopol"),
 | 
			
		||||
                    ("Europe/Skopje", "Europe/Skopje"),
 | 
			
		||||
                    ("Europe/Sofia", "Europe/Sofia"),
 | 
			
		||||
                    ("Europe/Stockholm", "Europe/Stockholm"),
 | 
			
		||||
                    ("Europe/Tallinn", "Europe/Tallinn"),
 | 
			
		||||
                    ("Europe/Tirane", "Europe/Tirane"),
 | 
			
		||||
                    ("Europe/Tiraspol", "Europe/Tiraspol"),
 | 
			
		||||
                    ("Europe/Ulyanovsk", "Europe/Ulyanovsk"),
 | 
			
		||||
                    ("Europe/Uzhgorod", "Europe/Uzhgorod"),
 | 
			
		||||
                    ("Europe/Vaduz", "Europe/Vaduz"),
 | 
			
		||||
                    ("Europe/Vatican", "Europe/Vatican"),
 | 
			
		||||
                    ("Europe/Vienna", "Europe/Vienna"),
 | 
			
		||||
                    ("Europe/Vilnius", "Europe/Vilnius"),
 | 
			
		||||
                    ("Europe/Volgograd", "Europe/Volgograd"),
 | 
			
		||||
                    ("Europe/Warsaw", "Europe/Warsaw"),
 | 
			
		||||
                    ("Europe/Zagreb", "Europe/Zagreb"),
 | 
			
		||||
                    ("Europe/Zaporozhye", "Europe/Zaporozhye"),
 | 
			
		||||
                    ("Europe/Zurich", "Europe/Zurich"),
 | 
			
		||||
                    ("GB", "GB"),
 | 
			
		||||
                    ("GB-Eire", "GB-Eire"),
 | 
			
		||||
                    ("GMT", "GMT"),
 | 
			
		||||
                    ("GMT+0", "GMT+0"),
 | 
			
		||||
                    ("GMT-0", "GMT-0"),
 | 
			
		||||
                    ("GMT0", "GMT0"),
 | 
			
		||||
                    ("Greenwich", "Greenwich"),
 | 
			
		||||
                    ("HST", "HST"),
 | 
			
		||||
                    ("Hongkong", "Hongkong"),
 | 
			
		||||
                    ("Iceland", "Iceland"),
 | 
			
		||||
                    ("Indian/Antananarivo", "Indian/Antananarivo"),
 | 
			
		||||
                    ("Indian/Chagos", "Indian/Chagos"),
 | 
			
		||||
                    ("Indian/Christmas", "Indian/Christmas"),
 | 
			
		||||
                    ("Indian/Cocos", "Indian/Cocos"),
 | 
			
		||||
                    ("Indian/Comoro", "Indian/Comoro"),
 | 
			
		||||
                    ("Indian/Kerguelen", "Indian/Kerguelen"),
 | 
			
		||||
                    ("Indian/Mahe", "Indian/Mahe"),
 | 
			
		||||
                    ("Indian/Maldives", "Indian/Maldives"),
 | 
			
		||||
                    ("Indian/Mauritius", "Indian/Mauritius"),
 | 
			
		||||
                    ("Indian/Mayotte", "Indian/Mayotte"),
 | 
			
		||||
                    ("Indian/Reunion", "Indian/Reunion"),
 | 
			
		||||
                    ("Iran", "Iran"),
 | 
			
		||||
                    ("Israel", "Israel"),
 | 
			
		||||
                    ("Jamaica", "Jamaica"),
 | 
			
		||||
                    ("Japan", "Japan"),
 | 
			
		||||
                    ("Kwajalein", "Kwajalein"),
 | 
			
		||||
                    ("Libya", "Libya"),
 | 
			
		||||
                    ("MET", "MET"),
 | 
			
		||||
                    ("MST", "MST"),
 | 
			
		||||
                    ("MST7MDT", "MST7MDT"),
 | 
			
		||||
                    ("Mexico/BajaNorte", "Mexico/BajaNorte"),
 | 
			
		||||
                    ("Mexico/BajaSur", "Mexico/BajaSur"),
 | 
			
		||||
                    ("Mexico/General", "Mexico/General"),
 | 
			
		||||
                    ("NZ", "NZ"),
 | 
			
		||||
                    ("NZ-CHAT", "NZ-CHAT"),
 | 
			
		||||
                    ("Navajo", "Navajo"),
 | 
			
		||||
                    ("PRC", "PRC"),
 | 
			
		||||
                    ("PST8PDT", "PST8PDT"),
 | 
			
		||||
                    ("Pacific/Apia", "Pacific/Apia"),
 | 
			
		||||
                    ("Pacific/Auckland", "Pacific/Auckland"),
 | 
			
		||||
                    ("Pacific/Bougainville", "Pacific/Bougainville"),
 | 
			
		||||
                    ("Pacific/Chatham", "Pacific/Chatham"),
 | 
			
		||||
                    ("Pacific/Chuuk", "Pacific/Chuuk"),
 | 
			
		||||
                    ("Pacific/Easter", "Pacific/Easter"),
 | 
			
		||||
                    ("Pacific/Efate", "Pacific/Efate"),
 | 
			
		||||
                    ("Pacific/Enderbury", "Pacific/Enderbury"),
 | 
			
		||||
                    ("Pacific/Fakaofo", "Pacific/Fakaofo"),
 | 
			
		||||
                    ("Pacific/Fiji", "Pacific/Fiji"),
 | 
			
		||||
                    ("Pacific/Funafuti", "Pacific/Funafuti"),
 | 
			
		||||
                    ("Pacific/Galapagos", "Pacific/Galapagos"),
 | 
			
		||||
                    ("Pacific/Gambier", "Pacific/Gambier"),
 | 
			
		||||
                    ("Pacific/Guadalcanal", "Pacific/Guadalcanal"),
 | 
			
		||||
                    ("Pacific/Guam", "Pacific/Guam"),
 | 
			
		||||
                    ("Pacific/Honolulu", "Pacific/Honolulu"),
 | 
			
		||||
                    ("Pacific/Johnston", "Pacific/Johnston"),
 | 
			
		||||
                    ("Pacific/Kanton", "Pacific/Kanton"),
 | 
			
		||||
                    ("Pacific/Kiritimati", "Pacific/Kiritimati"),
 | 
			
		||||
                    ("Pacific/Kosrae", "Pacific/Kosrae"),
 | 
			
		||||
                    ("Pacific/Kwajalein", "Pacific/Kwajalein"),
 | 
			
		||||
                    ("Pacific/Majuro", "Pacific/Majuro"),
 | 
			
		||||
                    ("Pacific/Marquesas", "Pacific/Marquesas"),
 | 
			
		||||
                    ("Pacific/Midway", "Pacific/Midway"),
 | 
			
		||||
                    ("Pacific/Nauru", "Pacific/Nauru"),
 | 
			
		||||
                    ("Pacific/Niue", "Pacific/Niue"),
 | 
			
		||||
                    ("Pacific/Norfolk", "Pacific/Norfolk"),
 | 
			
		||||
                    ("Pacific/Noumea", "Pacific/Noumea"),
 | 
			
		||||
                    ("Pacific/Pago_Pago", "Pacific/Pago_Pago"),
 | 
			
		||||
                    ("Pacific/Palau", "Pacific/Palau"),
 | 
			
		||||
                    ("Pacific/Pitcairn", "Pacific/Pitcairn"),
 | 
			
		||||
                    ("Pacific/Pohnpei", "Pacific/Pohnpei"),
 | 
			
		||||
                    ("Pacific/Ponape", "Pacific/Ponape"),
 | 
			
		||||
                    ("Pacific/Port_Moresby", "Pacific/Port_Moresby"),
 | 
			
		||||
                    ("Pacific/Rarotonga", "Pacific/Rarotonga"),
 | 
			
		||||
                    ("Pacific/Saipan", "Pacific/Saipan"),
 | 
			
		||||
                    ("Pacific/Samoa", "Pacific/Samoa"),
 | 
			
		||||
                    ("Pacific/Tahiti", "Pacific/Tahiti"),
 | 
			
		||||
                    ("Pacific/Tarawa", "Pacific/Tarawa"),
 | 
			
		||||
                    ("Pacific/Tongatapu", "Pacific/Tongatapu"),
 | 
			
		||||
                    ("Pacific/Truk", "Pacific/Truk"),
 | 
			
		||||
                    ("Pacific/Wake", "Pacific/Wake"),
 | 
			
		||||
                    ("Pacific/Wallis", "Pacific/Wallis"),
 | 
			
		||||
                    ("Pacific/Yap", "Pacific/Yap"),
 | 
			
		||||
                    ("Poland", "Poland"),
 | 
			
		||||
                    ("Portugal", "Portugal"),
 | 
			
		||||
                    ("ROC", "ROC"),
 | 
			
		||||
                    ("ROK", "ROK"),
 | 
			
		||||
                    ("Singapore", "Singapore"),
 | 
			
		||||
                    ("Turkey", "Turkey"),
 | 
			
		||||
                    ("UCT", "UCT"),
 | 
			
		||||
                    ("US/Alaska", "US/Alaska"),
 | 
			
		||||
                    ("US/Aleutian", "US/Aleutian"),
 | 
			
		||||
                    ("US/Arizona", "US/Arizona"),
 | 
			
		||||
                    ("US/Central", "US/Central"),
 | 
			
		||||
                    ("US/East-Indiana", "US/East-Indiana"),
 | 
			
		||||
                    ("US/Eastern", "US/Eastern"),
 | 
			
		||||
                    ("US/Hawaii", "US/Hawaii"),
 | 
			
		||||
                    ("US/Indiana-Starke", "US/Indiana-Starke"),
 | 
			
		||||
                    ("US/Michigan", "US/Michigan"),
 | 
			
		||||
                    ("US/Mountain", "US/Mountain"),
 | 
			
		||||
                    ("US/Pacific", "US/Pacific"),
 | 
			
		||||
                    ("US/Samoa", "US/Samoa"),
 | 
			
		||||
                    ("UTC", "UTC"),
 | 
			
		||||
                    ("Universal", "Universal"),
 | 
			
		||||
                    ("W-SU", "W-SU"),
 | 
			
		||||
                    ("WET", "WET"),
 | 
			
		||||
                    ("Zulu", "Zulu"),
 | 
			
		||||
                ],
 | 
			
		||||
                max_length=255,
 | 
			
		||||
                null=True,
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,17 @@
 | 
			
		||||
# Generated by Django 4.2.3 on 2023-07-18 01:15
 | 
			
		||||
 | 
			
		||||
from django.db import migrations
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("core", "0037_coresettings_open_ai_model_and_more"),
 | 
			
		||||
        ("agents", "0056_alter_agent_time_zone"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterUniqueTogether(
 | 
			
		||||
            name="agentcustomfield",
 | 
			
		||||
            unique_together={("agent", "field")},
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										633
									
								
								api/tacticalrmm/agents/migrations/0058_alter_agent_time_zone.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										633
									
								
								api/tacticalrmm/agents/migrations/0058_alter_agent_time_zone.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,633 @@
 | 
			
		||||
# Generated by Django 4.2.7 on 2023-11-09 19:56
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("agents", "0057_alter_agentcustomfield_unique_together"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="agent",
 | 
			
		||||
            name="time_zone",
 | 
			
		||||
            field=models.CharField(
 | 
			
		||||
                blank=True,
 | 
			
		||||
                choices=[
 | 
			
		||||
                    ("Africa/Abidjan", "Africa/Abidjan"),
 | 
			
		||||
                    ("Africa/Accra", "Africa/Accra"),
 | 
			
		||||
                    ("Africa/Addis_Ababa", "Africa/Addis_Ababa"),
 | 
			
		||||
                    ("Africa/Algiers", "Africa/Algiers"),
 | 
			
		||||
                    ("Africa/Asmara", "Africa/Asmara"),
 | 
			
		||||
                    ("Africa/Asmera", "Africa/Asmera"),
 | 
			
		||||
                    ("Africa/Bamako", "Africa/Bamako"),
 | 
			
		||||
                    ("Africa/Bangui", "Africa/Bangui"),
 | 
			
		||||
                    ("Africa/Banjul", "Africa/Banjul"),
 | 
			
		||||
                    ("Africa/Bissau", "Africa/Bissau"),
 | 
			
		||||
                    ("Africa/Blantyre", "Africa/Blantyre"),
 | 
			
		||||
                    ("Africa/Brazzaville", "Africa/Brazzaville"),
 | 
			
		||||
                    ("Africa/Bujumbura", "Africa/Bujumbura"),
 | 
			
		||||
                    ("Africa/Cairo", "Africa/Cairo"),
 | 
			
		||||
                    ("Africa/Casablanca", "Africa/Casablanca"),
 | 
			
		||||
                    ("Africa/Ceuta", "Africa/Ceuta"),
 | 
			
		||||
                    ("Africa/Conakry", "Africa/Conakry"),
 | 
			
		||||
                    ("Africa/Dakar", "Africa/Dakar"),
 | 
			
		||||
                    ("Africa/Dar_es_Salaam", "Africa/Dar_es_Salaam"),
 | 
			
		||||
                    ("Africa/Djibouti", "Africa/Djibouti"),
 | 
			
		||||
                    ("Africa/Douala", "Africa/Douala"),
 | 
			
		||||
                    ("Africa/El_Aaiun", "Africa/El_Aaiun"),
 | 
			
		||||
                    ("Africa/Freetown", "Africa/Freetown"),
 | 
			
		||||
                    ("Africa/Gaborone", "Africa/Gaborone"),
 | 
			
		||||
                    ("Africa/Harare", "Africa/Harare"),
 | 
			
		||||
                    ("Africa/Johannesburg", "Africa/Johannesburg"),
 | 
			
		||||
                    ("Africa/Juba", "Africa/Juba"),
 | 
			
		||||
                    ("Africa/Kampala", "Africa/Kampala"),
 | 
			
		||||
                    ("Africa/Khartoum", "Africa/Khartoum"),
 | 
			
		||||
                    ("Africa/Kigali", "Africa/Kigali"),
 | 
			
		||||
                    ("Africa/Kinshasa", "Africa/Kinshasa"),
 | 
			
		||||
                    ("Africa/Lagos", "Africa/Lagos"),
 | 
			
		||||
                    ("Africa/Libreville", "Africa/Libreville"),
 | 
			
		||||
                    ("Africa/Lome", "Africa/Lome"),
 | 
			
		||||
                    ("Africa/Luanda", "Africa/Luanda"),
 | 
			
		||||
                    ("Africa/Lubumbashi", "Africa/Lubumbashi"),
 | 
			
		||||
                    ("Africa/Lusaka", "Africa/Lusaka"),
 | 
			
		||||
                    ("Africa/Malabo", "Africa/Malabo"),
 | 
			
		||||
                    ("Africa/Maputo", "Africa/Maputo"),
 | 
			
		||||
                    ("Africa/Maseru", "Africa/Maseru"),
 | 
			
		||||
                    ("Africa/Mbabane", "Africa/Mbabane"),
 | 
			
		||||
                    ("Africa/Mogadishu", "Africa/Mogadishu"),
 | 
			
		||||
                    ("Africa/Monrovia", "Africa/Monrovia"),
 | 
			
		||||
                    ("Africa/Nairobi", "Africa/Nairobi"),
 | 
			
		||||
                    ("Africa/Ndjamena", "Africa/Ndjamena"),
 | 
			
		||||
                    ("Africa/Niamey", "Africa/Niamey"),
 | 
			
		||||
                    ("Africa/Nouakchott", "Africa/Nouakchott"),
 | 
			
		||||
                    ("Africa/Ouagadougou", "Africa/Ouagadougou"),
 | 
			
		||||
                    ("Africa/Porto-Novo", "Africa/Porto-Novo"),
 | 
			
		||||
                    ("Africa/Sao_Tome", "Africa/Sao_Tome"),
 | 
			
		||||
                    ("Africa/Timbuktu", "Africa/Timbuktu"),
 | 
			
		||||
                    ("Africa/Tripoli", "Africa/Tripoli"),
 | 
			
		||||
                    ("Africa/Tunis", "Africa/Tunis"),
 | 
			
		||||
                    ("Africa/Windhoek", "Africa/Windhoek"),
 | 
			
		||||
                    ("America/Adak", "America/Adak"),
 | 
			
		||||
                    ("America/Anchorage", "America/Anchorage"),
 | 
			
		||||
                    ("America/Anguilla", "America/Anguilla"),
 | 
			
		||||
                    ("America/Antigua", "America/Antigua"),
 | 
			
		||||
                    ("America/Araguaina", "America/Araguaina"),
 | 
			
		||||
                    (
 | 
			
		||||
                        "America/Argentina/Buenos_Aires",
 | 
			
		||||
                        "America/Argentina/Buenos_Aires",
 | 
			
		||||
                    ),
 | 
			
		||||
                    ("America/Argentina/Catamarca", "America/Argentina/Catamarca"),
 | 
			
		||||
                    (
 | 
			
		||||
                        "America/Argentina/ComodRivadavia",
 | 
			
		||||
                        "America/Argentina/ComodRivadavia",
 | 
			
		||||
                    ),
 | 
			
		||||
                    ("America/Argentina/Cordoba", "America/Argentina/Cordoba"),
 | 
			
		||||
                    ("America/Argentina/Jujuy", "America/Argentina/Jujuy"),
 | 
			
		||||
                    ("America/Argentina/La_Rioja", "America/Argentina/La_Rioja"),
 | 
			
		||||
                    ("America/Argentina/Mendoza", "America/Argentina/Mendoza"),
 | 
			
		||||
                    (
 | 
			
		||||
                        "America/Argentina/Rio_Gallegos",
 | 
			
		||||
                        "America/Argentina/Rio_Gallegos",
 | 
			
		||||
                    ),
 | 
			
		||||
                    ("America/Argentina/Salta", "America/Argentina/Salta"),
 | 
			
		||||
                    ("America/Argentina/San_Juan", "America/Argentina/San_Juan"),
 | 
			
		||||
                    ("America/Argentina/San_Luis", "America/Argentina/San_Luis"),
 | 
			
		||||
                    ("America/Argentina/Tucuman", "America/Argentina/Tucuman"),
 | 
			
		||||
                    ("America/Argentina/Ushuaia", "America/Argentina/Ushuaia"),
 | 
			
		||||
                    ("America/Aruba", "America/Aruba"),
 | 
			
		||||
                    ("America/Asuncion", "America/Asuncion"),
 | 
			
		||||
                    ("America/Atikokan", "America/Atikokan"),
 | 
			
		||||
                    ("America/Atka", "America/Atka"),
 | 
			
		||||
                    ("America/Bahia", "America/Bahia"),
 | 
			
		||||
                    ("America/Bahia_Banderas", "America/Bahia_Banderas"),
 | 
			
		||||
                    ("America/Barbados", "America/Barbados"),
 | 
			
		||||
                    ("America/Belem", "America/Belem"),
 | 
			
		||||
                    ("America/Belize", "America/Belize"),
 | 
			
		||||
                    ("America/Blanc-Sablon", "America/Blanc-Sablon"),
 | 
			
		||||
                    ("America/Boa_Vista", "America/Boa_Vista"),
 | 
			
		||||
                    ("America/Bogota", "America/Bogota"),
 | 
			
		||||
                    ("America/Boise", "America/Boise"),
 | 
			
		||||
                    ("America/Buenos_Aires", "America/Buenos_Aires"),
 | 
			
		||||
                    ("America/Cambridge_Bay", "America/Cambridge_Bay"),
 | 
			
		||||
                    ("America/Campo_Grande", "America/Campo_Grande"),
 | 
			
		||||
                    ("America/Cancun", "America/Cancun"),
 | 
			
		||||
                    ("America/Caracas", "America/Caracas"),
 | 
			
		||||
                    ("America/Catamarca", "America/Catamarca"),
 | 
			
		||||
                    ("America/Cayenne", "America/Cayenne"),
 | 
			
		||||
                    ("America/Cayman", "America/Cayman"),
 | 
			
		||||
                    ("America/Chicago", "America/Chicago"),
 | 
			
		||||
                    ("America/Chihuahua", "America/Chihuahua"),
 | 
			
		||||
                    ("America/Ciudad_Juarez", "America/Ciudad_Juarez"),
 | 
			
		||||
                    ("America/Coral_Harbour", "America/Coral_Harbour"),
 | 
			
		||||
                    ("America/Cordoba", "America/Cordoba"),
 | 
			
		||||
                    ("America/Costa_Rica", "America/Costa_Rica"),
 | 
			
		||||
                    ("America/Creston", "America/Creston"),
 | 
			
		||||
                    ("America/Cuiaba", "America/Cuiaba"),
 | 
			
		||||
                    ("America/Curacao", "America/Curacao"),
 | 
			
		||||
                    ("America/Danmarkshavn", "America/Danmarkshavn"),
 | 
			
		||||
                    ("America/Dawson", "America/Dawson"),
 | 
			
		||||
                    ("America/Dawson_Creek", "America/Dawson_Creek"),
 | 
			
		||||
                    ("America/Denver", "America/Denver"),
 | 
			
		||||
                    ("America/Detroit", "America/Detroit"),
 | 
			
		||||
                    ("America/Dominica", "America/Dominica"),
 | 
			
		||||
                    ("America/Edmonton", "America/Edmonton"),
 | 
			
		||||
                    ("America/Eirunepe", "America/Eirunepe"),
 | 
			
		||||
                    ("America/El_Salvador", "America/El_Salvador"),
 | 
			
		||||
                    ("America/Ensenada", "America/Ensenada"),
 | 
			
		||||
                    ("America/Fort_Nelson", "America/Fort_Nelson"),
 | 
			
		||||
                    ("America/Fort_Wayne", "America/Fort_Wayne"),
 | 
			
		||||
                    ("America/Fortaleza", "America/Fortaleza"),
 | 
			
		||||
                    ("America/Glace_Bay", "America/Glace_Bay"),
 | 
			
		||||
                    ("America/Godthab", "America/Godthab"),
 | 
			
		||||
                    ("America/Goose_Bay", "America/Goose_Bay"),
 | 
			
		||||
                    ("America/Grand_Turk", "America/Grand_Turk"),
 | 
			
		||||
                    ("America/Grenada", "America/Grenada"),
 | 
			
		||||
                    ("America/Guadeloupe", "America/Guadeloupe"),
 | 
			
		||||
                    ("America/Guatemala", "America/Guatemala"),
 | 
			
		||||
                    ("America/Guayaquil", "America/Guayaquil"),
 | 
			
		||||
                    ("America/Guyana", "America/Guyana"),
 | 
			
		||||
                    ("America/Halifax", "America/Halifax"),
 | 
			
		||||
                    ("America/Havana", "America/Havana"),
 | 
			
		||||
                    ("America/Hermosillo", "America/Hermosillo"),
 | 
			
		||||
                    ("America/Indiana/Indianapolis", "America/Indiana/Indianapolis"),
 | 
			
		||||
                    ("America/Indiana/Knox", "America/Indiana/Knox"),
 | 
			
		||||
                    ("America/Indiana/Marengo", "America/Indiana/Marengo"),
 | 
			
		||||
                    ("America/Indiana/Petersburg", "America/Indiana/Petersburg"),
 | 
			
		||||
                    ("America/Indiana/Tell_City", "America/Indiana/Tell_City"),
 | 
			
		||||
                    ("America/Indiana/Vevay", "America/Indiana/Vevay"),
 | 
			
		||||
                    ("America/Indiana/Vincennes", "America/Indiana/Vincennes"),
 | 
			
		||||
                    ("America/Indiana/Winamac", "America/Indiana/Winamac"),
 | 
			
		||||
                    ("America/Indianapolis", "America/Indianapolis"),
 | 
			
		||||
                    ("America/Inuvik", "America/Inuvik"),
 | 
			
		||||
                    ("America/Iqaluit", "America/Iqaluit"),
 | 
			
		||||
                    ("America/Jamaica", "America/Jamaica"),
 | 
			
		||||
                    ("America/Jujuy", "America/Jujuy"),
 | 
			
		||||
                    ("America/Juneau", "America/Juneau"),
 | 
			
		||||
                    ("America/Kentucky/Louisville", "America/Kentucky/Louisville"),
 | 
			
		||||
                    ("America/Kentucky/Monticello", "America/Kentucky/Monticello"),
 | 
			
		||||
                    ("America/Knox_IN", "America/Knox_IN"),
 | 
			
		||||
                    ("America/Kralendijk", "America/Kralendijk"),
 | 
			
		||||
                    ("America/La_Paz", "America/La_Paz"),
 | 
			
		||||
                    ("America/Lima", "America/Lima"),
 | 
			
		||||
                    ("America/Los_Angeles", "America/Los_Angeles"),
 | 
			
		||||
                    ("America/Louisville", "America/Louisville"),
 | 
			
		||||
                    ("America/Lower_Princes", "America/Lower_Princes"),
 | 
			
		||||
                    ("America/Maceio", "America/Maceio"),
 | 
			
		||||
                    ("America/Managua", "America/Managua"),
 | 
			
		||||
                    ("America/Manaus", "America/Manaus"),
 | 
			
		||||
                    ("America/Marigot", "America/Marigot"),
 | 
			
		||||
                    ("America/Martinique", "America/Martinique"),
 | 
			
		||||
                    ("America/Matamoros", "America/Matamoros"),
 | 
			
		||||
                    ("America/Mazatlan", "America/Mazatlan"),
 | 
			
		||||
                    ("America/Mendoza", "America/Mendoza"),
 | 
			
		||||
                    ("America/Menominee", "America/Menominee"),
 | 
			
		||||
                    ("America/Merida", "America/Merida"),
 | 
			
		||||
                    ("America/Metlakatla", "America/Metlakatla"),
 | 
			
		||||
                    ("America/Mexico_City", "America/Mexico_City"),
 | 
			
		||||
                    ("America/Miquelon", "America/Miquelon"),
 | 
			
		||||
                    ("America/Moncton", "America/Moncton"),
 | 
			
		||||
                    ("America/Monterrey", "America/Monterrey"),
 | 
			
		||||
                    ("America/Montevideo", "America/Montevideo"),
 | 
			
		||||
                    ("America/Montreal", "America/Montreal"),
 | 
			
		||||
                    ("America/Montserrat", "America/Montserrat"),
 | 
			
		||||
                    ("America/Nassau", "America/Nassau"),
 | 
			
		||||
                    ("America/New_York", "America/New_York"),
 | 
			
		||||
                    ("America/Nipigon", "America/Nipigon"),
 | 
			
		||||
                    ("America/Nome", "America/Nome"),
 | 
			
		||||
                    ("America/Noronha", "America/Noronha"),
 | 
			
		||||
                    ("America/North_Dakota/Beulah", "America/North_Dakota/Beulah"),
 | 
			
		||||
                    ("America/North_Dakota/Center", "America/North_Dakota/Center"),
 | 
			
		||||
                    (
 | 
			
		||||
                        "America/North_Dakota/New_Salem",
 | 
			
		||||
                        "America/North_Dakota/New_Salem",
 | 
			
		||||
                    ),
 | 
			
		||||
                    ("America/Nuuk", "America/Nuuk"),
 | 
			
		||||
                    ("America/Ojinaga", "America/Ojinaga"),
 | 
			
		||||
                    ("America/Panama", "America/Panama"),
 | 
			
		||||
                    ("America/Pangnirtung", "America/Pangnirtung"),
 | 
			
		||||
                    ("America/Paramaribo", "America/Paramaribo"),
 | 
			
		||||
                    ("America/Phoenix", "America/Phoenix"),
 | 
			
		||||
                    ("America/Port-au-Prince", "America/Port-au-Prince"),
 | 
			
		||||
                    ("America/Port_of_Spain", "America/Port_of_Spain"),
 | 
			
		||||
                    ("America/Porto_Acre", "America/Porto_Acre"),
 | 
			
		||||
                    ("America/Porto_Velho", "America/Porto_Velho"),
 | 
			
		||||
                    ("America/Puerto_Rico", "America/Puerto_Rico"),
 | 
			
		||||
                    ("America/Punta_Arenas", "America/Punta_Arenas"),
 | 
			
		||||
                    ("America/Rainy_River", "America/Rainy_River"),
 | 
			
		||||
                    ("America/Rankin_Inlet", "America/Rankin_Inlet"),
 | 
			
		||||
                    ("America/Recife", "America/Recife"),
 | 
			
		||||
                    ("America/Regina", "America/Regina"),
 | 
			
		||||
                    ("America/Resolute", "America/Resolute"),
 | 
			
		||||
                    ("America/Rio_Branco", "America/Rio_Branco"),
 | 
			
		||||
                    ("America/Rosario", "America/Rosario"),
 | 
			
		||||
                    ("America/Santa_Isabel", "America/Santa_Isabel"),
 | 
			
		||||
                    ("America/Santarem", "America/Santarem"),
 | 
			
		||||
                    ("America/Santiago", "America/Santiago"),
 | 
			
		||||
                    ("America/Santo_Domingo", "America/Santo_Domingo"),
 | 
			
		||||
                    ("America/Sao_Paulo", "America/Sao_Paulo"),
 | 
			
		||||
                    ("America/Scoresbysund", "America/Scoresbysund"),
 | 
			
		||||
                    ("America/Shiprock", "America/Shiprock"),
 | 
			
		||||
                    ("America/Sitka", "America/Sitka"),
 | 
			
		||||
                    ("America/St_Barthelemy", "America/St_Barthelemy"),
 | 
			
		||||
                    ("America/St_Johns", "America/St_Johns"),
 | 
			
		||||
                    ("America/St_Kitts", "America/St_Kitts"),
 | 
			
		||||
                    ("America/St_Lucia", "America/St_Lucia"),
 | 
			
		||||
                    ("America/St_Thomas", "America/St_Thomas"),
 | 
			
		||||
                    ("America/St_Vincent", "America/St_Vincent"),
 | 
			
		||||
                    ("America/Swift_Current", "America/Swift_Current"),
 | 
			
		||||
                    ("America/Tegucigalpa", "America/Tegucigalpa"),
 | 
			
		||||
                    ("America/Thule", "America/Thule"),
 | 
			
		||||
                    ("America/Thunder_Bay", "America/Thunder_Bay"),
 | 
			
		||||
                    ("America/Tijuana", "America/Tijuana"),
 | 
			
		||||
                    ("America/Toronto", "America/Toronto"),
 | 
			
		||||
                    ("America/Tortola", "America/Tortola"),
 | 
			
		||||
                    ("America/Vancouver", "America/Vancouver"),
 | 
			
		||||
                    ("America/Virgin", "America/Virgin"),
 | 
			
		||||
                    ("America/Whitehorse", "America/Whitehorse"),
 | 
			
		||||
                    ("America/Winnipeg", "America/Winnipeg"),
 | 
			
		||||
                    ("America/Yakutat", "America/Yakutat"),
 | 
			
		||||
                    ("America/Yellowknife", "America/Yellowknife"),
 | 
			
		||||
                    ("Antarctica/Casey", "Antarctica/Casey"),
 | 
			
		||||
                    ("Antarctica/Davis", "Antarctica/Davis"),
 | 
			
		||||
                    ("Antarctica/DumontDUrville", "Antarctica/DumontDUrville"),
 | 
			
		||||
                    ("Antarctica/Macquarie", "Antarctica/Macquarie"),
 | 
			
		||||
                    ("Antarctica/Mawson", "Antarctica/Mawson"),
 | 
			
		||||
                    ("Antarctica/McMurdo", "Antarctica/McMurdo"),
 | 
			
		||||
                    ("Antarctica/Palmer", "Antarctica/Palmer"),
 | 
			
		||||
                    ("Antarctica/Rothera", "Antarctica/Rothera"),
 | 
			
		||||
                    ("Antarctica/South_Pole", "Antarctica/South_Pole"),
 | 
			
		||||
                    ("Antarctica/Syowa", "Antarctica/Syowa"),
 | 
			
		||||
                    ("Antarctica/Troll", "Antarctica/Troll"),
 | 
			
		||||
                    ("Antarctica/Vostok", "Antarctica/Vostok"),
 | 
			
		||||
                    ("Arctic/Longyearbyen", "Arctic/Longyearbyen"),
 | 
			
		||||
                    ("Asia/Aden", "Asia/Aden"),
 | 
			
		||||
                    ("Asia/Almaty", "Asia/Almaty"),
 | 
			
		||||
                    ("Asia/Amman", "Asia/Amman"),
 | 
			
		||||
                    ("Asia/Anadyr", "Asia/Anadyr"),
 | 
			
		||||
                    ("Asia/Aqtau", "Asia/Aqtau"),
 | 
			
		||||
                    ("Asia/Aqtobe", "Asia/Aqtobe"),
 | 
			
		||||
                    ("Asia/Ashgabat", "Asia/Ashgabat"),
 | 
			
		||||
                    ("Asia/Ashkhabad", "Asia/Ashkhabad"),
 | 
			
		||||
                    ("Asia/Atyrau", "Asia/Atyrau"),
 | 
			
		||||
                    ("Asia/Baghdad", "Asia/Baghdad"),
 | 
			
		||||
                    ("Asia/Bahrain", "Asia/Bahrain"),
 | 
			
		||||
                    ("Asia/Baku", "Asia/Baku"),
 | 
			
		||||
                    ("Asia/Bangkok", "Asia/Bangkok"),
 | 
			
		||||
                    ("Asia/Barnaul", "Asia/Barnaul"),
 | 
			
		||||
                    ("Asia/Beirut", "Asia/Beirut"),
 | 
			
		||||
                    ("Asia/Bishkek", "Asia/Bishkek"),
 | 
			
		||||
                    ("Asia/Brunei", "Asia/Brunei"),
 | 
			
		||||
                    ("Asia/Calcutta", "Asia/Calcutta"),
 | 
			
		||||
                    ("Asia/Chita", "Asia/Chita"),
 | 
			
		||||
                    ("Asia/Choibalsan", "Asia/Choibalsan"),
 | 
			
		||||
                    ("Asia/Chongqing", "Asia/Chongqing"),
 | 
			
		||||
                    ("Asia/Chungking", "Asia/Chungking"),
 | 
			
		||||
                    ("Asia/Colombo", "Asia/Colombo"),
 | 
			
		||||
                    ("Asia/Dacca", "Asia/Dacca"),
 | 
			
		||||
                    ("Asia/Damascus", "Asia/Damascus"),
 | 
			
		||||
                    ("Asia/Dhaka", "Asia/Dhaka"),
 | 
			
		||||
                    ("Asia/Dili", "Asia/Dili"),
 | 
			
		||||
                    ("Asia/Dubai", "Asia/Dubai"),
 | 
			
		||||
                    ("Asia/Dushanbe", "Asia/Dushanbe"),
 | 
			
		||||
                    ("Asia/Famagusta", "Asia/Famagusta"),
 | 
			
		||||
                    ("Asia/Gaza", "Asia/Gaza"),
 | 
			
		||||
                    ("Asia/Harbin", "Asia/Harbin"),
 | 
			
		||||
                    ("Asia/Hebron", "Asia/Hebron"),
 | 
			
		||||
                    ("Asia/Ho_Chi_Minh", "Asia/Ho_Chi_Minh"),
 | 
			
		||||
                    ("Asia/Hong_Kong", "Asia/Hong_Kong"),
 | 
			
		||||
                    ("Asia/Hovd", "Asia/Hovd"),
 | 
			
		||||
                    ("Asia/Irkutsk", "Asia/Irkutsk"),
 | 
			
		||||
                    ("Asia/Istanbul", "Asia/Istanbul"),
 | 
			
		||||
                    ("Asia/Jakarta", "Asia/Jakarta"),
 | 
			
		||||
                    ("Asia/Jayapura", "Asia/Jayapura"),
 | 
			
		||||
                    ("Asia/Jerusalem", "Asia/Jerusalem"),
 | 
			
		||||
                    ("Asia/Kabul", "Asia/Kabul"),
 | 
			
		||||
                    ("Asia/Kamchatka", "Asia/Kamchatka"),
 | 
			
		||||
                    ("Asia/Karachi", "Asia/Karachi"),
 | 
			
		||||
                    ("Asia/Kashgar", "Asia/Kashgar"),
 | 
			
		||||
                    ("Asia/Kathmandu", "Asia/Kathmandu"),
 | 
			
		||||
                    ("Asia/Katmandu", "Asia/Katmandu"),
 | 
			
		||||
                    ("Asia/Khandyga", "Asia/Khandyga"),
 | 
			
		||||
                    ("Asia/Kolkata", "Asia/Kolkata"),
 | 
			
		||||
                    ("Asia/Krasnoyarsk", "Asia/Krasnoyarsk"),
 | 
			
		||||
                    ("Asia/Kuala_Lumpur", "Asia/Kuala_Lumpur"),
 | 
			
		||||
                    ("Asia/Kuching", "Asia/Kuching"),
 | 
			
		||||
                    ("Asia/Kuwait", "Asia/Kuwait"),
 | 
			
		||||
                    ("Asia/Macao", "Asia/Macao"),
 | 
			
		||||
                    ("Asia/Macau", "Asia/Macau"),
 | 
			
		||||
                    ("Asia/Magadan", "Asia/Magadan"),
 | 
			
		||||
                    ("Asia/Makassar", "Asia/Makassar"),
 | 
			
		||||
                    ("Asia/Manila", "Asia/Manila"),
 | 
			
		||||
                    ("Asia/Muscat", "Asia/Muscat"),
 | 
			
		||||
                    ("Asia/Nicosia", "Asia/Nicosia"),
 | 
			
		||||
                    ("Asia/Novokuznetsk", "Asia/Novokuznetsk"),
 | 
			
		||||
                    ("Asia/Novosibirsk", "Asia/Novosibirsk"),
 | 
			
		||||
                    ("Asia/Omsk", "Asia/Omsk"),
 | 
			
		||||
                    ("Asia/Oral", "Asia/Oral"),
 | 
			
		||||
                    ("Asia/Phnom_Penh", "Asia/Phnom_Penh"),
 | 
			
		||||
                    ("Asia/Pontianak", "Asia/Pontianak"),
 | 
			
		||||
                    ("Asia/Pyongyang", "Asia/Pyongyang"),
 | 
			
		||||
                    ("Asia/Qatar", "Asia/Qatar"),
 | 
			
		||||
                    ("Asia/Qostanay", "Asia/Qostanay"),
 | 
			
		||||
                    ("Asia/Qyzylorda", "Asia/Qyzylorda"),
 | 
			
		||||
                    ("Asia/Rangoon", "Asia/Rangoon"),
 | 
			
		||||
                    ("Asia/Riyadh", "Asia/Riyadh"),
 | 
			
		||||
                    ("Asia/Saigon", "Asia/Saigon"),
 | 
			
		||||
                    ("Asia/Sakhalin", "Asia/Sakhalin"),
 | 
			
		||||
                    ("Asia/Samarkand", "Asia/Samarkand"),
 | 
			
		||||
                    ("Asia/Seoul", "Asia/Seoul"),
 | 
			
		||||
                    ("Asia/Shanghai", "Asia/Shanghai"),
 | 
			
		||||
                    ("Asia/Singapore", "Asia/Singapore"),
 | 
			
		||||
                    ("Asia/Srednekolymsk", "Asia/Srednekolymsk"),
 | 
			
		||||
                    ("Asia/Taipei", "Asia/Taipei"),
 | 
			
		||||
                    ("Asia/Tashkent", "Asia/Tashkent"),
 | 
			
		||||
                    ("Asia/Tbilisi", "Asia/Tbilisi"),
 | 
			
		||||
                    ("Asia/Tehran", "Asia/Tehran"),
 | 
			
		||||
                    ("Asia/Tel_Aviv", "Asia/Tel_Aviv"),
 | 
			
		||||
                    ("Asia/Thimbu", "Asia/Thimbu"),
 | 
			
		||||
                    ("Asia/Thimphu", "Asia/Thimphu"),
 | 
			
		||||
                    ("Asia/Tokyo", "Asia/Tokyo"),
 | 
			
		||||
                    ("Asia/Tomsk", "Asia/Tomsk"),
 | 
			
		||||
                    ("Asia/Ujung_Pandang", "Asia/Ujung_Pandang"),
 | 
			
		||||
                    ("Asia/Ulaanbaatar", "Asia/Ulaanbaatar"),
 | 
			
		||||
                    ("Asia/Ulan_Bator", "Asia/Ulan_Bator"),
 | 
			
		||||
                    ("Asia/Urumqi", "Asia/Urumqi"),
 | 
			
		||||
                    ("Asia/Ust-Nera", "Asia/Ust-Nera"),
 | 
			
		||||
                    ("Asia/Vientiane", "Asia/Vientiane"),
 | 
			
		||||
                    ("Asia/Vladivostok", "Asia/Vladivostok"),
 | 
			
		||||
                    ("Asia/Yakutsk", "Asia/Yakutsk"),
 | 
			
		||||
                    ("Asia/Yangon", "Asia/Yangon"),
 | 
			
		||||
                    ("Asia/Yekaterinburg", "Asia/Yekaterinburg"),
 | 
			
		||||
                    ("Asia/Yerevan", "Asia/Yerevan"),
 | 
			
		||||
                    ("Atlantic/Azores", "Atlantic/Azores"),
 | 
			
		||||
                    ("Atlantic/Bermuda", "Atlantic/Bermuda"),
 | 
			
		||||
                    ("Atlantic/Canary", "Atlantic/Canary"),
 | 
			
		||||
                    ("Atlantic/Cape_Verde", "Atlantic/Cape_Verde"),
 | 
			
		||||
                    ("Atlantic/Faeroe", "Atlantic/Faeroe"),
 | 
			
		||||
                    ("Atlantic/Faroe", "Atlantic/Faroe"),
 | 
			
		||||
                    ("Atlantic/Jan_Mayen", "Atlantic/Jan_Mayen"),
 | 
			
		||||
                    ("Atlantic/Madeira", "Atlantic/Madeira"),
 | 
			
		||||
                    ("Atlantic/Reykjavik", "Atlantic/Reykjavik"),
 | 
			
		||||
                    ("Atlantic/South_Georgia", "Atlantic/South_Georgia"),
 | 
			
		||||
                    ("Atlantic/St_Helena", "Atlantic/St_Helena"),
 | 
			
		||||
                    ("Atlantic/Stanley", "Atlantic/Stanley"),
 | 
			
		||||
                    ("Australia/ACT", "Australia/ACT"),
 | 
			
		||||
                    ("Australia/Adelaide", "Australia/Adelaide"),
 | 
			
		||||
                    ("Australia/Brisbane", "Australia/Brisbane"),
 | 
			
		||||
                    ("Australia/Broken_Hill", "Australia/Broken_Hill"),
 | 
			
		||||
                    ("Australia/Canberra", "Australia/Canberra"),
 | 
			
		||||
                    ("Australia/Currie", "Australia/Currie"),
 | 
			
		||||
                    ("Australia/Darwin", "Australia/Darwin"),
 | 
			
		||||
                    ("Australia/Eucla", "Australia/Eucla"),
 | 
			
		||||
                    ("Australia/Hobart", "Australia/Hobart"),
 | 
			
		||||
                    ("Australia/LHI", "Australia/LHI"),
 | 
			
		||||
                    ("Australia/Lindeman", "Australia/Lindeman"),
 | 
			
		||||
                    ("Australia/Lord_Howe", "Australia/Lord_Howe"),
 | 
			
		||||
                    ("Australia/Melbourne", "Australia/Melbourne"),
 | 
			
		||||
                    ("Australia/NSW", "Australia/NSW"),
 | 
			
		||||
                    ("Australia/North", "Australia/North"),
 | 
			
		||||
                    ("Australia/Perth", "Australia/Perth"),
 | 
			
		||||
                    ("Australia/Queensland", "Australia/Queensland"),
 | 
			
		||||
                    ("Australia/South", "Australia/South"),
 | 
			
		||||
                    ("Australia/Sydney", "Australia/Sydney"),
 | 
			
		||||
                    ("Australia/Tasmania", "Australia/Tasmania"),
 | 
			
		||||
                    ("Australia/Victoria", "Australia/Victoria"),
 | 
			
		||||
                    ("Australia/West", "Australia/West"),
 | 
			
		||||
                    ("Australia/Yancowinna", "Australia/Yancowinna"),
 | 
			
		||||
                    ("Brazil/Acre", "Brazil/Acre"),
 | 
			
		||||
                    ("Brazil/DeNoronha", "Brazil/DeNoronha"),
 | 
			
		||||
                    ("Brazil/East", "Brazil/East"),
 | 
			
		||||
                    ("Brazil/West", "Brazil/West"),
 | 
			
		||||
                    ("CET", "CET"),
 | 
			
		||||
                    ("CST6CDT", "CST6CDT"),
 | 
			
		||||
                    ("Canada/Atlantic", "Canada/Atlantic"),
 | 
			
		||||
                    ("Canada/Central", "Canada/Central"),
 | 
			
		||||
                    ("Canada/Eastern", "Canada/Eastern"),
 | 
			
		||||
                    ("Canada/Mountain", "Canada/Mountain"),
 | 
			
		||||
                    ("Canada/Newfoundland", "Canada/Newfoundland"),
 | 
			
		||||
                    ("Canada/Pacific", "Canada/Pacific"),
 | 
			
		||||
                    ("Canada/Saskatchewan", "Canada/Saskatchewan"),
 | 
			
		||||
                    ("Canada/Yukon", "Canada/Yukon"),
 | 
			
		||||
                    ("Chile/Continental", "Chile/Continental"),
 | 
			
		||||
                    ("Chile/EasterIsland", "Chile/EasterIsland"),
 | 
			
		||||
                    ("Cuba", "Cuba"),
 | 
			
		||||
                    ("EET", "EET"),
 | 
			
		||||
                    ("EST", "EST"),
 | 
			
		||||
                    ("EST5EDT", "EST5EDT"),
 | 
			
		||||
                    ("Egypt", "Egypt"),
 | 
			
		||||
                    ("Eire", "Eire"),
 | 
			
		||||
                    ("Etc/GMT", "Etc/GMT"),
 | 
			
		||||
                    ("Etc/GMT+0", "Etc/GMT+0"),
 | 
			
		||||
                    ("Etc/GMT+1", "Etc/GMT+1"),
 | 
			
		||||
                    ("Etc/GMT+10", "Etc/GMT+10"),
 | 
			
		||||
                    ("Etc/GMT+11", "Etc/GMT+11"),
 | 
			
		||||
                    ("Etc/GMT+12", "Etc/GMT+12"),
 | 
			
		||||
                    ("Etc/GMT+2", "Etc/GMT+2"),
 | 
			
		||||
                    ("Etc/GMT+3", "Etc/GMT+3"),
 | 
			
		||||
                    ("Etc/GMT+4", "Etc/GMT+4"),
 | 
			
		||||
                    ("Etc/GMT+5", "Etc/GMT+5"),
 | 
			
		||||
                    ("Etc/GMT+6", "Etc/GMT+6"),
 | 
			
		||||
                    ("Etc/GMT+7", "Etc/GMT+7"),
 | 
			
		||||
                    ("Etc/GMT+8", "Etc/GMT+8"),
 | 
			
		||||
                    ("Etc/GMT+9", "Etc/GMT+9"),
 | 
			
		||||
                    ("Etc/GMT-0", "Etc/GMT-0"),
 | 
			
		||||
                    ("Etc/GMT-1", "Etc/GMT-1"),
 | 
			
		||||
                    ("Etc/GMT-10", "Etc/GMT-10"),
 | 
			
		||||
                    ("Etc/GMT-11", "Etc/GMT-11"),
 | 
			
		||||
                    ("Etc/GMT-12", "Etc/GMT-12"),
 | 
			
		||||
                    ("Etc/GMT-13", "Etc/GMT-13"),
 | 
			
		||||
                    ("Etc/GMT-14", "Etc/GMT-14"),
 | 
			
		||||
                    ("Etc/GMT-2", "Etc/GMT-2"),
 | 
			
		||||
                    ("Etc/GMT-3", "Etc/GMT-3"),
 | 
			
		||||
                    ("Etc/GMT-4", "Etc/GMT-4"),
 | 
			
		||||
                    ("Etc/GMT-5", "Etc/GMT-5"),
 | 
			
		||||
                    ("Etc/GMT-6", "Etc/GMT-6"),
 | 
			
		||||
                    ("Etc/GMT-7", "Etc/GMT-7"),
 | 
			
		||||
                    ("Etc/GMT-8", "Etc/GMT-8"),
 | 
			
		||||
                    ("Etc/GMT-9", "Etc/GMT-9"),
 | 
			
		||||
                    ("Etc/GMT0", "Etc/GMT0"),
 | 
			
		||||
                    ("Etc/Greenwich", "Etc/Greenwich"),
 | 
			
		||||
                    ("Etc/UCT", "Etc/UCT"),
 | 
			
		||||
                    ("Etc/UTC", "Etc/UTC"),
 | 
			
		||||
                    ("Etc/Universal", "Etc/Universal"),
 | 
			
		||||
                    ("Etc/Zulu", "Etc/Zulu"),
 | 
			
		||||
                    ("Europe/Amsterdam", "Europe/Amsterdam"),
 | 
			
		||||
                    ("Europe/Andorra", "Europe/Andorra"),
 | 
			
		||||
                    ("Europe/Astrakhan", "Europe/Astrakhan"),
 | 
			
		||||
                    ("Europe/Athens", "Europe/Athens"),
 | 
			
		||||
                    ("Europe/Belfast", "Europe/Belfast"),
 | 
			
		||||
                    ("Europe/Belgrade", "Europe/Belgrade"),
 | 
			
		||||
                    ("Europe/Berlin", "Europe/Berlin"),
 | 
			
		||||
                    ("Europe/Bratislava", "Europe/Bratislava"),
 | 
			
		||||
                    ("Europe/Brussels", "Europe/Brussels"),
 | 
			
		||||
                    ("Europe/Bucharest", "Europe/Bucharest"),
 | 
			
		||||
                    ("Europe/Budapest", "Europe/Budapest"),
 | 
			
		||||
                    ("Europe/Busingen", "Europe/Busingen"),
 | 
			
		||||
                    ("Europe/Chisinau", "Europe/Chisinau"),
 | 
			
		||||
                    ("Europe/Copenhagen", "Europe/Copenhagen"),
 | 
			
		||||
                    ("Europe/Dublin", "Europe/Dublin"),
 | 
			
		||||
                    ("Europe/Gibraltar", "Europe/Gibraltar"),
 | 
			
		||||
                    ("Europe/Guernsey", "Europe/Guernsey"),
 | 
			
		||||
                    ("Europe/Helsinki", "Europe/Helsinki"),
 | 
			
		||||
                    ("Europe/Isle_of_Man", "Europe/Isle_of_Man"),
 | 
			
		||||
                    ("Europe/Istanbul", "Europe/Istanbul"),
 | 
			
		||||
                    ("Europe/Jersey", "Europe/Jersey"),
 | 
			
		||||
                    ("Europe/Kaliningrad", "Europe/Kaliningrad"),
 | 
			
		||||
                    ("Europe/Kiev", "Europe/Kiev"),
 | 
			
		||||
                    ("Europe/Kirov", "Europe/Kirov"),
 | 
			
		||||
                    ("Europe/Kyiv", "Europe/Kyiv"),
 | 
			
		||||
                    ("Europe/Lisbon", "Europe/Lisbon"),
 | 
			
		||||
                    ("Europe/Ljubljana", "Europe/Ljubljana"),
 | 
			
		||||
                    ("Europe/London", "Europe/London"),
 | 
			
		||||
                    ("Europe/Luxembourg", "Europe/Luxembourg"),
 | 
			
		||||
                    ("Europe/Madrid", "Europe/Madrid"),
 | 
			
		||||
                    ("Europe/Malta", "Europe/Malta"),
 | 
			
		||||
                    ("Europe/Mariehamn", "Europe/Mariehamn"),
 | 
			
		||||
                    ("Europe/Minsk", "Europe/Minsk"),
 | 
			
		||||
                    ("Europe/Monaco", "Europe/Monaco"),
 | 
			
		||||
                    ("Europe/Moscow", "Europe/Moscow"),
 | 
			
		||||
                    ("Europe/Nicosia", "Europe/Nicosia"),
 | 
			
		||||
                    ("Europe/Oslo", "Europe/Oslo"),
 | 
			
		||||
                    ("Europe/Paris", "Europe/Paris"),
 | 
			
		||||
                    ("Europe/Podgorica", "Europe/Podgorica"),
 | 
			
		||||
                    ("Europe/Prague", "Europe/Prague"),
 | 
			
		||||
                    ("Europe/Riga", "Europe/Riga"),
 | 
			
		||||
                    ("Europe/Rome", "Europe/Rome"),
 | 
			
		||||
                    ("Europe/Samara", "Europe/Samara"),
 | 
			
		||||
                    ("Europe/San_Marino", "Europe/San_Marino"),
 | 
			
		||||
                    ("Europe/Sarajevo", "Europe/Sarajevo"),
 | 
			
		||||
                    ("Europe/Saratov", "Europe/Saratov"),
 | 
			
		||||
                    ("Europe/Simferopol", "Europe/Simferopol"),
 | 
			
		||||
                    ("Europe/Skopje", "Europe/Skopje"),
 | 
			
		||||
                    ("Europe/Sofia", "Europe/Sofia"),
 | 
			
		||||
                    ("Europe/Stockholm", "Europe/Stockholm"),
 | 
			
		||||
                    ("Europe/Tallinn", "Europe/Tallinn"),
 | 
			
		||||
                    ("Europe/Tirane", "Europe/Tirane"),
 | 
			
		||||
                    ("Europe/Tiraspol", "Europe/Tiraspol"),
 | 
			
		||||
                    ("Europe/Ulyanovsk", "Europe/Ulyanovsk"),
 | 
			
		||||
                    ("Europe/Uzhgorod", "Europe/Uzhgorod"),
 | 
			
		||||
                    ("Europe/Vaduz", "Europe/Vaduz"),
 | 
			
		||||
                    ("Europe/Vatican", "Europe/Vatican"),
 | 
			
		||||
                    ("Europe/Vienna", "Europe/Vienna"),
 | 
			
		||||
                    ("Europe/Vilnius", "Europe/Vilnius"),
 | 
			
		||||
                    ("Europe/Volgograd", "Europe/Volgograd"),
 | 
			
		||||
                    ("Europe/Warsaw", "Europe/Warsaw"),
 | 
			
		||||
                    ("Europe/Zagreb", "Europe/Zagreb"),
 | 
			
		||||
                    ("Europe/Zaporozhye", "Europe/Zaporozhye"),
 | 
			
		||||
                    ("Europe/Zurich", "Europe/Zurich"),
 | 
			
		||||
                    ("Factory", "Factory"),
 | 
			
		||||
                    ("GB", "GB"),
 | 
			
		||||
                    ("GB-Eire", "GB-Eire"),
 | 
			
		||||
                    ("GMT", "GMT"),
 | 
			
		||||
                    ("GMT+0", "GMT+0"),
 | 
			
		||||
                    ("GMT-0", "GMT-0"),
 | 
			
		||||
                    ("GMT0", "GMT0"),
 | 
			
		||||
                    ("Greenwich", "Greenwich"),
 | 
			
		||||
                    ("HST", "HST"),
 | 
			
		||||
                    ("Hongkong", "Hongkong"),
 | 
			
		||||
                    ("Iceland", "Iceland"),
 | 
			
		||||
                    ("Indian/Antananarivo", "Indian/Antananarivo"),
 | 
			
		||||
                    ("Indian/Chagos", "Indian/Chagos"),
 | 
			
		||||
                    ("Indian/Christmas", "Indian/Christmas"),
 | 
			
		||||
                    ("Indian/Cocos", "Indian/Cocos"),
 | 
			
		||||
                    ("Indian/Comoro", "Indian/Comoro"),
 | 
			
		||||
                    ("Indian/Kerguelen", "Indian/Kerguelen"),
 | 
			
		||||
                    ("Indian/Mahe", "Indian/Mahe"),
 | 
			
		||||
                    ("Indian/Maldives", "Indian/Maldives"),
 | 
			
		||||
                    ("Indian/Mauritius", "Indian/Mauritius"),
 | 
			
		||||
                    ("Indian/Mayotte", "Indian/Mayotte"),
 | 
			
		||||
                    ("Indian/Reunion", "Indian/Reunion"),
 | 
			
		||||
                    ("Iran", "Iran"),
 | 
			
		||||
                    ("Israel", "Israel"),
 | 
			
		||||
                    ("Jamaica", "Jamaica"),
 | 
			
		||||
                    ("Japan", "Japan"),
 | 
			
		||||
                    ("Kwajalein", "Kwajalein"),
 | 
			
		||||
                    ("Libya", "Libya"),
 | 
			
		||||
                    ("MET", "MET"),
 | 
			
		||||
                    ("MST", "MST"),
 | 
			
		||||
                    ("MST7MDT", "MST7MDT"),
 | 
			
		||||
                    ("Mexico/BajaNorte", "Mexico/BajaNorte"),
 | 
			
		||||
                    ("Mexico/BajaSur", "Mexico/BajaSur"),
 | 
			
		||||
                    ("Mexico/General", "Mexico/General"),
 | 
			
		||||
                    ("NZ", "NZ"),
 | 
			
		||||
                    ("NZ-CHAT", "NZ-CHAT"),
 | 
			
		||||
                    ("Navajo", "Navajo"),
 | 
			
		||||
                    ("PRC", "PRC"),
 | 
			
		||||
                    ("PST8PDT", "PST8PDT"),
 | 
			
		||||
                    ("Pacific/Apia", "Pacific/Apia"),
 | 
			
		||||
                    ("Pacific/Auckland", "Pacific/Auckland"),
 | 
			
		||||
                    ("Pacific/Bougainville", "Pacific/Bougainville"),
 | 
			
		||||
                    ("Pacific/Chatham", "Pacific/Chatham"),
 | 
			
		||||
                    ("Pacific/Chuuk", "Pacific/Chuuk"),
 | 
			
		||||
                    ("Pacific/Easter", "Pacific/Easter"),
 | 
			
		||||
                    ("Pacific/Efate", "Pacific/Efate"),
 | 
			
		||||
                    ("Pacific/Enderbury", "Pacific/Enderbury"),
 | 
			
		||||
                    ("Pacific/Fakaofo", "Pacific/Fakaofo"),
 | 
			
		||||
                    ("Pacific/Fiji", "Pacific/Fiji"),
 | 
			
		||||
                    ("Pacific/Funafuti", "Pacific/Funafuti"),
 | 
			
		||||
                    ("Pacific/Galapagos", "Pacific/Galapagos"),
 | 
			
		||||
                    ("Pacific/Gambier", "Pacific/Gambier"),
 | 
			
		||||
                    ("Pacific/Guadalcanal", "Pacific/Guadalcanal"),
 | 
			
		||||
                    ("Pacific/Guam", "Pacific/Guam"),
 | 
			
		||||
                    ("Pacific/Honolulu", "Pacific/Honolulu"),
 | 
			
		||||
                    ("Pacific/Johnston", "Pacific/Johnston"),
 | 
			
		||||
                    ("Pacific/Kanton", "Pacific/Kanton"),
 | 
			
		||||
                    ("Pacific/Kiritimati", "Pacific/Kiritimati"),
 | 
			
		||||
                    ("Pacific/Kosrae", "Pacific/Kosrae"),
 | 
			
		||||
                    ("Pacific/Kwajalein", "Pacific/Kwajalein"),
 | 
			
		||||
                    ("Pacific/Majuro", "Pacific/Majuro"),
 | 
			
		||||
                    ("Pacific/Marquesas", "Pacific/Marquesas"),
 | 
			
		||||
                    ("Pacific/Midway", "Pacific/Midway"),
 | 
			
		||||
                    ("Pacific/Nauru", "Pacific/Nauru"),
 | 
			
		||||
                    ("Pacific/Niue", "Pacific/Niue"),
 | 
			
		||||
                    ("Pacific/Norfolk", "Pacific/Norfolk"),
 | 
			
		||||
                    ("Pacific/Noumea", "Pacific/Noumea"),
 | 
			
		||||
                    ("Pacific/Pago_Pago", "Pacific/Pago_Pago"),
 | 
			
		||||
                    ("Pacific/Palau", "Pacific/Palau"),
 | 
			
		||||
                    ("Pacific/Pitcairn", "Pacific/Pitcairn"),
 | 
			
		||||
                    ("Pacific/Pohnpei", "Pacific/Pohnpei"),
 | 
			
		||||
                    ("Pacific/Ponape", "Pacific/Ponape"),
 | 
			
		||||
                    ("Pacific/Port_Moresby", "Pacific/Port_Moresby"),
 | 
			
		||||
                    ("Pacific/Rarotonga", "Pacific/Rarotonga"),
 | 
			
		||||
                    ("Pacific/Saipan", "Pacific/Saipan"),
 | 
			
		||||
                    ("Pacific/Samoa", "Pacific/Samoa"),
 | 
			
		||||
                    ("Pacific/Tahiti", "Pacific/Tahiti"),
 | 
			
		||||
                    ("Pacific/Tarawa", "Pacific/Tarawa"),
 | 
			
		||||
                    ("Pacific/Tongatapu", "Pacific/Tongatapu"),
 | 
			
		||||
                    ("Pacific/Truk", "Pacific/Truk"),
 | 
			
		||||
                    ("Pacific/Wake", "Pacific/Wake"),
 | 
			
		||||
                    ("Pacific/Wallis", "Pacific/Wallis"),
 | 
			
		||||
                    ("Pacific/Yap", "Pacific/Yap"),
 | 
			
		||||
                    ("Poland", "Poland"),
 | 
			
		||||
                    ("Portugal", "Portugal"),
 | 
			
		||||
                    ("ROC", "ROC"),
 | 
			
		||||
                    ("ROK", "ROK"),
 | 
			
		||||
                    ("Singapore", "Singapore"),
 | 
			
		||||
                    ("Turkey", "Turkey"),
 | 
			
		||||
                    ("UCT", "UCT"),
 | 
			
		||||
                    ("US/Alaska", "US/Alaska"),
 | 
			
		||||
                    ("US/Aleutian", "US/Aleutian"),
 | 
			
		||||
                    ("US/Arizona", "US/Arizona"),
 | 
			
		||||
                    ("US/Central", "US/Central"),
 | 
			
		||||
                    ("US/East-Indiana", "US/East-Indiana"),
 | 
			
		||||
                    ("US/Eastern", "US/Eastern"),
 | 
			
		||||
                    ("US/Hawaii", "US/Hawaii"),
 | 
			
		||||
                    ("US/Indiana-Starke", "US/Indiana-Starke"),
 | 
			
		||||
                    ("US/Michigan", "US/Michigan"),
 | 
			
		||||
                    ("US/Mountain", "US/Mountain"),
 | 
			
		||||
                    ("US/Pacific", "US/Pacific"),
 | 
			
		||||
                    ("US/Samoa", "US/Samoa"),
 | 
			
		||||
                    ("UTC", "UTC"),
 | 
			
		||||
                    ("Universal", "Universal"),
 | 
			
		||||
                    ("W-SU", "W-SU"),
 | 
			
		||||
                    ("WET", "WET"),
 | 
			
		||||
                    ("Zulu", "Zulu"),
 | 
			
		||||
                    ("localtime", "localtime"),
 | 
			
		||||
                ],
 | 
			
		||||
                max_length=255,
 | 
			
		||||
                null=True,
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
# Generated by Django 4.2.10 on 2024-02-19 05:57
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("agents", "0058_alter_agent_time_zone"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="agenthistory",
 | 
			
		||||
            name="id",
 | 
			
		||||
            field=models.BigAutoField(primary_key=True, serialize=False),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -1,30 +1,51 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
import logging
 | 
			
		||||
import re
 | 
			
		||||
from collections import Counter
 | 
			
		||||
from distutils.version import LooseVersion
 | 
			
		||||
from typing import Any, Optional, List, Dict, Union, Sequence, cast, TYPE_CHECKING
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from packaging import version as pyver
 | 
			
		||||
from contextlib import suppress
 | 
			
		||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Union, cast
 | 
			
		||||
 | 
			
		||||
import msgpack
 | 
			
		||||
import nats
 | 
			
		||||
import validators
 | 
			
		||||
from asgiref.sync import sync_to_async
 | 
			
		||||
from core.models import TZ_CHOICES
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.contrib.postgres.fields import ArrayField
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from logs.models import BaseAuditModel, DebugLog
 | 
			
		||||
from nats.errors import TimeoutError
 | 
			
		||||
from packaging import version as pyver
 | 
			
		||||
from packaging.version import Version as LooseVersion
 | 
			
		||||
 | 
			
		||||
from core.utils import get_core_settings
 | 
			
		||||
from agents.utils import get_agent_url
 | 
			
		||||
from checks.models import CheckResult
 | 
			
		||||
from core.models import TZ_CHOICES
 | 
			
		||||
from core.utils import _b64_to_hex, get_core_settings, send_command_with_mesh
 | 
			
		||||
from logs.models import BaseAuditModel, DebugLog, PendingAction
 | 
			
		||||
from tacticalrmm.constants import (
 | 
			
		||||
    AGENT_STATUS_OFFLINE,
 | 
			
		||||
    AGENT_STATUS_ONLINE,
 | 
			
		||||
    AGENT_STATUS_OVERDUE,
 | 
			
		||||
    AGENT_TBL_PEND_ACTION_CNT_CACHE_PREFIX,
 | 
			
		||||
    ONLINE_AGENTS,
 | 
			
		||||
    AgentHistoryType,
 | 
			
		||||
    AgentMonType,
 | 
			
		||||
    AgentPlat,
 | 
			
		||||
    AlertSeverity,
 | 
			
		||||
    CheckStatus,
 | 
			
		||||
    CheckType,
 | 
			
		||||
    CustomFieldType,
 | 
			
		||||
    DebugLogType,
 | 
			
		||||
    GoArch,
 | 
			
		||||
    PAAction,
 | 
			
		||||
    PAStatus,
 | 
			
		||||
)
 | 
			
		||||
from tacticalrmm.helpers import setup_nats_options
 | 
			
		||||
from tacticalrmm.models import PermissionQuerySet
 | 
			
		||||
from tacticalrmm.constants import ONLINE_AGENTS
 | 
			
		||||
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
    from alerts.models import Alert, AlertTemplate
 | 
			
		||||
    from automation.models import Policy
 | 
			
		||||
    from alerts.models import AlertTemplate, Alert
 | 
			
		||||
    from autotasks.models import AutomatedTask
 | 
			
		||||
    from checks.models import Check
 | 
			
		||||
    from clients.models import Client
 | 
			
		||||
@@ -33,6 +54,8 @@ if TYPE_CHECKING:
 | 
			
		||||
# type helpers
 | 
			
		||||
Disk = Union[Dict[str, Any], str]
 | 
			
		||||
 | 
			
		||||
logger = logging.getLogger("trmm")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Agent(BaseAuditModel):
 | 
			
		||||
    class Meta:
 | 
			
		||||
@@ -44,9 +67,12 @@ class Agent(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
    version = models.CharField(default="0.1.0", max_length=255)
 | 
			
		||||
    operating_system = models.CharField(null=True, blank=True, max_length=255)
 | 
			
		||||
    plat = models.CharField(max_length=255, default="windows")
 | 
			
		||||
    goarch = models.CharField(max_length=255, null=True, blank=True)
 | 
			
		||||
    plat_release = models.CharField(max_length=255, null=True, blank=True)
 | 
			
		||||
    plat: "AgentPlat" = models.CharField(  # type: ignore
 | 
			
		||||
        max_length=255, choices=AgentPlat.choices, default=AgentPlat.WINDOWS
 | 
			
		||||
    )
 | 
			
		||||
    goarch: "GoArch" = models.CharField(  # type: ignore
 | 
			
		||||
        max_length=255, choices=GoArch.choices, null=True, blank=True
 | 
			
		||||
    )
 | 
			
		||||
    hostname = models.CharField(max_length=255)
 | 
			
		||||
    agent_id = models.CharField(max_length=200, unique=True)
 | 
			
		||||
    last_seen = models.DateTimeField(null=True, blank=True)
 | 
			
		||||
@@ -57,7 +83,9 @@ class Agent(BaseAuditModel):
 | 
			
		||||
    boot_time = models.FloatField(null=True, blank=True)
 | 
			
		||||
    logged_in_username = models.CharField(null=True, blank=True, max_length=255)
 | 
			
		||||
    last_logged_in_user = models.CharField(null=True, blank=True, max_length=255)
 | 
			
		||||
    monitoring_type = models.CharField(max_length=30)
 | 
			
		||||
    monitoring_type = models.CharField(
 | 
			
		||||
        max_length=30, choices=AgentMonType.choices, default=AgentMonType.SERVER
 | 
			
		||||
    )
 | 
			
		||||
    description = models.CharField(null=True, blank=True, max_length=255)
 | 
			
		||||
    mesh_node_id = models.CharField(null=True, blank=True, max_length=255)
 | 
			
		||||
    overdue_email_alert = models.BooleanField(default=False)
 | 
			
		||||
@@ -98,6 +126,22 @@ class Agent(BaseAuditModel):
 | 
			
		||||
    def __str__(self) -> str:
 | 
			
		||||
        return self.hostname
 | 
			
		||||
 | 
			
		||||
    def save(self, *args, **kwargs):
 | 
			
		||||
        # prevent recursion since calling set_alert_template() also calls save()
 | 
			
		||||
        if not hasattr(self, "_processing_set_alert_template"):
 | 
			
		||||
            self._processing_set_alert_template = False
 | 
			
		||||
 | 
			
		||||
        if self.pk and not self._processing_set_alert_template:
 | 
			
		||||
            orig = Agent.objects.get(pk=self.pk)
 | 
			
		||||
            mon_type_changed = self.monitoring_type != orig.monitoring_type
 | 
			
		||||
            site_changed = self.site_id != orig.site_id
 | 
			
		||||
            if mon_type_changed or site_changed:
 | 
			
		||||
                self._processing_set_alert_template = True
 | 
			
		||||
                self.set_alert_template()
 | 
			
		||||
                self._processing_set_alert_template = False
 | 
			
		||||
 | 
			
		||||
        super().save(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def client(self) -> "Client":
 | 
			
		||||
        return self.site.client
 | 
			
		||||
@@ -107,13 +151,14 @@ class Agent(BaseAuditModel):
 | 
			
		||||
        # return the default timezone unless the timezone is explicity set per agent
 | 
			
		||||
        if self.time_zone:
 | 
			
		||||
            return self.time_zone
 | 
			
		||||
        else:
 | 
			
		||||
            return get_core_settings().default_time_zone
 | 
			
		||||
 | 
			
		||||
        return get_core_settings().default_time_zone
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def is_posix(self) -> bool:
 | 
			
		||||
        return self.plat == "linux" or self.plat == "darwin"
 | 
			
		||||
        return self.plat in {AgentPlat.LINUX, AgentPlat.DARWIN}
 | 
			
		||||
 | 
			
		||||
    # DEPRECATED, use goarch instead
 | 
			
		||||
    @property
 | 
			
		||||
    def arch(self) -> Optional[str]:
 | 
			
		||||
        if self.is_posix:
 | 
			
		||||
@@ -126,41 +171,70 @@ class Agent(BaseAuditModel):
 | 
			
		||||
                return "32"
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def winagent_dl(self) -> Optional[str]:
 | 
			
		||||
        if self.arch == "64":
 | 
			
		||||
            return settings.DL_64
 | 
			
		||||
        elif self.arch == "32":
 | 
			
		||||
            return settings.DL_32
 | 
			
		||||
        return None
 | 
			
		||||
    def do_update(self, *, token: str = "", force: bool = False) -> str:
 | 
			
		||||
        ver = settings.LATEST_AGENT_VER
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def win_inno_exe(self) -> Optional[str]:
 | 
			
		||||
        if self.arch == "64":
 | 
			
		||||
            return f"winagent-v{settings.LATEST_AGENT_VER}.exe"
 | 
			
		||||
        elif self.arch == "32":
 | 
			
		||||
            return f"winagent-v{settings.LATEST_AGENT_VER}-x86.exe"
 | 
			
		||||
        return None
 | 
			
		||||
        if not self.goarch:
 | 
			
		||||
            DebugLog.warning(
 | 
			
		||||
                agent=self,
 | 
			
		||||
                log_type=DebugLogType.AGENT_ISSUES,
 | 
			
		||||
                message=f"Unable to determine arch on {self.hostname}({self.agent_id}). Skipping agent update.",
 | 
			
		||||
            )
 | 
			
		||||
            return "noarch"
 | 
			
		||||
 | 
			
		||||
        if pyver.parse(self.version) <= pyver.parse("1.3.0"):
 | 
			
		||||
            return "not supported"
 | 
			
		||||
 | 
			
		||||
        url = get_agent_url(goarch=self.goarch, plat=self.plat, token=token)
 | 
			
		||||
        bin = f"tacticalagent-v{ver}-{self.plat}-{self.goarch}.exe"
 | 
			
		||||
 | 
			
		||||
        if not force:
 | 
			
		||||
            if self.pendingactions.filter(  # type: ignore
 | 
			
		||||
                action_type=PAAction.AGENT_UPDATE, status=PAStatus.PENDING
 | 
			
		||||
            ).exists():
 | 
			
		||||
                self.pendingactions.filter(  # type: ignore
 | 
			
		||||
                    action_type=PAAction.AGENT_UPDATE, status=PAStatus.PENDING
 | 
			
		||||
                ).delete()
 | 
			
		||||
 | 
			
		||||
            PendingAction.objects.create(
 | 
			
		||||
                agent=self,
 | 
			
		||||
                action_type=PAAction.AGENT_UPDATE,
 | 
			
		||||
                details={
 | 
			
		||||
                    "url": url,
 | 
			
		||||
                    "version": ver,
 | 
			
		||||
                    "inno": bin,
 | 
			
		||||
                },
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        nats_data = {
 | 
			
		||||
            "func": "agentupdate",
 | 
			
		||||
            "payload": {
 | 
			
		||||
                "url": url,
 | 
			
		||||
                "version": ver,
 | 
			
		||||
                "inno": bin,
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
        asyncio.run(self.nats_cmd(nats_data, wait=False))
 | 
			
		||||
        return "created"
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def status(self) -> str:
 | 
			
		||||
        offline = djangotime.now() - djangotime.timedelta(minutes=self.offline_time)
 | 
			
		||||
        overdue = djangotime.now() - djangotime.timedelta(minutes=self.overdue_time)
 | 
			
		||||
        now = djangotime.now()
 | 
			
		||||
        offline = now - djangotime.timedelta(minutes=self.offline_time)
 | 
			
		||||
        overdue = now - djangotime.timedelta(minutes=self.overdue_time)
 | 
			
		||||
 | 
			
		||||
        if self.last_seen is not None:
 | 
			
		||||
            if (self.last_seen < offline) and (self.last_seen > overdue):
 | 
			
		||||
                return "offline"
 | 
			
		||||
                return AGENT_STATUS_OFFLINE
 | 
			
		||||
            elif (self.last_seen < offline) and (self.last_seen < overdue):
 | 
			
		||||
                return "overdue"
 | 
			
		||||
                return AGENT_STATUS_OVERDUE
 | 
			
		||||
            else:
 | 
			
		||||
                return "online"
 | 
			
		||||
                return AGENT_STATUS_ONLINE
 | 
			
		||||
        else:
 | 
			
		||||
            return "offline"
 | 
			
		||||
            return AGENT_STATUS_OFFLINE
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def checks(self) -> Dict[str, Any]:
 | 
			
		||||
        from checks.models import CheckResult
 | 
			
		||||
 | 
			
		||||
        total, passing, failing, warning, info = 0, 0, 0, 0, 0
 | 
			
		||||
 | 
			
		||||
        for check in self.get_checks_with_policies(exclude_overridden=True):
 | 
			
		||||
@@ -168,23 +242,29 @@ class Agent(BaseAuditModel):
 | 
			
		||||
            if (
 | 
			
		||||
                not hasattr(check.check_result, "status")
 | 
			
		||||
                or isinstance(check.check_result, CheckResult)
 | 
			
		||||
                and check.check_result.status == "passing"
 | 
			
		||||
                and check.check_result.status == CheckStatus.PASSING
 | 
			
		||||
            ):
 | 
			
		||||
                passing += 1
 | 
			
		||||
            elif (
 | 
			
		||||
                isinstance(check.check_result, CheckResult)
 | 
			
		||||
                and check.check_result.status == "failing"
 | 
			
		||||
                and check.check_result.status == CheckStatus.FAILING
 | 
			
		||||
            ):
 | 
			
		||||
                alert_severity = (
 | 
			
		||||
                    check.check_result.alert_severity
 | 
			
		||||
                    if check.check_type in ["memory", "cpuload", "diskspace", "script"]
 | 
			
		||||
                    if check.check_type
 | 
			
		||||
                    in (
 | 
			
		||||
                        CheckType.MEMORY,
 | 
			
		||||
                        CheckType.CPU_LOAD,
 | 
			
		||||
                        CheckType.DISK_SPACE,
 | 
			
		||||
                        CheckType.SCRIPT,
 | 
			
		||||
                    )
 | 
			
		||||
                    else check.alert_severity
 | 
			
		||||
                )
 | 
			
		||||
                if alert_severity == "error":
 | 
			
		||||
                if alert_severity == AlertSeverity.ERROR:
 | 
			
		||||
                    failing += 1
 | 
			
		||||
                elif alert_severity == "warning":
 | 
			
		||||
                elif alert_severity == AlertSeverity.WARNING:
 | 
			
		||||
                    warning += 1
 | 
			
		||||
                elif alert_severity == "info":
 | 
			
		||||
                elif alert_severity == AlertSeverity.INFO:
 | 
			
		||||
                    info += 1
 | 
			
		||||
 | 
			
		||||
        ret = {
 | 
			
		||||
@@ -197,6 +277,15 @@ class Agent(BaseAuditModel):
 | 
			
		||||
        }
 | 
			
		||||
        return ret
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def pending_actions_count(self) -> int:
 | 
			
		||||
        ret = cache.get(f"{AGENT_TBL_PEND_ACTION_CNT_CACHE_PREFIX}{self.pk}")
 | 
			
		||||
        if ret is None:
 | 
			
		||||
            ret = self.pendingactions.filter(status=PAStatus.PENDING).count()
 | 
			
		||||
            cache.set(f"{AGENT_TBL_PEND_ACTION_CNT_CACHE_PREFIX}{self.pk}", ret, 600)
 | 
			
		||||
 | 
			
		||||
        return ret
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def cpu_model(self) -> List[str]:
 | 
			
		||||
        if self.is_posix:
 | 
			
		||||
@@ -209,7 +298,20 @@ class Agent(BaseAuditModel):
 | 
			
		||||
        try:
 | 
			
		||||
            cpus = self.wmi_detail["cpu"]
 | 
			
		||||
            for cpu in cpus:
 | 
			
		||||
                ret.append([x["Name"] for x in cpu if "Name" in x][0])
 | 
			
		||||
                name = [x["Name"] for x in cpu if "Name" in x][0]
 | 
			
		||||
                lp, nc = "", ""
 | 
			
		||||
                with suppress(Exception):
 | 
			
		||||
                    lp = [
 | 
			
		||||
                        x["NumberOfLogicalProcessors"]
 | 
			
		||||
                        for x in cpu
 | 
			
		||||
                        if "NumberOfCores" in x
 | 
			
		||||
                    ][0]
 | 
			
		||||
                    nc = [x["NumberOfCores"] for x in cpu if "NumberOfCores" in x][0]
 | 
			
		||||
                if lp and nc:
 | 
			
		||||
                    cpu_string = f"{name}, {nc}C/{lp}T"
 | 
			
		||||
                else:
 | 
			
		||||
                    cpu_string = name
 | 
			
		||||
                ret.append(cpu_string)
 | 
			
		||||
            return ret
 | 
			
		||||
        except:
 | 
			
		||||
            return ["unknown cpu model"]
 | 
			
		||||
@@ -273,8 +375,8 @@ class Agent(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
        if len(ret) == 1:
 | 
			
		||||
            return cast(str, ret[0])
 | 
			
		||||
        else:
 | 
			
		||||
            return ", ".join(ret) if ret else "error getting local ips"
 | 
			
		||||
 | 
			
		||||
        return ", ".join(ret) if ret else "error getting local ips"
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def make_model(self) -> str:
 | 
			
		||||
@@ -284,7 +386,7 @@ class Agent(BaseAuditModel):
 | 
			
		||||
            except:
 | 
			
		||||
                return "error getting make/model"
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
        with suppress(Exception):
 | 
			
		||||
            comp_sys = self.wmi_detail["comp_sys"][0]
 | 
			
		||||
            comp_sys_prod = self.wmi_detail["comp_sys_prod"][0]
 | 
			
		||||
            make = [x["Vendor"] for x in comp_sys_prod if "Vendor" in x][0]
 | 
			
		||||
@@ -301,14 +403,10 @@ class Agent(BaseAuditModel):
 | 
			
		||||
                    model = sysfam
 | 
			
		||||
 | 
			
		||||
            return f"{make} {model}"
 | 
			
		||||
        except:
 | 
			
		||||
            pass
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
        with suppress(Exception):
 | 
			
		||||
            comp_sys_prod = self.wmi_detail["comp_sys_prod"][0]
 | 
			
		||||
            return cast(str, [x["Version"] for x in comp_sys_prod if "Version" in x][0])
 | 
			
		||||
        except:
 | 
			
		||||
            pass
 | 
			
		||||
 | 
			
		||||
        return "unknown make/model"
 | 
			
		||||
 | 
			
		||||
@@ -341,6 +439,23 @@ class Agent(BaseAuditModel):
 | 
			
		||||
        except:
 | 
			
		||||
            return ["unknown disk"]
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def serial_number(self) -> str:
 | 
			
		||||
        if self.is_posix:
 | 
			
		||||
            try:
 | 
			
		||||
                return self.wmi_detail["serialnumber"]
 | 
			
		||||
            except:
 | 
			
		||||
                return ""
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            return self.wmi_detail["bios"][0][0]["SerialNumber"]
 | 
			
		||||
        except:
 | 
			
		||||
            return ""
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def hex_mesh_node_id(self) -> str:
 | 
			
		||||
        return _b64_to_hex(self.mesh_node_id)
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def online_agents(cls, min_version: str = "") -> "List[Agent]":
 | 
			
		||||
        if min_version:
 | 
			
		||||
@@ -348,10 +463,14 @@ class Agent(BaseAuditModel):
 | 
			
		||||
                i
 | 
			
		||||
                for i in cls.objects.only(*ONLINE_AGENTS)
 | 
			
		||||
                if pyver.parse(i.version) >= pyver.parse(min_version)
 | 
			
		||||
                and i.status == "online"
 | 
			
		||||
                and i.status == AGENT_STATUS_ONLINE
 | 
			
		||||
            ]
 | 
			
		||||
 | 
			
		||||
        return [i for i in cls.objects.only(*ONLINE_AGENTS) if i.status == "online"]
 | 
			
		||||
        return [
 | 
			
		||||
            i
 | 
			
		||||
            for i in cls.objects.only(*ONLINE_AGENTS)
 | 
			
		||||
            if i.status == AGENT_STATUS_ONLINE
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
    def is_supported_script(self, platforms: List[str]) -> bool:
 | 
			
		||||
        return self.plat.lower() in platforms if platforms else True
 | 
			
		||||
@@ -359,7 +478,6 @@ class Agent(BaseAuditModel):
 | 
			
		||||
    def get_checks_with_policies(
 | 
			
		||||
        self, exclude_overridden: bool = False
 | 
			
		||||
    ) -> "List[Check]":
 | 
			
		||||
 | 
			
		||||
        if exclude_overridden:
 | 
			
		||||
            checks = (
 | 
			
		||||
                list(
 | 
			
		||||
@@ -374,12 +492,10 @@ class Agent(BaseAuditModel):
 | 
			
		||||
        return self.add_check_results(checks)
 | 
			
		||||
 | 
			
		||||
    def get_tasks_with_policies(self) -> "List[AutomatedTask]":
 | 
			
		||||
 | 
			
		||||
        tasks = list(self.autotasks.all()) + self.get_tasks_from_policies()
 | 
			
		||||
        return self.add_task_results(tasks)
 | 
			
		||||
 | 
			
		||||
    def add_task_results(self, tasks: "List[AutomatedTask]") -> "List[AutomatedTask]":
 | 
			
		||||
 | 
			
		||||
        results = self.taskresults.all()  # type: ignore
 | 
			
		||||
 | 
			
		||||
        for task in tasks:
 | 
			
		||||
@@ -391,7 +507,6 @@ class Agent(BaseAuditModel):
 | 
			
		||||
        return tasks
 | 
			
		||||
 | 
			
		||||
    def add_check_results(self, checks: "List[Check]") -> "List[Check]":
 | 
			
		||||
 | 
			
		||||
        results = self.checkresults.all()  # type: ignore
 | 
			
		||||
 | 
			
		||||
        for check in checks:
 | 
			
		||||
@@ -415,7 +530,7 @@ class Agent(BaseAuditModel):
 | 
			
		||||
        models.prefetch_related_objects(
 | 
			
		||||
            [
 | 
			
		||||
                policy
 | 
			
		||||
                for policy in [self.policy, site_policy, client_policy, default_policy]
 | 
			
		||||
                for policy in (self.policy, site_policy, client_policy, default_policy)
 | 
			
		||||
                if policy
 | 
			
		||||
            ],
 | 
			
		||||
            "excluded_agents",
 | 
			
		||||
@@ -428,24 +543,32 @@ class Agent(BaseAuditModel):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            "agent_policy": self.policy
 | 
			
		||||
            if self.policy and not self.policy.is_agent_excluded(self)
 | 
			
		||||
            else None,
 | 
			
		||||
            "site_policy": site_policy
 | 
			
		||||
            if (site_policy and not site_policy.is_agent_excluded(self))
 | 
			
		||||
            and not self.block_policy_inheritance
 | 
			
		||||
            else None,
 | 
			
		||||
            "client_policy": client_policy
 | 
			
		||||
            if (client_policy and not client_policy.is_agent_excluded(self))
 | 
			
		||||
            and not self.block_policy_inheritance
 | 
			
		||||
            and not self.site.block_policy_inheritance
 | 
			
		||||
            else None,
 | 
			
		||||
            "default_policy": default_policy
 | 
			
		||||
            if (default_policy and not default_policy.is_agent_excluded(self))
 | 
			
		||||
            and not self.block_policy_inheritance
 | 
			
		||||
            and not self.site.block_policy_inheritance
 | 
			
		||||
            and not self.client.block_policy_inheritance
 | 
			
		||||
            else None,
 | 
			
		||||
            "agent_policy": (
 | 
			
		||||
                self.policy
 | 
			
		||||
                if self.policy and not self.policy.is_agent_excluded(self)
 | 
			
		||||
                else None
 | 
			
		||||
            ),
 | 
			
		||||
            "site_policy": (
 | 
			
		||||
                site_policy
 | 
			
		||||
                if (site_policy and not site_policy.is_agent_excluded(self))
 | 
			
		||||
                and not self.block_policy_inheritance
 | 
			
		||||
                else None
 | 
			
		||||
            ),
 | 
			
		||||
            "client_policy": (
 | 
			
		||||
                client_policy
 | 
			
		||||
                if (client_policy and not client_policy.is_agent_excluded(self))
 | 
			
		||||
                and not self.block_policy_inheritance
 | 
			
		||||
                and not self.site.block_policy_inheritance
 | 
			
		||||
                else None
 | 
			
		||||
            ),
 | 
			
		||||
            "default_policy": (
 | 
			
		||||
                default_policy
 | 
			
		||||
                if (default_policy and not default_policy.is_agent_excluded(self))
 | 
			
		||||
                and not self.block_policy_inheritance
 | 
			
		||||
                and not self.site.block_policy_inheritance
 | 
			
		||||
                and not self.client.block_policy_inheritance
 | 
			
		||||
                else None
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def check_run_interval(self) -> int:
 | 
			
		||||
@@ -453,7 +576,6 @@ class Agent(BaseAuditModel):
 | 
			
		||||
        # determine if any agent checks have a custom interval and set the lowest interval
 | 
			
		||||
        for check in self.get_checks_with_policies():
 | 
			
		||||
            if check.run_interval and check.run_interval < interval:
 | 
			
		||||
 | 
			
		||||
                # don't allow check runs less than 15s
 | 
			
		||||
                interval = 15 if check.run_interval < 15 else check.run_interval
 | 
			
		||||
 | 
			
		||||
@@ -468,13 +590,19 @@ class Agent(BaseAuditModel):
 | 
			
		||||
        wait: bool = False,
 | 
			
		||||
        run_on_any: bool = False,
 | 
			
		||||
        history_pk: int = 0,
 | 
			
		||||
        run_as_user: bool = False,
 | 
			
		||||
        env_vars: list[str] = [],
 | 
			
		||||
    ) -> Any:
 | 
			
		||||
 | 
			
		||||
        from scripts.models import Script
 | 
			
		||||
 | 
			
		||||
        script = Script.objects.get(pk=scriptpk)
 | 
			
		||||
 | 
			
		||||
        # always override if set on script model
 | 
			
		||||
        if script.run_as_user:
 | 
			
		||||
            run_as_user = True
 | 
			
		||||
 | 
			
		||||
        parsed_args = script.parse_script_args(self, script.shell, args)
 | 
			
		||||
        parsed_env_vars = script.parse_script_env_vars(self, script.shell, env_vars)
 | 
			
		||||
 | 
			
		||||
        data = {
 | 
			
		||||
            "func": "runscriptfull" if full else "runscript",
 | 
			
		||||
@@ -484,6 +612,10 @@ class Agent(BaseAuditModel):
 | 
			
		||||
                "code": script.code,
 | 
			
		||||
                "shell": script.shell,
 | 
			
		||||
            },
 | 
			
		||||
            "run_as_user": run_as_user,
 | 
			
		||||
            "env_vars": parsed_env_vars,
 | 
			
		||||
            "nushell_enable_config": settings.NUSHELL_ENABLE_CONFIG,
 | 
			
		||||
            "deno_default_permissions": settings.DENO_DEFAULT_PERMISSIONS,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if history_pk != 0:
 | 
			
		||||
@@ -519,7 +651,7 @@ class Agent(BaseAuditModel):
 | 
			
		||||
    def approve_updates(self) -> None:
 | 
			
		||||
        patch_policy = self.get_patch_policy()
 | 
			
		||||
 | 
			
		||||
        severity_list = list()
 | 
			
		||||
        severity_list = []
 | 
			
		||||
        if patch_policy.critical == "approve":
 | 
			
		||||
            severity_list.append("Critical")
 | 
			
		||||
 | 
			
		||||
@@ -551,17 +683,14 @@ class Agent(BaseAuditModel):
 | 
			
		||||
        if not agent_policy:
 | 
			
		||||
            agent_policy = WinUpdatePolicy.objects.create(agent=self)
 | 
			
		||||
 | 
			
		||||
        # Get the list of policies applied to the agent and select the
 | 
			
		||||
        # highest priority one.
 | 
			
		||||
        policies = self.get_agent_policies()
 | 
			
		||||
 | 
			
		||||
        processed_policies: List[int] = list()
 | 
			
		||||
        for _, policy in policies.items():
 | 
			
		||||
            if (
 | 
			
		||||
                policy
 | 
			
		||||
                and policy.active
 | 
			
		||||
                and policy.pk not in processed_policies
 | 
			
		||||
                and policy.winupdatepolicy.exists()
 | 
			
		||||
            ):
 | 
			
		||||
            if policy and policy.active and policy.winupdatepolicy.exists():
 | 
			
		||||
                patch_policy = policy.winupdatepolicy.first()
 | 
			
		||||
                break
 | 
			
		||||
 | 
			
		||||
        # if policy still doesn't exist return the agent patch policy
 | 
			
		||||
        if not patch_policy:
 | 
			
		||||
@@ -613,7 +742,7 @@ class Agent(BaseAuditModel):
 | 
			
		||||
        policies = self.get_agent_policies()
 | 
			
		||||
 | 
			
		||||
        # loop through all policies applied to agent and return an alert_template if found
 | 
			
		||||
        processed_policies: List[int] = list()
 | 
			
		||||
        processed_policies: List[int] = []
 | 
			
		||||
        for key, policy in policies.items():
 | 
			
		||||
            # default alert_template will override a default policy with alert template applied
 | 
			
		||||
            if (
 | 
			
		||||
@@ -678,10 +807,10 @@ class Agent(BaseAuditModel):
 | 
			
		||||
            cache_key = f"agent_{self.agent_id}_checks"
 | 
			
		||||
 | 
			
		||||
        elif self.policy:
 | 
			
		||||
            cache_key = f"site_{self.monitoring_type}_{self.site_id}_policy_{self.policy_id}_checks"
 | 
			
		||||
            cache_key = f"site_{self.monitoring_type}_{self.plat}_{self.site_id}_policy_{self.policy_id}_checks"
 | 
			
		||||
 | 
			
		||||
        else:
 | 
			
		||||
            cache_key = f"site_{self.monitoring_type}_{self.site_id}_checks"
 | 
			
		||||
            cache_key = f"site_{self.monitoring_type}_{self.plat}_{self.site_id}_checks"
 | 
			
		||||
 | 
			
		||||
        cached_checks = cache.get(cache_key)
 | 
			
		||||
        if isinstance(cached_checks, list):
 | 
			
		||||
@@ -703,10 +832,10 @@ class Agent(BaseAuditModel):
 | 
			
		||||
            cache_key = f"agent_{self.agent_id}_tasks"
 | 
			
		||||
 | 
			
		||||
        elif self.policy:
 | 
			
		||||
            cache_key = f"site_{self.monitoring_type}_{self.site_id}_policy_{self.policy_id}_tasks"
 | 
			
		||||
            cache_key = f"site_{self.monitoring_type}_{self.plat}_{self.site_id}_policy_{self.policy_id}_tasks"
 | 
			
		||||
 | 
			
		||||
        else:
 | 
			
		||||
            cache_key = f"site_{self.monitoring_type}_{self.site_id}_tasks"
 | 
			
		||||
            cache_key = f"site_{self.monitoring_type}_{self.plat}_{self.site_id}_tasks"
 | 
			
		||||
 | 
			
		||||
        cached_tasks = cache.get(cache_key)
 | 
			
		||||
        if isinstance(cached_tasks, list):
 | 
			
		||||
@@ -714,25 +843,15 @@ class Agent(BaseAuditModel):
 | 
			
		||||
        else:
 | 
			
		||||
            # get agent tasks based on policies
 | 
			
		||||
            tasks = Policy.get_policy_tasks(self)
 | 
			
		||||
            cache.set(f"site_{self.site_id}_tasks", tasks, 600)
 | 
			
		||||
            cache.set(cache_key, tasks, 600)
 | 
			
		||||
            return tasks
 | 
			
		||||
 | 
			
		||||
    def _do_nats_debug(self, agent: "Agent", message: str) -> None:
 | 
			
		||||
        DebugLog.error(agent=agent, log_type="agent_issues", message=message)
 | 
			
		||||
 | 
			
		||||
    async def nats_cmd(
 | 
			
		||||
        self, data: Dict[Any, Any], timeout: int = 30, wait: bool = True
 | 
			
		||||
    ) -> Any:
 | 
			
		||||
        options = {
 | 
			
		||||
            "servers": f"tls://{settings.ALLOWED_HOSTS[0]}:4222",
 | 
			
		||||
            "user": "tacticalrmm",
 | 
			
		||||
            "password": settings.SECRET_KEY,
 | 
			
		||||
            "connect_timeout": 3,
 | 
			
		||||
            "max_reconnect_attempts": 2,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        opts = setup_nats_options()
 | 
			
		||||
        try:
 | 
			
		||||
            nc = await nats.connect(**options)
 | 
			
		||||
            nc = await nats.connect(**opts)
 | 
			
		||||
        except:
 | 
			
		||||
            return "natsdown"
 | 
			
		||||
 | 
			
		||||
@@ -748,9 +867,7 @@ class Agent(BaseAuditModel):
 | 
			
		||||
                    ret = msgpack.loads(msg.data)
 | 
			
		||||
                except Exception as e:
 | 
			
		||||
                    ret = str(e)
 | 
			
		||||
                    await sync_to_async(self._do_nats_debug, thread_sensitive=False)(
 | 
			
		||||
                        agent=self, message=ret
 | 
			
		||||
                    )
 | 
			
		||||
                    logger.error(e)
 | 
			
		||||
 | 
			
		||||
            await nc.close()
 | 
			
		||||
            return ret
 | 
			
		||||
@@ -759,6 +876,41 @@ class Agent(BaseAuditModel):
 | 
			
		||||
            await nc.flush()
 | 
			
		||||
            await nc.close()
 | 
			
		||||
 | 
			
		||||
    def recover(self, mode: str, mesh_uri: str, wait: bool = True) -> tuple[str, bool]:
 | 
			
		||||
        """
 | 
			
		||||
        Return type: tuple(message: str, error: bool)
 | 
			
		||||
        """
 | 
			
		||||
        if mode == "tacagent":
 | 
			
		||||
            if self.plat == AgentPlat.LINUX:
 | 
			
		||||
                cmd = "systemctl restart tacticalagent.service"
 | 
			
		||||
                shell = 3
 | 
			
		||||
            elif self.plat == AgentPlat.DARWIN:
 | 
			
		||||
                cmd = "launchctl kickstart -k system/tacticalagent"
 | 
			
		||||
                shell = 3
 | 
			
		||||
            else:
 | 
			
		||||
                cmd = "net stop tacticalrmm & taskkill /F /IM tacticalrmm.exe & net start tacticalrmm"
 | 
			
		||||
                shell = 1
 | 
			
		||||
 | 
			
		||||
            asyncio.run(
 | 
			
		||||
                send_command_with_mesh(cmd, mesh_uri, self.mesh_node_id, shell, 0)
 | 
			
		||||
            )
 | 
			
		||||
            return "ok", False
 | 
			
		||||
 | 
			
		||||
        elif mode == "mesh":
 | 
			
		||||
            data = {"func": "recover", "payload": {"mode": mode}}
 | 
			
		||||
            if wait:
 | 
			
		||||
                r = asyncio.run(self.nats_cmd(data, timeout=20))
 | 
			
		||||
                if r == "ok":
 | 
			
		||||
                    return "ok", False
 | 
			
		||||
                else:
 | 
			
		||||
                    return str(r), True
 | 
			
		||||
            else:
 | 
			
		||||
                asyncio.run(self.nats_cmd(data, timeout=20, wait=False))
 | 
			
		||||
 | 
			
		||||
            return "ok", False
 | 
			
		||||
 | 
			
		||||
        return "invalid", True
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def serialize(agent: "Agent") -> Dict[str, Any]:
 | 
			
		||||
        # serializes the agent and returns json
 | 
			
		||||
@@ -767,7 +919,7 @@ class Agent(BaseAuditModel):
 | 
			
		||||
        return AgentAuditSerializer(agent).data
 | 
			
		||||
 | 
			
		||||
    def delete_superseded_updates(self) -> None:
 | 
			
		||||
        try:
 | 
			
		||||
        with suppress(Exception):
 | 
			
		||||
            pks = []  # list of pks to delete
 | 
			
		||||
            kbs = list(self.winupdates.values_list("kb", flat=True))
 | 
			
		||||
            d = Counter(kbs)
 | 
			
		||||
@@ -778,8 +930,10 @@ class Agent(BaseAuditModel):
 | 
			
		||||
                # extract the version from the title and sort from oldest to newest
 | 
			
		||||
                # skip if no version info is available therefore nothing to parse
 | 
			
		||||
                try:
 | 
			
		||||
                    matches = r"(Version|Versão)"
 | 
			
		||||
                    pattern = r"\(" + matches + r"(.*?)\)"
 | 
			
		||||
                    vers = [
 | 
			
		||||
                        re.search(r"\(Version(.*?)\)", i).group(1).strip()
 | 
			
		||||
                        re.search(pattern, i, flags=re.IGNORECASE).group(2).strip()
 | 
			
		||||
                        for i in titles
 | 
			
		||||
                    ]
 | 
			
		||||
                    sorted_vers = sorted(vers, key=LooseVersion)
 | 
			
		||||
@@ -792,8 +946,6 @@ class Agent(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
            pks = list(set(pks))
 | 
			
		||||
            self.winupdates.filter(pk__in=pks).delete()
 | 
			
		||||
        except:
 | 
			
		||||
            pass
 | 
			
		||||
 | 
			
		||||
    def should_create_alert(
 | 
			
		||||
        self, alert_template: "Optional[AlertTemplate]" = None
 | 
			
		||||
@@ -903,60 +1055,54 @@ class AgentCustomField(models.Model):
 | 
			
		||||
        default=list,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        unique_together = (("agent", "field"),)
 | 
			
		||||
 | 
			
		||||
    def __str__(self) -> str:
 | 
			
		||||
        return self.field.name
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def value(self) -> Union[List[Any], bool, str]:
 | 
			
		||||
        if self.field.type == "multiple":
 | 
			
		||||
        if self.field.type == CustomFieldType.MULTIPLE:
 | 
			
		||||
            return cast(List[str], self.multiple_value)
 | 
			
		||||
        elif self.field.type == "checkbox":
 | 
			
		||||
        elif self.field.type == CustomFieldType.CHECKBOX:
 | 
			
		||||
            return self.bool_value
 | 
			
		||||
        else:
 | 
			
		||||
            return cast(str, self.string_value)
 | 
			
		||||
 | 
			
		||||
        return cast(str, self.string_value)
 | 
			
		||||
 | 
			
		||||
    def save_to_field(self, value: Union[List[Any], bool, str]) -> None:
 | 
			
		||||
        if self.field.type in [
 | 
			
		||||
            "text",
 | 
			
		||||
            "number",
 | 
			
		||||
            "single",
 | 
			
		||||
            "datetime",
 | 
			
		||||
        ]:
 | 
			
		||||
        if self.field.type in (
 | 
			
		||||
            CustomFieldType.TEXT,
 | 
			
		||||
            CustomFieldType.NUMBER,
 | 
			
		||||
            CustomFieldType.SINGLE,
 | 
			
		||||
            CustomFieldType.DATETIME,
 | 
			
		||||
        ):
 | 
			
		||||
            self.string_value = cast(str, value)
 | 
			
		||||
            self.save()
 | 
			
		||||
        elif self.field.type == "multiple":
 | 
			
		||||
        elif self.field.type == CustomFieldType.MULTIPLE:
 | 
			
		||||
            self.multiple_value = value.split(",")
 | 
			
		||||
            self.save()
 | 
			
		||||
        elif self.field.type == "checkbox":
 | 
			
		||||
        elif self.field.type == CustomFieldType.CHECKBOX:
 | 
			
		||||
            self.bool_value = bool(value)
 | 
			
		||||
            self.save()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
AGENT_HISTORY_TYPES = (
 | 
			
		||||
    ("task_run", "Task Run"),
 | 
			
		||||
    ("script_run", "Script Run"),
 | 
			
		||||
    ("cmd_run", "CMD Run"),
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
AGENT_HISTORY_STATUS = (("success", "Success"), ("failure", "Failure"))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AgentHistory(models.Model):
 | 
			
		||||
    objects = PermissionQuerySet.as_manager()
 | 
			
		||||
 | 
			
		||||
    id = models.BigAutoField(primary_key=True)
 | 
			
		||||
    agent = models.ForeignKey(
 | 
			
		||||
        Agent,
 | 
			
		||||
        related_name="history",
 | 
			
		||||
        on_delete=models.CASCADE,
 | 
			
		||||
    )
 | 
			
		||||
    time = models.DateTimeField(auto_now_add=True)
 | 
			
		||||
    type = models.CharField(
 | 
			
		||||
        max_length=50, choices=AGENT_HISTORY_TYPES, default="cmd_run"
 | 
			
		||||
    type: "AgentHistoryType" = models.CharField(
 | 
			
		||||
        max_length=50,
 | 
			
		||||
        choices=AgentHistoryType.choices,
 | 
			
		||||
        default=AgentHistoryType.CMD_RUN,
 | 
			
		||||
    )
 | 
			
		||||
    command = models.TextField(null=True, blank=True, default="")
 | 
			
		||||
    status = models.CharField(
 | 
			
		||||
        max_length=50, choices=AGENT_HISTORY_STATUS, default="success"
 | 
			
		||||
    )
 | 
			
		||||
    username = models.CharField(max_length=255, default="system")
 | 
			
		||||
    results = models.TextField(null=True, blank=True)
 | 
			
		||||
    script = models.ForeignKey(
 | 
			
		||||
 
 | 
			
		||||
@@ -27,6 +27,9 @@ class AgentPerms(permissions.BasePermission):
 | 
			
		||||
 | 
			
		||||
class RecoverAgentPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view) -> bool:
 | 
			
		||||
        if "agent_id" not in view.kwargs.keys():
 | 
			
		||||
            return _has_perm(r, "can_recover_agents")
 | 
			
		||||
 | 
			
		||||
        return _has_perm(r, "can_recover_agents") and _has_perm_on_agent(
 | 
			
		||||
            r.user, view.kwargs["agent_id"]
 | 
			
		||||
        )
 | 
			
		||||
@@ -44,13 +47,6 @@ class UpdateAgentPerms(permissions.BasePermission):
 | 
			
		||||
        return _has_perm(r, "can_update_agents")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PingAgentPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view) -> bool:
 | 
			
		||||
        return _has_perm(r, "can_ping_agents") and _has_perm_on_agent(
 | 
			
		||||
            r.user, view.kwargs["agent_id"]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ManageProcPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view) -> bool:
 | 
			
		||||
        return _has_perm(r, "can_manage_procs") and _has_perm_on_agent(
 | 
			
		||||
@@ -93,10 +89,8 @@ class RunScriptPerms(permissions.BasePermission):
 | 
			
		||||
 | 
			
		||||
class AgentNotesPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view) -> bool:
 | 
			
		||||
 | 
			
		||||
        # permissions for GET /agents/notes/ endpoint
 | 
			
		||||
        if r.method == "GET":
 | 
			
		||||
 | 
			
		||||
            # permissions for /agents/<agent_id>/notes endpoint
 | 
			
		||||
            if "agent_id" in view.kwargs.keys():
 | 
			
		||||
                return _has_perm(r, "can_list_notes") and _has_perm_on_agent(
 | 
			
		||||
@@ -119,5 +113,15 @@ class AgentHistoryPerms(permissions.BasePermission):
 | 
			
		||||
            return _has_perm(r, "can_list_agent_history") and _has_perm_on_agent(
 | 
			
		||||
                r.user, view.kwargs["agent_id"]
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            return _has_perm(r, "can_list_agent_history")
 | 
			
		||||
 | 
			
		||||
        return _has_perm(r, "can_list_agent_history")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AgentWOLPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view) -> bool:
 | 
			
		||||
        if "agent_id" in view.kwargs.keys():
 | 
			
		||||
            return _has_perm(r, "can_send_wol") and _has_perm_on_agent(
 | 
			
		||||
                r.user, view.kwargs["agent_id"]
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        return _has_perm(r, "can_send_wol")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import pytz
 | 
			
		||||
from rest_framework import serializers
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.constants import AGENT_STATUS_ONLINE, ALL_TIMEZONES
 | 
			
		||||
from winupdate.serializers import WinUpdatePolicySerializer
 | 
			
		||||
 | 
			
		||||
from .models import Agent, AgentCustomField, AgentHistory, Note
 | 
			
		||||
@@ -69,7 +70,7 @@ class AgentSerializer(serializers.ModelSerializer):
 | 
			
		||||
        return policies
 | 
			
		||||
 | 
			
		||||
    def get_all_timezones(self, obj):
 | 
			
		||||
        return pytz.all_timezones
 | 
			
		||||
        return ALL_TIMEZONES
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Agent
 | 
			
		||||
@@ -88,29 +89,35 @@ class AgentTableSerializer(serializers.ModelSerializer):
 | 
			
		||||
    last_seen = serializers.ReadOnlyField()
 | 
			
		||||
    pending_actions_count = serializers.ReadOnlyField()
 | 
			
		||||
    has_patches_pending = serializers.ReadOnlyField()
 | 
			
		||||
    cpu_model = serializers.ReadOnlyField()
 | 
			
		||||
    graphics = serializers.ReadOnlyField()
 | 
			
		||||
    local_ips = serializers.ReadOnlyField()
 | 
			
		||||
    make_model = serializers.ReadOnlyField()
 | 
			
		||||
    physical_disks = serializers.ReadOnlyField()
 | 
			
		||||
    serial_number = serializers.ReadOnlyField()
 | 
			
		||||
    custom_fields = AgentCustomFieldSerializer(many=True, read_only=True)
 | 
			
		||||
 | 
			
		||||
    def get_alert_template(self, obj):
 | 
			
		||||
 | 
			
		||||
        if not obj.alert_template:
 | 
			
		||||
            return None
 | 
			
		||||
        else:
 | 
			
		||||
            return {
 | 
			
		||||
                "name": obj.alert_template.name,
 | 
			
		||||
                "always_email": obj.alert_template.agent_always_email,
 | 
			
		||||
                "always_text": obj.alert_template.agent_always_text,
 | 
			
		||||
                "always_alert": obj.alert_template.agent_always_alert,
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            "name": obj.alert_template.name,
 | 
			
		||||
            "always_email": obj.alert_template.agent_always_email,
 | 
			
		||||
            "always_text": obj.alert_template.agent_always_text,
 | 
			
		||||
            "always_alert": obj.alert_template.agent_always_alert,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def get_logged_username(self, obj) -> str:
 | 
			
		||||
        if obj.logged_in_username == "None" and obj.status == "online":
 | 
			
		||||
        if obj.logged_in_username == "None" and obj.status == AGENT_STATUS_ONLINE:
 | 
			
		||||
            return obj.last_logged_in_user
 | 
			
		||||
        elif obj.logged_in_username != "None":
 | 
			
		||||
            return obj.logged_in_username
 | 
			
		||||
        else:
 | 
			
		||||
            return "-"
 | 
			
		||||
 | 
			
		||||
        return "-"
 | 
			
		||||
 | 
			
		||||
    def get_italic(self, obj) -> bool:
 | 
			
		||||
        return obj.logged_in_username == "None" and obj.status == "online"
 | 
			
		||||
        return obj.logged_in_username == "None" and obj.status == AGENT_STATUS_ONLINE
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Agent
 | 
			
		||||
@@ -139,16 +146,20 @@ class AgentTableSerializer(serializers.ModelSerializer):
 | 
			
		||||
            "plat",
 | 
			
		||||
            "goarch",
 | 
			
		||||
            "has_patches_pending",
 | 
			
		||||
            "version",
 | 
			
		||||
            "operating_system",
 | 
			
		||||
            "public_ip",
 | 
			
		||||
            "cpu_model",
 | 
			
		||||
            "graphics",
 | 
			
		||||
            "local_ips",
 | 
			
		||||
            "make_model",
 | 
			
		||||
            "physical_disks",
 | 
			
		||||
            "custom_fields",
 | 
			
		||||
            "serial_number",
 | 
			
		||||
        ]
 | 
			
		||||
        depth = 2
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class WinAgentSerializer(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Agent
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AgentHostnameSerializer(serializers.ModelSerializer):
 | 
			
		||||
    client = serializers.ReadOnlyField(source="client.name")
 | 
			
		||||
    site = serializers.ReadOnlyField(source="site.name")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,107 +1,41 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
import datetime as dt
 | 
			
		||||
import random
 | 
			
		||||
from time import sleep
 | 
			
		||||
from typing import Optional
 | 
			
		||||
from typing import TYPE_CHECKING, Optional
 | 
			
		||||
 | 
			
		||||
from django.core.management import call_command
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from agents.utils import get_agent_url
 | 
			
		||||
from core.utils import get_core_settings
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from logs.models import DebugLog, PendingAction
 | 
			
		||||
from packaging import version as pyver
 | 
			
		||||
from logs.models import DebugLog
 | 
			
		||||
from scripts.models import Script
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.celery import app
 | 
			
		||||
from tacticalrmm.constants import (
 | 
			
		||||
    AGENT_DEFER,
 | 
			
		||||
    AGENT_OUTAGES_LOCK,
 | 
			
		||||
    AGENT_STATUS_OVERDUE,
 | 
			
		||||
    CheckStatus,
 | 
			
		||||
    DebugLogType,
 | 
			
		||||
)
 | 
			
		||||
from tacticalrmm.helpers import rand_range
 | 
			
		||||
from tacticalrmm.utils import redis_lock
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def agent_update(agent_id: str, force: bool = False) -> str:
 | 
			
		||||
 | 
			
		||||
    agent = Agent.objects.get(agent_id=agent_id)
 | 
			
		||||
 | 
			
		||||
    if pyver.parse(agent.version) <= pyver.parse("1.3.0"):
 | 
			
		||||
        return "not supported"
 | 
			
		||||
 | 
			
		||||
    # skip if we can't determine the arch
 | 
			
		||||
    if agent.arch is None:
 | 
			
		||||
        DebugLog.warning(
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            log_type="agent_issues",
 | 
			
		||||
            message=f"Unable to determine arch on {agent.hostname}({agent.agent_id}). Skipping agent update.",
 | 
			
		||||
        )
 | 
			
		||||
        return "noarch"
 | 
			
		||||
 | 
			
		||||
    version = settings.LATEST_AGENT_VER
 | 
			
		||||
    inno = agent.win_inno_exe
 | 
			
		||||
    url = get_agent_url(agent.arch, agent.plat)
 | 
			
		||||
 | 
			
		||||
    if not force:
 | 
			
		||||
        if agent.pendingactions.filter(
 | 
			
		||||
            action_type="agentupdate", status="pending"
 | 
			
		||||
        ).exists():
 | 
			
		||||
            agent.pendingactions.filter(
 | 
			
		||||
                action_type="agentupdate", status="pending"
 | 
			
		||||
            ).delete()
 | 
			
		||||
 | 
			
		||||
        PendingAction.objects.create(
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            action_type="agentupdate",
 | 
			
		||||
            details={
 | 
			
		||||
                "url": url,
 | 
			
		||||
                "version": version,
 | 
			
		||||
                "inno": inno,
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    nats_data = {
 | 
			
		||||
        "func": "agentupdate",
 | 
			
		||||
        "payload": {
 | 
			
		||||
            "url": url,
 | 
			
		||||
            "version": version,
 | 
			
		||||
            "inno": inno,
 | 
			
		||||
        },
 | 
			
		||||
    }
 | 
			
		||||
    asyncio.run(agent.nats_cmd(nats_data, wait=False))
 | 
			
		||||
    return "created"
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
    from django.db.models.query import QuerySet
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def force_code_sign(agent_ids: list[str]) -> None:
 | 
			
		||||
    chunks = (agent_ids[i : i + 70] for i in range(0, len(agent_ids), 70))
 | 
			
		||||
    for chunk in chunks:
 | 
			
		||||
        for agent_id in chunk:
 | 
			
		||||
            agent_update(agent_id=agent_id, force=True)
 | 
			
		||||
        sleep(2)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def send_agent_update_task(agent_ids: list[str]) -> None:
 | 
			
		||||
    chunks = (agent_ids[i : i + 70] for i in range(0, len(agent_ids), 70))
 | 
			
		||||
    for chunk in chunks:
 | 
			
		||||
        for agent_id in chunk:
 | 
			
		||||
            agent_update(agent_id)
 | 
			
		||||
        sleep(2)
 | 
			
		||||
def send_agent_update_task(*, agent_ids: list[str], token: str, force: bool) -> None:
 | 
			
		||||
    agents: "QuerySet[Agent]" = Agent.objects.defer(*AGENT_DEFER).filter(
 | 
			
		||||
        agent_id__in=agent_ids
 | 
			
		||||
    )
 | 
			
		||||
    for agent in agents:
 | 
			
		||||
        agent.do_update(token=token, force=force)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def auto_self_agent_update_task() -> None:
 | 
			
		||||
    core = get_core_settings()
 | 
			
		||||
    if not core.agent_auto_update:
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    q = Agent.objects.only("agent_id", "version")
 | 
			
		||||
    agent_ids: list[str] = [
 | 
			
		||||
        i.agent_id
 | 
			
		||||
        for i in q
 | 
			
		||||
        if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    chunks = (agent_ids[i : i + 70] for i in range(0, len(agent_ids), 70))
 | 
			
		||||
    for chunk in chunks:
 | 
			
		||||
        for agent_id in chunk:
 | 
			
		||||
            agent_update(agent_id)
 | 
			
		||||
        sleep(2)
 | 
			
		||||
    call_command("update_agents")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
@@ -114,7 +48,7 @@ def agent_outage_email_task(pk: int, alert_interval: Optional[float] = None) ->
 | 
			
		||||
        return "alert not found"
 | 
			
		||||
 | 
			
		||||
    if not alert.email_sent:
 | 
			
		||||
        sleep(random.randint(1, 5))
 | 
			
		||||
        sleep(rand_range(100, 1500))
 | 
			
		||||
        alert.agent.send_outage_email()
 | 
			
		||||
        alert.email_sent = djangotime.now()
 | 
			
		||||
        alert.save(update_fields=["email_sent"])
 | 
			
		||||
@@ -123,7 +57,7 @@ def agent_outage_email_task(pk: int, alert_interval: Optional[float] = None) ->
 | 
			
		||||
            # send an email only if the last email sent is older than alert interval
 | 
			
		||||
            delta = djangotime.now() - dt.timedelta(days=alert_interval)
 | 
			
		||||
            if alert.email_sent < delta:
 | 
			
		||||
                sleep(random.randint(1, 5))
 | 
			
		||||
                sleep(rand_range(100, 1500))
 | 
			
		||||
                alert.agent.send_outage_email()
 | 
			
		||||
                alert.email_sent = djangotime.now()
 | 
			
		||||
                alert.save(update_fields=["email_sent"])
 | 
			
		||||
@@ -135,7 +69,7 @@ def agent_outage_email_task(pk: int, alert_interval: Optional[float] = None) ->
 | 
			
		||||
def agent_recovery_email_task(pk: int) -> str:
 | 
			
		||||
    from alerts.models import Alert
 | 
			
		||||
 | 
			
		||||
    sleep(random.randint(1, 5))
 | 
			
		||||
    sleep(rand_range(100, 1500))
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        alert = Alert.objects.get(pk=pk)
 | 
			
		||||
@@ -159,7 +93,7 @@ def agent_outage_sms_task(pk: int, alert_interval: Optional[float] = None) -> st
 | 
			
		||||
        return "alert not found"
 | 
			
		||||
 | 
			
		||||
    if not alert.sms_sent:
 | 
			
		||||
        sleep(random.randint(1, 3))
 | 
			
		||||
        sleep(rand_range(100, 1500))
 | 
			
		||||
        alert.agent.send_outage_sms()
 | 
			
		||||
        alert.sms_sent = djangotime.now()
 | 
			
		||||
        alert.save(update_fields=["sms_sent"])
 | 
			
		||||
@@ -168,7 +102,7 @@ def agent_outage_sms_task(pk: int, alert_interval: Optional[float] = None) -> st
 | 
			
		||||
            # send an sms only if the last sms sent is older than alert interval
 | 
			
		||||
            delta = djangotime.now() - dt.timedelta(days=alert_interval)
 | 
			
		||||
            if alert.sms_sent < delta:
 | 
			
		||||
                sleep(random.randint(1, 3))
 | 
			
		||||
                sleep(rand_range(100, 1500))
 | 
			
		||||
                alert.agent.send_outage_sms()
 | 
			
		||||
                alert.sms_sent = djangotime.now()
 | 
			
		||||
                alert.save(update_fields=["sms_sent"])
 | 
			
		||||
@@ -180,7 +114,7 @@ def agent_outage_sms_task(pk: int, alert_interval: Optional[float] = None) -> st
 | 
			
		||||
def agent_recovery_sms_task(pk: int) -> str:
 | 
			
		||||
    from alerts.models import Alert
 | 
			
		||||
 | 
			
		||||
    sleep(random.randint(1, 3))
 | 
			
		||||
    sleep(rand_range(100, 1500))
 | 
			
		||||
    try:
 | 
			
		||||
        alert = Alert.objects.get(pk=pk)
 | 
			
		||||
    except Alert.DoesNotExist:
 | 
			
		||||
@@ -193,24 +127,20 @@ def agent_recovery_sms_task(pk: int) -> str:
 | 
			
		||||
    return "ok"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def agent_outages_task() -> None:
 | 
			
		||||
    from alerts.models import Alert
 | 
			
		||||
@app.task(bind=True)
 | 
			
		||||
def agent_outages_task(self) -> str:
 | 
			
		||||
    with redis_lock(AGENT_OUTAGES_LOCK, self.app.oid) as acquired:
 | 
			
		||||
        if not acquired:
 | 
			
		||||
            return f"{self.app.oid} still running"
 | 
			
		||||
 | 
			
		||||
    agents = Agent.objects.only(
 | 
			
		||||
        "pk",
 | 
			
		||||
        "agent_id",
 | 
			
		||||
        "last_seen",
 | 
			
		||||
        "offline_time",
 | 
			
		||||
        "overdue_time",
 | 
			
		||||
        "overdue_email_alert",
 | 
			
		||||
        "overdue_text_alert",
 | 
			
		||||
        "overdue_dashboard_alert",
 | 
			
		||||
    )
 | 
			
		||||
        from alerts.models import Alert
 | 
			
		||||
        from core.tasks import _get_agent_qs
 | 
			
		||||
 | 
			
		||||
    for agent in agents:
 | 
			
		||||
        if agent.status == "overdue":
 | 
			
		||||
            Alert.handle_alert_failure(agent)
 | 
			
		||||
        for agent in _get_agent_qs():
 | 
			
		||||
            if agent.status == AGENT_STATUS_OVERDUE:
 | 
			
		||||
                Alert.handle_alert_failure(agent)
 | 
			
		||||
 | 
			
		||||
        return "completed"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
@@ -221,6 +151,8 @@ def run_script_email_results_task(
 | 
			
		||||
    emails: list[str],
 | 
			
		||||
    args: list[str] = [],
 | 
			
		||||
    history_pk: int = 0,
 | 
			
		||||
    run_as_user: bool = False,
 | 
			
		||||
    env_vars: list[str] = [],
 | 
			
		||||
):
 | 
			
		||||
    agent = Agent.objects.get(pk=agentpk)
 | 
			
		||||
    script = Script.objects.get(pk=scriptpk)
 | 
			
		||||
@@ -231,11 +163,13 @@ def run_script_email_results_task(
 | 
			
		||||
        timeout=nats_timeout,
 | 
			
		||||
        wait=True,
 | 
			
		||||
        history_pk=history_pk,
 | 
			
		||||
        run_as_user=run_as_user,
 | 
			
		||||
        env_vars=env_vars,
 | 
			
		||||
    )
 | 
			
		||||
    if r == "timeout":
 | 
			
		||||
        DebugLog.error(
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            log_type="scripting",
 | 
			
		||||
            log_type=DebugLogType.SCRIPTING,
 | 
			
		||||
            message=f"{agent.hostname}({agent.pk}) timed out running script.",
 | 
			
		||||
        )
 | 
			
		||||
        return
 | 
			
		||||
@@ -289,7 +223,7 @@ def clear_faults_task(older_than_days: int) -> None:
 | 
			
		||||
        for check in agent.get_checks_with_policies():
 | 
			
		||||
            # reset check status
 | 
			
		||||
            if check.check_result:
 | 
			
		||||
                check.check_result.status = "passing"
 | 
			
		||||
                check.check_result.status = CheckStatus.PASSING
 | 
			
		||||
                check.check_result.save(update_fields=["status"])
 | 
			
		||||
            if check.alert.filter(agent=agent, resolved=False).exists():
 | 
			
		||||
                alert = Alert.create_or_return_check_alert(check, agent=agent)
 | 
			
		||||
@@ -318,3 +252,8 @@ def prune_agent_history(older_than_days: int) -> str:
 | 
			
		||||
    ).delete()
 | 
			
		||||
 | 
			
		||||
    return "ok"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def bulk_recover_agents_task() -> None:
 | 
			
		||||
    call_command("bulk_restart_agents")
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										0
									
								
								api/tacticalrmm/agents/tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								api/tacticalrmm/agents/tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										106
									
								
								api/tacticalrmm/agents/tests/test_agent_installs.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								api/tacticalrmm/agents/tests/test_agent_installs.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,106 @@
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestAgentInstalls(TacticalTestCase):
 | 
			
		||||
    def setUp(self) -> None:
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        self.setup_coresettings()
 | 
			
		||||
        self.setup_base_instance()
 | 
			
		||||
 | 
			
		||||
    @patch("agents.utils.generate_linux_install")
 | 
			
		||||
    @patch("knox.models.AuthToken.objects.create")
 | 
			
		||||
    @patch("tacticalrmm.utils.generate_winagent_exe")
 | 
			
		||||
    @patch("core.utils.token_is_valid")
 | 
			
		||||
    @patch("agents.utils.get_agent_url")
 | 
			
		||||
    def test_install_agent(
 | 
			
		||||
        self,
 | 
			
		||||
        mock_agent_url,
 | 
			
		||||
        mock_token_valid,
 | 
			
		||||
        mock_gen_win_exe,
 | 
			
		||||
        mock_auth,
 | 
			
		||||
        mock_linux_install,
 | 
			
		||||
    ):
 | 
			
		||||
        mock_agent_url.return_value = "https://example.com"
 | 
			
		||||
        mock_token_valid.return_value = "", False
 | 
			
		||||
        mock_gen_win_exe.return_value = Response("ok")
 | 
			
		||||
        mock_auth.return_value = "", "token"
 | 
			
		||||
        mock_linux_install.return_value = Response("ok")
 | 
			
		||||
 | 
			
		||||
        url = "/agents/installer/"
 | 
			
		||||
 | 
			
		||||
        # test windows dynamic exe
 | 
			
		||||
        data = {
 | 
			
		||||
            "installMethod": "exe",
 | 
			
		||||
            "client": self.site2.client.pk,
 | 
			
		||||
            "site": self.site2.pk,
 | 
			
		||||
            "expires": 24,
 | 
			
		||||
            "agenttype": "server",
 | 
			
		||||
            "power": 0,
 | 
			
		||||
            "rdp": 1,
 | 
			
		||||
            "ping": 0,
 | 
			
		||||
            "goarch": "amd64",
 | 
			
		||||
            "api": "https://api.example.com",
 | 
			
		||||
            "fileName": "rmm-client-site-server.exe",
 | 
			
		||||
            "plat": "windows",
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        mock_gen_win_exe.assert_called_with(
 | 
			
		||||
            client=self.site2.client.pk,
 | 
			
		||||
            site=self.site2.pk,
 | 
			
		||||
            agent_type="server",
 | 
			
		||||
            rdp=1,
 | 
			
		||||
            ping=0,
 | 
			
		||||
            power=0,
 | 
			
		||||
            goarch="amd64",
 | 
			
		||||
            token="token",
 | 
			
		||||
            api="https://api.example.com",
 | 
			
		||||
            file_name="rmm-client-site-server.exe",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # test linux no code sign
 | 
			
		||||
        data["plat"] = "linux"
 | 
			
		||||
        data["installMethod"] = "bash"
 | 
			
		||||
        data["rdp"] = 0
 | 
			
		||||
        data["agenttype"] = "workstation"
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
 | 
			
		||||
        # test linux
 | 
			
		||||
        mock_token_valid.return_value = "token123", True
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        mock_linux_install.assert_called_with(
 | 
			
		||||
            client=str(self.site2.client.pk),
 | 
			
		||||
            site=str(self.site2.pk),
 | 
			
		||||
            agent_type="workstation",
 | 
			
		||||
            arch="amd64",
 | 
			
		||||
            token="token",
 | 
			
		||||
            api="https://api.example.com",
 | 
			
		||||
            download_url="https://example.com",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # test manual
 | 
			
		||||
        data["rdp"] = 1
 | 
			
		||||
        data["installMethod"] = "manual"
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertIn("rdp", r.json()["cmd"])
 | 
			
		||||
        self.assertNotIn("power", r.json()["cmd"])
 | 
			
		||||
 | 
			
		||||
        data.update({"ping": 1, "power": 1})
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertIn("power", r.json()["cmd"])
 | 
			
		||||
        self.assertIn("ping", r.json()["cmd"])
 | 
			
		||||
 | 
			
		||||
        # test powershell
 | 
			
		||||
        data["installMethod"] = "powershell"
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
							
								
								
									
										61
									
								
								api/tacticalrmm/agents/tests/test_agent_save.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								api/tacticalrmm/agents/tests/test_agent_save.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,61 @@
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
 | 
			
		||||
from model_bakery import baker
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from tacticalrmm.constants import AgentMonType
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AgentSaveTestCase(TacticalTestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.client1 = baker.make("clients.Client")
 | 
			
		||||
        self.client2 = baker.make("clients.Client")
 | 
			
		||||
        self.site1 = baker.make("clients.Site", client=self.client1)
 | 
			
		||||
        self.site2 = baker.make("clients.Site", client=self.client2)
 | 
			
		||||
        self.site3 = baker.make("clients.Site", client=self.client2)
 | 
			
		||||
        self.agent = baker.make(
 | 
			
		||||
            "agents.Agent",
 | 
			
		||||
            site=self.site1,
 | 
			
		||||
            monitoring_type=AgentMonType.SERVER,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @patch.object(Agent, "set_alert_template")
 | 
			
		||||
    def test_set_alert_template_called_on_mon_type_change(
 | 
			
		||||
        self, mock_set_alert_template
 | 
			
		||||
    ):
 | 
			
		||||
        self.agent.monitoring_type = AgentMonType.WORKSTATION
 | 
			
		||||
        self.agent.save()
 | 
			
		||||
        mock_set_alert_template.assert_called_once()
 | 
			
		||||
 | 
			
		||||
    @patch.object(Agent, "set_alert_template")
 | 
			
		||||
    def test_set_alert_template_called_on_site_change(self, mock_set_alert_template):
 | 
			
		||||
        self.agent.site = self.site2
 | 
			
		||||
        self.agent.save()
 | 
			
		||||
        mock_set_alert_template.assert_called_once()
 | 
			
		||||
 | 
			
		||||
    @patch.object(Agent, "set_alert_template")
 | 
			
		||||
    def test_set_alert_template_called_on_site_and_montype_change(
 | 
			
		||||
        self, mock_set_alert_template
 | 
			
		||||
    ):
 | 
			
		||||
        print(f"before: {self.agent.monitoring_type} site: {self.agent.site_id}")
 | 
			
		||||
        self.agent.site = self.site3
 | 
			
		||||
        self.agent.monitoring_type = AgentMonType.WORKSTATION
 | 
			
		||||
        self.agent.save()
 | 
			
		||||
        mock_set_alert_template.assert_called_once()
 | 
			
		||||
        print(f"after: {self.agent.monitoring_type} site: {self.agent.site_id}")
 | 
			
		||||
 | 
			
		||||
    @patch.object(Agent, "set_alert_template")
 | 
			
		||||
    def test_set_alert_template_not_called_without_changes(
 | 
			
		||||
        self, mock_set_alert_template
 | 
			
		||||
    ):
 | 
			
		||||
        self.agent.save()
 | 
			
		||||
        mock_set_alert_template.assert_not_called()
 | 
			
		||||
 | 
			
		||||
    @patch.object(Agent, "set_alert_template")
 | 
			
		||||
    def test_set_alert_template_not_called_on_non_relevant_field_change(
 | 
			
		||||
        self, mock_set_alert_template
 | 
			
		||||
    ):
 | 
			
		||||
        self.agent.hostname = "abc123"
 | 
			
		||||
        self.agent.save()
 | 
			
		||||
        mock_set_alert_template.assert_not_called()
 | 
			
		||||
							
								
								
									
										313
									
								
								api/tacticalrmm/agents/tests/test_agent_update.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										313
									
								
								api/tacticalrmm/agents/tests/test_agent_update.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,313 @@
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.core.management import call_command
 | 
			
		||||
from model_bakery import baker
 | 
			
		||||
from packaging import version as pyver
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from agents.tasks import auto_self_agent_update_task, send_agent_update_task
 | 
			
		||||
from logs.models import PendingAction
 | 
			
		||||
from tacticalrmm.constants import (
 | 
			
		||||
    AGENT_DEFER,
 | 
			
		||||
    AgentMonType,
 | 
			
		||||
    AgentPlat,
 | 
			
		||||
    GoArch,
 | 
			
		||||
    PAAction,
 | 
			
		||||
    PAStatus,
 | 
			
		||||
)
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestAgentUpdate(TacticalTestCase):
 | 
			
		||||
    def setUp(self) -> None:
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        self.setup_coresettings()
 | 
			
		||||
        self.setup_base_instance()
 | 
			
		||||
 | 
			
		||||
    @patch("agents.management.commands.update_agents.send_agent_update_task.delay")
 | 
			
		||||
    @patch("agents.management.commands.update_agents.token_is_valid")
 | 
			
		||||
    @patch("agents.management.commands.update_agents.get_core_settings")
 | 
			
		||||
    def test_update_agents_mgmt_command(self, mock_core, mock_token, mock_update):
 | 
			
		||||
        mock_token.return_value = ("token123", True)
 | 
			
		||||
 | 
			
		||||
        baker.make_recipe(
 | 
			
		||||
            "agents.online_agent",
 | 
			
		||||
            site=self.site1,
 | 
			
		||||
            monitoring_type=AgentMonType.SERVER,
 | 
			
		||||
            plat=AgentPlat.WINDOWS,
 | 
			
		||||
            version="2.0.3",
 | 
			
		||||
            _quantity=6,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        baker.make_recipe(
 | 
			
		||||
            "agents.online_agent",
 | 
			
		||||
            site=self.site3,
 | 
			
		||||
            monitoring_type=AgentMonType.WORKSTATION,
 | 
			
		||||
            plat=AgentPlat.LINUX,
 | 
			
		||||
            version="2.0.3",
 | 
			
		||||
            _quantity=5,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        baker.make_recipe(
 | 
			
		||||
            "agents.online_agent",
 | 
			
		||||
            site=self.site2,
 | 
			
		||||
            monitoring_type=AgentMonType.SERVER,
 | 
			
		||||
            plat=AgentPlat.WINDOWS,
 | 
			
		||||
            version=settings.LATEST_AGENT_VER,
 | 
			
		||||
            _quantity=8,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        mock_core.return_value.agent_auto_update = False
 | 
			
		||||
        call_command("update_agents")
 | 
			
		||||
        mock_update.assert_not_called()
 | 
			
		||||
 | 
			
		||||
        mock_core.return_value.agent_auto_update = True
 | 
			
		||||
        call_command("update_agents")
 | 
			
		||||
 | 
			
		||||
        ids = list(
 | 
			
		||||
            Agent.objects.defer(*AGENT_DEFER)
 | 
			
		||||
            .exclude(version=settings.LATEST_AGENT_VER)
 | 
			
		||||
            .values_list("agent_id", flat=True)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        mock_update.assert_called_with(agent_ids=ids, token="token123", force=False)
 | 
			
		||||
 | 
			
		||||
    @patch("agents.models.Agent.nats_cmd")
 | 
			
		||||
    @patch("agents.models.get_agent_url")
 | 
			
		||||
    def test_do_update(self, mock_agent_url, mock_nats_cmd):
 | 
			
		||||
        mock_agent_url.return_value = "https://example.com/123"
 | 
			
		||||
 | 
			
		||||
        # test noarch
 | 
			
		||||
        agent_noarch = baker.make_recipe(
 | 
			
		||||
            "agents.online_agent",
 | 
			
		||||
            site=self.site1,
 | 
			
		||||
            monitoring_type=AgentMonType.SERVER,
 | 
			
		||||
            plat=AgentPlat.WINDOWS,
 | 
			
		||||
            version="2.1.1",
 | 
			
		||||
        )
 | 
			
		||||
        r = agent_noarch.do_update(token="", force=True)
 | 
			
		||||
        self.assertEqual(r, "noarch")
 | 
			
		||||
 | 
			
		||||
        # test too old
 | 
			
		||||
        agent_old = baker.make_recipe(
 | 
			
		||||
            "agents.online_agent",
 | 
			
		||||
            site=self.site2,
 | 
			
		||||
            monitoring_type=AgentMonType.SERVER,
 | 
			
		||||
            plat=AgentPlat.WINDOWS,
 | 
			
		||||
            version="1.3.0",
 | 
			
		||||
            goarch=GoArch.AMD64,
 | 
			
		||||
        )
 | 
			
		||||
        r = agent_old.do_update(token="", force=True)
 | 
			
		||||
        self.assertEqual(r, "not supported")
 | 
			
		||||
 | 
			
		||||
        win = baker.make_recipe(
 | 
			
		||||
            "agents.online_agent",
 | 
			
		||||
            site=self.site1,
 | 
			
		||||
            monitoring_type=AgentMonType.SERVER,
 | 
			
		||||
            plat=AgentPlat.WINDOWS,
 | 
			
		||||
            version="2.1.1",
 | 
			
		||||
            goarch=GoArch.AMD64,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        lin = baker.make_recipe(
 | 
			
		||||
            "agents.online_agent",
 | 
			
		||||
            site=self.site3,
 | 
			
		||||
            monitoring_type=AgentMonType.WORKSTATION,
 | 
			
		||||
            plat=AgentPlat.LINUX,
 | 
			
		||||
            version="2.1.1",
 | 
			
		||||
            goarch=GoArch.ARM32,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # test windows agent update
 | 
			
		||||
        r = win.do_update(token="", force=False)
 | 
			
		||||
        self.assertEqual(r, "created")
 | 
			
		||||
        mock_nats_cmd.assert_called_with(
 | 
			
		||||
            {
 | 
			
		||||
                "func": "agentupdate",
 | 
			
		||||
                "payload": {
 | 
			
		||||
                    "url": "https://example.com/123",
 | 
			
		||||
                    "version": settings.LATEST_AGENT_VER,
 | 
			
		||||
                    "inno": f"tacticalagent-v{settings.LATEST_AGENT_VER}-windows-amd64.exe",
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            wait=False,
 | 
			
		||||
        )
 | 
			
		||||
        action1 = PendingAction.objects.get(agent__agent_id=win.agent_id)
 | 
			
		||||
        self.assertEqual(action1.action_type, PAAction.AGENT_UPDATE)
 | 
			
		||||
        self.assertEqual(action1.status, PAStatus.PENDING)
 | 
			
		||||
        self.assertEqual(action1.details["url"], "https://example.com/123")
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            action1.details["inno"],
 | 
			
		||||
            f"tacticalagent-v{settings.LATEST_AGENT_VER}-windows-amd64.exe",
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(action1.details["version"], settings.LATEST_AGENT_VER)
 | 
			
		||||
 | 
			
		||||
        mock_nats_cmd.reset_mock()
 | 
			
		||||
 | 
			
		||||
        # test linux agent update
 | 
			
		||||
        r = lin.do_update(token="", force=False)
 | 
			
		||||
        mock_nats_cmd.assert_called_with(
 | 
			
		||||
            {
 | 
			
		||||
                "func": "agentupdate",
 | 
			
		||||
                "payload": {
 | 
			
		||||
                    "url": "https://example.com/123",
 | 
			
		||||
                    "version": settings.LATEST_AGENT_VER,
 | 
			
		||||
                    "inno": f"tacticalagent-v{settings.LATEST_AGENT_VER}-linux-arm.exe",
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            wait=False,
 | 
			
		||||
        )
 | 
			
		||||
        action2 = PendingAction.objects.get(agent__agent_id=lin.agent_id)
 | 
			
		||||
        self.assertEqual(action2.action_type, PAAction.AGENT_UPDATE)
 | 
			
		||||
        self.assertEqual(action2.status, PAStatus.PENDING)
 | 
			
		||||
        self.assertEqual(action2.details["url"], "https://example.com/123")
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            action2.details["inno"],
 | 
			
		||||
            f"tacticalagent-v{settings.LATEST_AGENT_VER}-linux-arm.exe",
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(action2.details["version"], settings.LATEST_AGENT_VER)
 | 
			
		||||
 | 
			
		||||
        # check if old agent update pending actions are being deleted
 | 
			
		||||
        # should only be 1 pending action at all times
 | 
			
		||||
        pa_count = win.pendingactions.filter(
 | 
			
		||||
            action_type=PAAction.AGENT_UPDATE, status=PAStatus.PENDING
 | 
			
		||||
        ).count()
 | 
			
		||||
        self.assertEqual(pa_count, 1)
 | 
			
		||||
 | 
			
		||||
        for _ in range(4):
 | 
			
		||||
            win.do_update(token="", force=False)
 | 
			
		||||
 | 
			
		||||
        pa_count = win.pendingactions.filter(
 | 
			
		||||
            action_type=PAAction.AGENT_UPDATE, status=PAStatus.PENDING
 | 
			
		||||
        ).count()
 | 
			
		||||
        self.assertEqual(pa_count, 1)
 | 
			
		||||
 | 
			
		||||
    def test_auto_self_agent_update_task(self):
 | 
			
		||||
        auto_self_agent_update_task()
 | 
			
		||||
 | 
			
		||||
    @patch("agents.models.Agent.do_update")
 | 
			
		||||
    def test_send_agent_update_task(self, mock_update):
 | 
			
		||||
        baker.make_recipe(
 | 
			
		||||
            "agents.online_agent",
 | 
			
		||||
            site=self.site2,
 | 
			
		||||
            monitoring_type=AgentMonType.SERVER,
 | 
			
		||||
            plat=AgentPlat.WINDOWS,
 | 
			
		||||
            version="2.1.1",
 | 
			
		||||
            goarch=GoArch.AMD64,
 | 
			
		||||
            _quantity=6,
 | 
			
		||||
        )
 | 
			
		||||
        ids = list(
 | 
			
		||||
            Agent.objects.defer(*AGENT_DEFER)
 | 
			
		||||
            .exclude(version=settings.LATEST_AGENT_VER)
 | 
			
		||||
            .values_list("agent_id", flat=True)
 | 
			
		||||
        )
 | 
			
		||||
        send_agent_update_task(agent_ids=ids, token="", force=False)
 | 
			
		||||
        self.assertEqual(mock_update.call_count, 6)
 | 
			
		||||
 | 
			
		||||
    @patch("agents.views.token_is_valid")
 | 
			
		||||
    @patch("agents.tasks.send_agent_update_task.delay")
 | 
			
		||||
    def test_update_agents(self, mock_update, mock_token):
 | 
			
		||||
        mock_token.return_value = ("", False)
 | 
			
		||||
        url = "/agents/update/"
 | 
			
		||||
        baker.make_recipe(
 | 
			
		||||
            "agents.online_agent",
 | 
			
		||||
            site=self.site2,
 | 
			
		||||
            monitoring_type=AgentMonType.SERVER,
 | 
			
		||||
            plat=AgentPlat.WINDOWS,
 | 
			
		||||
            version="2.1.1",
 | 
			
		||||
            goarch=GoArch.AMD64,
 | 
			
		||||
            _quantity=7,
 | 
			
		||||
        )
 | 
			
		||||
        baker.make_recipe(
 | 
			
		||||
            "agents.online_agent",
 | 
			
		||||
            site=self.site2,
 | 
			
		||||
            monitoring_type=AgentMonType.SERVER,
 | 
			
		||||
            plat=AgentPlat.WINDOWS,
 | 
			
		||||
            version=settings.LATEST_AGENT_VER,
 | 
			
		||||
            goarch=GoArch.AMD64,
 | 
			
		||||
            _quantity=3,
 | 
			
		||||
        )
 | 
			
		||||
        baker.make_recipe(
 | 
			
		||||
            "agents.online_agent",
 | 
			
		||||
            site=self.site2,
 | 
			
		||||
            monitoring_type=AgentMonType.WORKSTATION,
 | 
			
		||||
            plat=AgentPlat.LINUX,
 | 
			
		||||
            version="2.0.1",
 | 
			
		||||
            goarch=GoArch.ARM32,
 | 
			
		||||
            _quantity=9,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        agent_ids: list[str] = list(
 | 
			
		||||
            Agent.objects.only("agent_id").values_list("agent_id", flat=True)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        data = {"agent_ids": agent_ids}
 | 
			
		||||
        expected: list[str] = [
 | 
			
		||||
            i.agent_id
 | 
			
		||||
            for i in Agent.objects.only("agent_id", "version")
 | 
			
		||||
            if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        mock_update.assert_called_with(agent_ids=expected, token="", force=False)
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
    @patch("agents.views.token_is_valid")
 | 
			
		||||
    @patch("agents.tasks.send_agent_update_task.delay")
 | 
			
		||||
    def test_agent_update_permissions(self, update_task, mock_token):
 | 
			
		||||
        mock_token.return_value = ("", False)
 | 
			
		||||
 | 
			
		||||
        agents = baker.make_recipe("agents.agent", _quantity=5)
 | 
			
		||||
        other_agents = baker.make_recipe("agents.agent", _quantity=7)
 | 
			
		||||
 | 
			
		||||
        url = "/agents/update/"
 | 
			
		||||
 | 
			
		||||
        data = {
 | 
			
		||||
            "agent_ids": [agent.agent_id for agent in agents]
 | 
			
		||||
            + [agent.agent_id for agent in other_agents]
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        # test superuser access
 | 
			
		||||
        self.check_authorized_superuser("post", url, data)
 | 
			
		||||
        update_task.assert_called_with(
 | 
			
		||||
            agent_ids=data["agent_ids"], token="", force=False
 | 
			
		||||
        )
 | 
			
		||||
        update_task.reset_mock()
 | 
			
		||||
 | 
			
		||||
        user = self.create_user_with_roles([])
 | 
			
		||||
        self.client.force_authenticate(user=user)
 | 
			
		||||
 | 
			
		||||
        self.check_not_authorized("post", url, data)
 | 
			
		||||
        update_task.assert_not_called()
 | 
			
		||||
 | 
			
		||||
        user.role.can_update_agents = True
 | 
			
		||||
        user.role.save()
 | 
			
		||||
 | 
			
		||||
        self.check_authorized("post", url, data)
 | 
			
		||||
        update_task.assert_called_with(
 | 
			
		||||
            agent_ids=data["agent_ids"], token="", force=False
 | 
			
		||||
        )
 | 
			
		||||
        update_task.reset_mock()
 | 
			
		||||
 | 
			
		||||
        # limit to client
 | 
			
		||||
        # user.role.can_view_clients.set([agents[0].client])
 | 
			
		||||
        # self.check_authorized("post", url, data)
 | 
			
		||||
        # update_task.assert_called_with(agent_ids=[agent.agent_id for agent in agents])
 | 
			
		||||
        # update_task.reset_mock()
 | 
			
		||||
 | 
			
		||||
        # add site
 | 
			
		||||
        # user.role.can_view_sites.set([other_agents[0].site])
 | 
			
		||||
        # self.check_authorized("post", url, data)
 | 
			
		||||
        # update_task.assert_called_with(agent_ids=data["agent_ids"])
 | 
			
		||||
        # update_task.reset_mock()
 | 
			
		||||
 | 
			
		||||
        # remove client permissions
 | 
			
		||||
        # user.role.can_view_clients.clear()
 | 
			
		||||
        # self.check_authorized("post", url, data)
 | 
			
		||||
        # update_task.assert_called_with(
 | 
			
		||||
        #     agent_ids=[agent.agent_id for agent in other_agents]
 | 
			
		||||
        # )
 | 
			
		||||
							
								
								
									
										59
									
								
								api/tacticalrmm/agents/tests/test_agent_utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								api/tacticalrmm/agents/tests/test_agent_utils.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,59 @@
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
 | 
			
		||||
from agents.utils import generate_linux_install, get_agent_url
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestAgentUtils(TacticalTestCase):
 | 
			
		||||
    def setUp(self) -> None:
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        self.setup_coresettings()
 | 
			
		||||
        self.setup_base_instance()
 | 
			
		||||
 | 
			
		||||
    def test_get_agent_url(self):
 | 
			
		||||
        ver = settings.LATEST_AGENT_VER
 | 
			
		||||
 | 
			
		||||
        # test without token
 | 
			
		||||
        r = get_agent_url(goarch="amd64", plat="windows", token="")
 | 
			
		||||
        expected = f"https://github.com/amidaware/rmmagent/releases/download/v{ver}/tacticalagent-v{ver}-windows-amd64.exe"
 | 
			
		||||
        self.assertEqual(r, expected)
 | 
			
		||||
 | 
			
		||||
        # test with token
 | 
			
		||||
        r = get_agent_url(goarch="386", plat="linux", token="token123")
 | 
			
		||||
        expected = f"https://{settings.AGENTS_URL}version={ver}&arch=386&token=token123&plat=linux&api=api.example.com"
 | 
			
		||||
 | 
			
		||||
    @patch("agents.utils.get_mesh_device_id")
 | 
			
		||||
    @patch("agents.utils.asyncio.run")
 | 
			
		||||
    @patch("agents.utils.get_mesh_ws_url")
 | 
			
		||||
    @patch("agents.utils.get_core_settings")
 | 
			
		||||
    def test_generate_linux_install(
 | 
			
		||||
        self, mock_core, mock_mesh, mock_async_run, mock_mesh_device_id
 | 
			
		||||
    ):
 | 
			
		||||
        mock_mesh_device_id.return_value = "meshdeviceid"
 | 
			
		||||
        mock_core.return_value.mesh_site = "meshsite"
 | 
			
		||||
        mock_async_run.return_value = "meshid"
 | 
			
		||||
        mock_mesh.return_value = "meshws"
 | 
			
		||||
        r = generate_linux_install(
 | 
			
		||||
            client="1",
 | 
			
		||||
            site="1",
 | 
			
		||||
            agent_type="server",
 | 
			
		||||
            arch="amd64",
 | 
			
		||||
            token="token123",
 | 
			
		||||
            api="api.example.com",
 | 
			
		||||
            download_url="asdasd3423",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        ret = r.getvalue().decode("utf-8")
 | 
			
		||||
 | 
			
		||||
        self.assertIn(r"agentDL='asdasd3423'", ret)
 | 
			
		||||
        self.assertIn(
 | 
			
		||||
            r"meshDL='meshsite/meshagents?id=meshid&installflags=2&meshinstall=6'", ret
 | 
			
		||||
        )
 | 
			
		||||
        self.assertIn(r"apiURL='api.example.com'", ret)
 | 
			
		||||
        self.assertIn(r"agentDL='asdasd3423'", ret)
 | 
			
		||||
        self.assertIn(r"token='token123'", ret)
 | 
			
		||||
        self.assertIn(r"clientID='1'", ret)
 | 
			
		||||
        self.assertIn(r"siteID='1'", ret)
 | 
			
		||||
        self.assertIn(r"agentType='server'", ret)
 | 
			
		||||
@@ -1,28 +1,32 @@
 | 
			
		||||
import json
 | 
			
		||||
import os
 | 
			
		||||
from itertools import cycle
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
import pytz
 | 
			
		||||
from typing import TYPE_CHECKING
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
from zoneinfo import ZoneInfo
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.test import modify_settings
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from logs.models import PendingAction
 | 
			
		||||
from model_bakery import baker
 | 
			
		||||
from packaging import version as pyver
 | 
			
		||||
from winupdate.models import WinUpdatePolicy
 | 
			
		||||
from winupdate.serializers import WinUpdatePolicySerializer
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
 | 
			
		||||
from .models import Agent, AgentCustomField, AgentHistory, Note
 | 
			
		||||
from .serializers import (
 | 
			
		||||
from agents.models import Agent, AgentCustomField, AgentHistory, Note
 | 
			
		||||
from agents.serializers import (
 | 
			
		||||
    AgentHistorySerializer,
 | 
			
		||||
    AgentHostnameSerializer,
 | 
			
		||||
    AgentNoteSerializer,
 | 
			
		||||
    AgentSerializer,
 | 
			
		||||
)
 | 
			
		||||
from .tasks import auto_self_agent_update_task
 | 
			
		||||
from tacticalrmm.constants import (
 | 
			
		||||
    AGENT_STATUS_OFFLINE,
 | 
			
		||||
    AGENT_STATUS_ONLINE,
 | 
			
		||||
    AgentMonType,
 | 
			
		||||
    CustomFieldModel,
 | 
			
		||||
    CustomFieldType,
 | 
			
		||||
    EvtLogNames,
 | 
			
		||||
)
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
from winupdate.models import WinUpdatePolicy
 | 
			
		||||
from winupdate.serializers import WinUpdatePolicySerializer
 | 
			
		||||
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
    from clients.models import Client, Site
 | 
			
		||||
@@ -30,11 +34,6 @@ if TYPE_CHECKING:
 | 
			
		||||
base_url = "/agents"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@modify_settings(
 | 
			
		||||
    MIDDLEWARE={
 | 
			
		||||
        "remove": "tacticalrmm.middleware.LinuxMiddleware",
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
class TestAgentsList(TacticalTestCase):
 | 
			
		||||
    def setUp(self) -> None:
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
@@ -51,24 +50,27 @@ class TestAgentsList(TacticalTestCase):
 | 
			
		||||
        site3: "Site" = baker.make("clients.Site", client=company2)
 | 
			
		||||
 | 
			
		||||
        baker.make_recipe(
 | 
			
		||||
            "agents.online_agent", site=site1, monitoring_type="server", _quantity=15
 | 
			
		||||
            "agents.online_agent",
 | 
			
		||||
            site=site1,
 | 
			
		||||
            monitoring_type=AgentMonType.SERVER,
 | 
			
		||||
            _quantity=15,
 | 
			
		||||
        )
 | 
			
		||||
        baker.make_recipe(
 | 
			
		||||
            "agents.online_agent",
 | 
			
		||||
            site=site2,
 | 
			
		||||
            monitoring_type="workstation",
 | 
			
		||||
            monitoring_type=AgentMonType.WORKSTATION,
 | 
			
		||||
            _quantity=10,
 | 
			
		||||
        )
 | 
			
		||||
        baker.make_recipe(
 | 
			
		||||
            "agents.online_agent",
 | 
			
		||||
            site=site3,
 | 
			
		||||
            monitoring_type="server",
 | 
			
		||||
            monitoring_type=AgentMonType.SERVER,
 | 
			
		||||
            _quantity=4,
 | 
			
		||||
        )
 | 
			
		||||
        baker.make_recipe(
 | 
			
		||||
            "agents.online_agent",
 | 
			
		||||
            site=site3,
 | 
			
		||||
            monitoring_type="workstation",
 | 
			
		||||
            monitoring_type=AgentMonType.WORKSTATION,
 | 
			
		||||
            _quantity=7,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@@ -100,11 +102,6 @@ class TestAgentsList(TacticalTestCase):
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@modify_settings(
 | 
			
		||||
    MIDDLEWARE={
 | 
			
		||||
        "remove": "tacticalrmm.middleware.LinuxMiddleware",
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
class TestAgentViews(TacticalTestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
@@ -117,6 +114,17 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
        )
 | 
			
		||||
        baker.make_recipe("winupdate.winupdate_policy", agent=self.agent)
 | 
			
		||||
 | 
			
		||||
    @patch("agents.tasks.bulk_recover_agents_task.delay")
 | 
			
		||||
    def test_bulk_agent_recovery(self, mock_task):
 | 
			
		||||
        mock_task.return_value = None
 | 
			
		||||
        url = f"{base_url}/bulkrecovery/"
 | 
			
		||||
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        mock_task.assert_called_once()
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    def test_get_agent(self):
 | 
			
		||||
        url = f"{base_url}/{self.agent.agent_id}/"
 | 
			
		||||
 | 
			
		||||
@@ -170,7 +178,11 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(data["run_time_days"], [2, 3, 6])
 | 
			
		||||
 | 
			
		||||
        # test adding custom fields
 | 
			
		||||
        field = baker.make("core.CustomField", model="agent", type="number")
 | 
			
		||||
        field = baker.make(
 | 
			
		||||
            "core.CustomField",
 | 
			
		||||
            model=CustomFieldModel.AGENT,
 | 
			
		||||
            type=CustomFieldType.NUMBER,
 | 
			
		||||
        )
 | 
			
		||||
        data = {
 | 
			
		||||
            "site": site.pk,
 | 
			
		||||
            "description": "asjdk234andasd",
 | 
			
		||||
@@ -227,7 +239,7 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
        self.agent.save(update_fields=["policy"])
 | 
			
		||||
        _ = self.agent.get_patch_policy()
 | 
			
		||||
 | 
			
		||||
        self.agent.monitoring_type = "workstation"
 | 
			
		||||
        self.agent.monitoring_type = AgentMonType.WORKSTATION
 | 
			
		||||
        self.agent.save(update_fields=["monitoring_type"])
 | 
			
		||||
        _ = self.agent.get_patch_policy()
 | 
			
		||||
 | 
			
		||||
@@ -239,52 +251,21 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
        self.coresettings.save(update_fields=["server_policy", "workstation_policy"])
 | 
			
		||||
        _ = self.agent.get_patch_policy()
 | 
			
		||||
 | 
			
		||||
        self.agent.monitoring_type = "server"
 | 
			
		||||
        self.agent.monitoring_type = AgentMonType.SERVER
 | 
			
		||||
        self.agent.save(update_fields=["monitoring_type"])
 | 
			
		||||
        _ = self.agent.get_patch_policy()
 | 
			
		||||
 | 
			
		||||
    def test_get_agent_versions(self):
 | 
			
		||||
        url = "/agents/versions/"
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
 | 
			
		||||
        with self.assertNumQueries(1):
 | 
			
		||||
            r = self.client.get(url)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        assert any(i["hostname"] == self.agent.hostname for i in r.json()["agents"])
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    @patch("agents.tasks.send_agent_update_task.delay")
 | 
			
		||||
    def test_update_agents(self, mock_task):
 | 
			
		||||
        url = f"{base_url}/update/"
 | 
			
		||||
        baker.make_recipe(
 | 
			
		||||
            "agents.agent",
 | 
			
		||||
            operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
 | 
			
		||||
            version=settings.LATEST_AGENT_VER,
 | 
			
		||||
            _quantity=15,
 | 
			
		||||
        )
 | 
			
		||||
        baker.make_recipe(
 | 
			
		||||
            "agents.agent",
 | 
			
		||||
            operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
 | 
			
		||||
            version="1.3.0",
 | 
			
		||||
            _quantity=15,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        agent_ids: list[str] = list(
 | 
			
		||||
            Agent.objects.only("agent_id", "version").values_list("agent_id", flat=True)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        data = {"agent_ids": agent_ids}
 | 
			
		||||
        expected: list[str] = [
 | 
			
		||||
            i.agent_id
 | 
			
		||||
            for i in Agent.objects.only("agent_id", "version")
 | 
			
		||||
            if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        mock_task.assert_called_with(agent_ids=expected)
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
    @patch("time.sleep", return_value=None)
 | 
			
		||||
    @patch("agents.models.Agent.nats_cmd")
 | 
			
		||||
    def test_agent_ping(self, nats_cmd, mock_sleep):
 | 
			
		||||
@@ -293,25 +274,25 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
        nats_cmd.return_value = "timeout"
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        ret = {"name": self.agent.hostname, "status": "offline"}
 | 
			
		||||
        ret = {"name": self.agent.hostname, "status": AGENT_STATUS_OFFLINE}
 | 
			
		||||
        self.assertEqual(r.json(), ret)
 | 
			
		||||
 | 
			
		||||
        nats_cmd.return_value = "natsdown"
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        ret = {"name": self.agent.hostname, "status": "offline"}
 | 
			
		||||
        ret = {"name": self.agent.hostname, "status": AGENT_STATUS_OFFLINE}
 | 
			
		||||
        self.assertEqual(r.json(), ret)
 | 
			
		||||
 | 
			
		||||
        nats_cmd.return_value = "pong"
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        ret = {"name": self.agent.hostname, "status": "online"}
 | 
			
		||||
        ret = {"name": self.agent.hostname, "status": AGENT_STATUS_ONLINE}
 | 
			
		||||
        self.assertEqual(r.json(), ret)
 | 
			
		||||
 | 
			
		||||
        nats_cmd.return_value = "asdasjdaksdasd"
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        ret = {"name": self.agent.hostname, "status": "offline"}
 | 
			
		||||
        ret = {"name": self.agent.hostname, "status": AGENT_STATUS_OFFLINE}
 | 
			
		||||
        self.assertEqual(r.json(), ret)
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
@@ -371,7 +352,7 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
                "func": "eventlog",
 | 
			
		||||
                "timeout": 30,
 | 
			
		||||
                "payload": {
 | 
			
		||||
                    "logname": "Application",
 | 
			
		||||
                    "logname": EvtLogNames.APPLICATION,
 | 
			
		||||
                    "days": str(22),
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
@@ -386,7 +367,7 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
                "func": "eventlog",
 | 
			
		||||
                "timeout": 180,
 | 
			
		||||
                "payload": {
 | 
			
		||||
                    "logname": "Security",
 | 
			
		||||
                    "logname": EvtLogNames.SECURITY,
 | 
			
		||||
                    "days": str(6),
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
@@ -422,6 +403,7 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
            "cmd": "ipconfig",
 | 
			
		||||
            "shell": "cmd",
 | 
			
		||||
            "timeout": 30,
 | 
			
		||||
            "run_as_user": False,
 | 
			
		||||
        }
 | 
			
		||||
        mock_ret.return_value = "nt authority\\system"
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
@@ -436,16 +418,20 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
    @patch("agents.models.Agent.nats_cmd")
 | 
			
		||||
    def test_reboot_later(self, nats_cmd):
 | 
			
		||||
        nats_cmd.return_value = "ok"
 | 
			
		||||
        url = f"{base_url}/{self.agent.agent_id}/reboot/"
 | 
			
		||||
 | 
			
		||||
        data = {
 | 
			
		||||
            "datetime": "2025-08-29T18:41:02",
 | 
			
		||||
        }
 | 
			
		||||
        # ensure we don't allow dates in past
 | 
			
		||||
        data = {"datetime": "2022-07-11T01:51"}
 | 
			
		||||
        r = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
        self.assertEqual(r.data, "Date cannot be set in the past")
 | 
			
		||||
 | 
			
		||||
        nats_cmd.return_value = "ok"
 | 
			
		||||
        # test with date in future
 | 
			
		||||
        data["datetime"] = "2027-08-29T18:41"
 | 
			
		||||
        r = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(r.data["time"], "August 29, 2025 at 06:41 PM")
 | 
			
		||||
        self.assertEqual(r.data["time"], "August 29, 2027 at 06:41 PM")
 | 
			
		||||
        self.assertEqual(r.data["agent"], self.agent.hostname)
 | 
			
		||||
 | 
			
		||||
        nats_data = {
 | 
			
		||||
@@ -458,12 +444,12 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
                "multiple_instances": 2,
 | 
			
		||||
                "trigger": "runonce",
 | 
			
		||||
                "name": r.data["task_name"],
 | 
			
		||||
                "start_year": 2025,
 | 
			
		||||
                "start_year": 2027,
 | 
			
		||||
                "start_month": 8,
 | 
			
		||||
                "start_day": 29,
 | 
			
		||||
                "start_hour": 18,
 | 
			
		||||
                "start_min": 41,
 | 
			
		||||
                "expire_year": 2025,
 | 
			
		||||
                "expire_year": 2027,
 | 
			
		||||
                "expire_month": 8,
 | 
			
		||||
                "expire_day": 29,
 | 
			
		||||
                "expire_hour": 18,
 | 
			
		||||
@@ -486,42 +472,6 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("patch", url)
 | 
			
		||||
 | 
			
		||||
    def test_install_agent(self):
 | 
			
		||||
        url = f"{base_url}/installer/"
 | 
			
		||||
 | 
			
		||||
        site = baker.make("clients.Site")
 | 
			
		||||
        data = {
 | 
			
		||||
            "client": site.client.pk,
 | 
			
		||||
            "site": site.pk,
 | 
			
		||||
            "arch": "64",
 | 
			
		||||
            "expires": 23,
 | 
			
		||||
            "installMethod": "manual",
 | 
			
		||||
            "api": "https://api.example.com",
 | 
			
		||||
            "agenttype": "server",
 | 
			
		||||
            "rdp": 1,
 | 
			
		||||
            "ping": 0,
 | 
			
		||||
            "power": 0,
 | 
			
		||||
            "fileName": "rmm-client-site-server.exe",
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        data["arch"] = "64"
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertIn("rdp", r.json()["cmd"])
 | 
			
		||||
        self.assertNotIn("power", r.json()["cmd"])
 | 
			
		||||
 | 
			
		||||
        data.update({"ping": 1, "power": 1})
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertIn("power", r.json()["cmd"])
 | 
			
		||||
        self.assertIn("ping", r.json()["cmd"])
 | 
			
		||||
 | 
			
		||||
        data["installMethod"] = "powershell"
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
    @patch("meshctrl.utils.get_login_token")
 | 
			
		||||
    def test_meshcentral_tabs(self, mock_token):
 | 
			
		||||
        url = f"{base_url}/{self.agent.agent_id}/meshcentral/"
 | 
			
		||||
@@ -576,10 +526,9 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
    @patch("agents.tasks.run_script_email_results_task.delay")
 | 
			
		||||
    @patch("agents.models.Agent.run_script")
 | 
			
		||||
    def test_run_script(self, run_script, email_task):
 | 
			
		||||
        from agents.models import AgentCustomField, AgentHistory, Note
 | 
			
		||||
        from clients.models import ClientCustomField, SiteCustomField
 | 
			
		||||
 | 
			
		||||
        from .models import AgentCustomField, AgentHistory, Note
 | 
			
		||||
 | 
			
		||||
        run_script.return_value = "ok"
 | 
			
		||||
        url = f"/agents/{self.agent.agent_id}/runscript/"
 | 
			
		||||
        script = baker.make_recipe("scripts.script")
 | 
			
		||||
@@ -590,6 +539,8 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
            "output": "wait",
 | 
			
		||||
            "args": [],
 | 
			
		||||
            "timeout": 15,
 | 
			
		||||
            "run_as_user": False,
 | 
			
		||||
            "env_vars": ["hello=world", "foo=bar"],
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
@@ -599,7 +550,13 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
            raise AgentHistory.DoesNotExist
 | 
			
		||||
 | 
			
		||||
        run_script.assert_called_with(
 | 
			
		||||
            scriptpk=script.pk, args=[], timeout=18, wait=True, history_pk=hist.pk
 | 
			
		||||
            scriptpk=script.pk,
 | 
			
		||||
            args=[],
 | 
			
		||||
            timeout=18,
 | 
			
		||||
            wait=True,
 | 
			
		||||
            history_pk=hist.pk,
 | 
			
		||||
            run_as_user=False,
 | 
			
		||||
            env_vars=["hello=world", "foo=bar"],
 | 
			
		||||
        )
 | 
			
		||||
        run_script.reset_mock()
 | 
			
		||||
 | 
			
		||||
@@ -611,15 +568,21 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
            "timeout": 15,
 | 
			
		||||
            "emailMode": "default",
 | 
			
		||||
            "emails": ["admin@example.com", "bob@example.com"],
 | 
			
		||||
            "run_as_user": False,
 | 
			
		||||
            "env_vars": ["hello=world", "foo=bar"],
 | 
			
		||||
        }
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        hist = AgentHistory.objects.filter(agent=self.agent, script=script).last()
 | 
			
		||||
        email_task.assert_called_with(
 | 
			
		||||
            agentpk=self.agent.pk,
 | 
			
		||||
            scriptpk=script.pk,
 | 
			
		||||
            nats_timeout=18,
 | 
			
		||||
            emails=[],
 | 
			
		||||
            args=["abc", "123"],
 | 
			
		||||
            history_pk=hist.pk,
 | 
			
		||||
            run_as_user=False,
 | 
			
		||||
            env_vars=["hello=world", "foo=bar"],
 | 
			
		||||
        )
 | 
			
		||||
        email_task.reset_mock()
 | 
			
		||||
 | 
			
		||||
@@ -627,12 +590,16 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
        data["emailMode"] = "custom"
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        hist = AgentHistory.objects.filter(agent=self.agent, script=script).last()
 | 
			
		||||
        email_task.assert_called_with(
 | 
			
		||||
            agentpk=self.agent.pk,
 | 
			
		||||
            scriptpk=script.pk,
 | 
			
		||||
            nats_timeout=18,
 | 
			
		||||
            emails=["admin@example.com", "bob@example.com"],
 | 
			
		||||
            args=["abc", "123"],
 | 
			
		||||
            history_pk=hist.pk,
 | 
			
		||||
            run_as_user=False,
 | 
			
		||||
            env_vars=["hello=world", "foo=bar"],
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # test fire and forget
 | 
			
		||||
@@ -641,6 +608,8 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
            "output": "forget",
 | 
			
		||||
            "args": ["hello", "world"],
 | 
			
		||||
            "timeout": 22,
 | 
			
		||||
            "run_as_user": True,
 | 
			
		||||
            "env_vars": ["hello=world", "foo=bar"],
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
@@ -650,14 +619,19 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
            raise AgentHistory.DoesNotExist
 | 
			
		||||
 | 
			
		||||
        run_script.assert_called_with(
 | 
			
		||||
            scriptpk=script.pk, args=["hello", "world"], timeout=25, history_pk=hist.pk
 | 
			
		||||
            scriptpk=script.pk,
 | 
			
		||||
            args=["hello", "world"],
 | 
			
		||||
            timeout=25,
 | 
			
		||||
            history_pk=hist.pk,
 | 
			
		||||
            run_as_user=True,
 | 
			
		||||
            env_vars=["hello=world", "foo=bar"],
 | 
			
		||||
        )
 | 
			
		||||
        run_script.reset_mock()
 | 
			
		||||
 | 
			
		||||
        # test collector
 | 
			
		||||
 | 
			
		||||
        # save to agent custom field
 | 
			
		||||
        custom_field = baker.make("core.CustomField", model="agent")
 | 
			
		||||
        custom_field = baker.make("core.CustomField", model=CustomFieldModel.AGENT)
 | 
			
		||||
        data = {
 | 
			
		||||
            "script": script.pk,
 | 
			
		||||
            "output": "collector",
 | 
			
		||||
@@ -665,6 +639,8 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
            "timeout": 22,
 | 
			
		||||
            "custom_field": custom_field.pk,
 | 
			
		||||
            "save_all_output": True,
 | 
			
		||||
            "run_as_user": False,
 | 
			
		||||
            "env_vars": ["hello=world", "foo=bar"],
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
@@ -679,6 +655,8 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
            timeout=25,
 | 
			
		||||
            wait=True,
 | 
			
		||||
            history_pk=hist.pk,
 | 
			
		||||
            run_as_user=False,
 | 
			
		||||
            env_vars=["hello=world", "foo=bar"],
 | 
			
		||||
        )
 | 
			
		||||
        run_script.reset_mock()
 | 
			
		||||
 | 
			
		||||
@@ -696,6 +674,8 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
            "timeout": 22,
 | 
			
		||||
            "custom_field": custom_field.pk,
 | 
			
		||||
            "save_all_output": False,
 | 
			
		||||
            "run_as_user": False,
 | 
			
		||||
            "env_vars": ["hello=world", "foo=bar"],
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
@@ -710,6 +690,8 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
            timeout=25,
 | 
			
		||||
            wait=True,
 | 
			
		||||
            history_pk=hist.pk,
 | 
			
		||||
            run_as_user=False,
 | 
			
		||||
            env_vars=["hello=world", "foo=bar"],
 | 
			
		||||
        )
 | 
			
		||||
        run_script.reset_mock()
 | 
			
		||||
 | 
			
		||||
@@ -721,7 +703,7 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # save to client custom field
 | 
			
		||||
        custom_field = baker.make("core.CustomField", model="client")
 | 
			
		||||
        custom_field = baker.make("core.CustomField", model=CustomFieldModel.CLIENT)
 | 
			
		||||
        data = {
 | 
			
		||||
            "script": script.pk,
 | 
			
		||||
            "output": "collector",
 | 
			
		||||
@@ -729,6 +711,8 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
            "timeout": 22,
 | 
			
		||||
            "custom_field": custom_field.pk,
 | 
			
		||||
            "save_all_output": False,
 | 
			
		||||
            "run_as_user": False,
 | 
			
		||||
            "env_vars": ["hello=world", "foo=bar"],
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
@@ -743,6 +727,8 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
            timeout=25,
 | 
			
		||||
            wait=True,
 | 
			
		||||
            history_pk=hist.pk,
 | 
			
		||||
            run_as_user=False,
 | 
			
		||||
            env_vars=["hello=world", "foo=bar"],
 | 
			
		||||
        )
 | 
			
		||||
        run_script.reset_mock()
 | 
			
		||||
 | 
			
		||||
@@ -759,6 +745,8 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
            "output": "note",
 | 
			
		||||
            "args": ["hello", "world"],
 | 
			
		||||
            "timeout": 22,
 | 
			
		||||
            "run_as_user": False,
 | 
			
		||||
            "env_vars": ["hello=world", "foo=bar"],
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
@@ -773,6 +761,8 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
            timeout=25,
 | 
			
		||||
            wait=True,
 | 
			
		||||
            history_pk=hist.pk,
 | 
			
		||||
            run_as_user=False,
 | 
			
		||||
            env_vars=["hello=world", "foo=bar"],
 | 
			
		||||
        )
 | 
			
		||||
        run_script.reset_mock()
 | 
			
		||||
 | 
			
		||||
@@ -865,7 +855,6 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
        self.check_not_authenticated("delete", url)
 | 
			
		||||
 | 
			
		||||
    def test_get_agent_history(self):
 | 
			
		||||
 | 
			
		||||
        # setup data
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        history = baker.make("agents.AgentHistory", agent=agent, _quantity=30)
 | 
			
		||||
@@ -877,17 +866,12 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        # test pulling data
 | 
			
		||||
        r = self.client.get(url, format="json")
 | 
			
		||||
        ctx = {"default_tz": pytz.timezone("America/Los_Angeles")}
 | 
			
		||||
        ctx = {"default_tz": ZoneInfo("America/Los_Angeles")}
 | 
			
		||||
        data = AgentHistorySerializer(history, many=True, context=ctx).data
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(r.data, data)  # type:ignore
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@modify_settings(
 | 
			
		||||
    MIDDLEWARE={
 | 
			
		||||
        "remove": "tacticalrmm.middleware.LinuxMiddleware",
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
class TestAgentViewsNew(TacticalTestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
@@ -922,11 +906,6 @@ class TestAgentViewsNew(TacticalTestCase):
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@modify_settings(
 | 
			
		||||
    MIDDLEWARE={
 | 
			
		||||
        "remove": "tacticalrmm.middleware.LinuxMiddleware",
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
class TestAgentPermissions(TacticalTestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.setup_client()
 | 
			
		||||
@@ -1031,7 +1010,6 @@ class TestAgentPermissions(TacticalTestCase):
 | 
			
		||||
    @patch("time.sleep")
 | 
			
		||||
    @patch("agents.models.Agent.nats_cmd", return_value="ok")
 | 
			
		||||
    def test_agent_actions_permissions(self, nats_cmd, sleep):
 | 
			
		||||
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        unauthorized_agent = baker.make_recipe("agents.agent")
 | 
			
		||||
 | 
			
		||||
@@ -1042,7 +1020,6 @@ class TestAgentPermissions(TacticalTestCase):
 | 
			
		||||
            {"method": "post", "action": "recover", "role": "can_recover_agents"},
 | 
			
		||||
            {"method": "post", "action": "reboot", "role": "can_reboot_agents"},
 | 
			
		||||
            {"method": "patch", "action": "reboot", "role": "can_reboot_agents"},
 | 
			
		||||
            {"method": "get", "action": "ping", "role": "can_ping_agents"},
 | 
			
		||||
            {"method": "get", "action": "meshcentral", "role": "can_use_mesh"},
 | 
			
		||||
            {"method": "post", "action": "meshcentral/recover", "role": "can_use_mesh"},
 | 
			
		||||
            {"method": "get", "action": "processes", "role": "can_manage_procs"},
 | 
			
		||||
@@ -1122,55 +1099,6 @@ class TestAgentPermissions(TacticalTestCase):
 | 
			
		||||
        self.check_authorized("post", url, site_data)
 | 
			
		||||
        self.check_authorized("post", url, client_data)
 | 
			
		||||
 | 
			
		||||
    @patch("agents.tasks.send_agent_update_task.delay")
 | 
			
		||||
    def test_agent_update_permissions(self, update_task):
 | 
			
		||||
        agents = baker.make_recipe("agents.agent", _quantity=5)
 | 
			
		||||
        other_agents = baker.make_recipe("agents.agent", _quantity=7)
 | 
			
		||||
 | 
			
		||||
        url = f"{base_url}/update/"
 | 
			
		||||
 | 
			
		||||
        data = {
 | 
			
		||||
            "agent_ids": [agent.agent_id for agent in agents]
 | 
			
		||||
            + [agent.agent_id for agent in other_agents]
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        # test superuser access
 | 
			
		||||
        self.check_authorized_superuser("post", url, data)
 | 
			
		||||
        update_task.assert_called_with(agent_ids=data["agent_ids"])
 | 
			
		||||
        update_task.reset_mock()
 | 
			
		||||
 | 
			
		||||
        user = self.create_user_with_roles([])
 | 
			
		||||
        self.client.force_authenticate(user=user)
 | 
			
		||||
 | 
			
		||||
        self.check_not_authorized("post", url, data)
 | 
			
		||||
        update_task.assert_not_called()
 | 
			
		||||
 | 
			
		||||
        user.role.can_update_agents = True
 | 
			
		||||
        user.role.save()
 | 
			
		||||
 | 
			
		||||
        self.check_authorized("post", url, data)
 | 
			
		||||
        update_task.assert_called_with(agent_ids=data["agent_ids"])
 | 
			
		||||
        update_task.reset_mock()
 | 
			
		||||
 | 
			
		||||
        # limit to client
 | 
			
		||||
        # user.role.can_view_clients.set([agents[0].client])
 | 
			
		||||
        # self.check_authorized("post", url, data)
 | 
			
		||||
        # update_task.assert_called_with(agent_ids=[agent.agent_id for agent in agents])
 | 
			
		||||
        # update_task.reset_mock()
 | 
			
		||||
 | 
			
		||||
        # add site
 | 
			
		||||
        # user.role.can_view_sites.set([other_agents[0].site])
 | 
			
		||||
        # self.check_authorized("post", url, data)
 | 
			
		||||
        # update_task.assert_called_with(agent_ids=data["agent_ids"])
 | 
			
		||||
        # update_task.reset_mock()
 | 
			
		||||
 | 
			
		||||
        # remove client permissions
 | 
			
		||||
        # user.role.can_view_clients.clear()
 | 
			
		||||
        # self.check_authorized("post", url, data)
 | 
			
		||||
        # update_task.assert_called_with(
 | 
			
		||||
        #     agent_ids=[agent.agent_id for agent in other_agents]
 | 
			
		||||
        # )
 | 
			
		||||
 | 
			
		||||
    def test_get_agent_version_permissions(self):
 | 
			
		||||
        agents = baker.make_recipe("agents.agent", _quantity=5)
 | 
			
		||||
        other_agents = baker.make_recipe("agents.agent", _quantity=7)
 | 
			
		||||
@@ -1208,7 +1136,6 @@ class TestAgentPermissions(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(len(response.data["agents"]), 7)
 | 
			
		||||
 | 
			
		||||
    def test_generating_agent_installer_permissions(self):
 | 
			
		||||
 | 
			
		||||
        client = baker.make("clients.Client")
 | 
			
		||||
        client_site = baker.make("clients.Site", client=client)
 | 
			
		||||
        site = baker.make("clients.Site")
 | 
			
		||||
@@ -1271,7 +1198,6 @@ class TestAgentPermissions(TacticalTestCase):
 | 
			
		||||
        self.check_not_authorized("post", url, data)
 | 
			
		||||
 | 
			
		||||
    def test_agent_notes_permissions(self):
 | 
			
		||||
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        notes = baker.make("agents.Note", agent=agent, _quantity=5)
 | 
			
		||||
 | 
			
		||||
@@ -1360,9 +1286,9 @@ class TestAgentPermissions(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        sites = baker.make("clients.Site", _quantity=2)
 | 
			
		||||
        agent = baker.make_recipe("agents.agent", site=sites[0])
 | 
			
		||||
        history = baker.make("agents.AgentHistory", agent=agent, _quantity=5)
 | 
			
		||||
        history = baker.make("agents.AgentHistory", agent=agent, _quantity=5)  # noqa
 | 
			
		||||
        unauthorized_agent = baker.make_recipe("agents.agent", site=sites[1])
 | 
			
		||||
        unauthorized_history = baker.make(
 | 
			
		||||
        unauthorized_history = baker.make(  # noqa
 | 
			
		||||
            "agents.AgentHistory", agent=unauthorized_agent, _quantity=6
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@@ -1400,154 +1326,13 @@ class TestAgentPermissions(TacticalTestCase):
 | 
			
		||||
        self.check_authorized_superuser("get", unauthorized_url)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@modify_settings(
 | 
			
		||||
    MIDDLEWARE={
 | 
			
		||||
        "remove": "tacticalrmm.middleware.LinuxMiddleware",
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
class TestAgentTasks(TacticalTestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        self.setup_coresettings()
 | 
			
		||||
 | 
			
		||||
    @patch("agents.utils.get_agent_url")
 | 
			
		||||
    @patch("agents.models.Agent.nats_cmd")
 | 
			
		||||
    def test_agent_update(self, nats_cmd, get_url):
 | 
			
		||||
        get_url.return_value = "https://exe.tacticalrmm.io"
 | 
			
		||||
 | 
			
		||||
        from agents.tasks import agent_update
 | 
			
		||||
 | 
			
		||||
        agent_noarch = baker.make_recipe(
 | 
			
		||||
            "agents.agent",
 | 
			
		||||
            operating_system="Error getting OS",
 | 
			
		||||
            version=settings.LATEST_AGENT_VER,
 | 
			
		||||
        )
 | 
			
		||||
        r = agent_update(agent_noarch.agent_id)
 | 
			
		||||
        self.assertEqual(r, "noarch")
 | 
			
		||||
 | 
			
		||||
        agent_130 = baker.make_recipe(
 | 
			
		||||
            "agents.agent",
 | 
			
		||||
            operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
 | 
			
		||||
            version="1.3.0",
 | 
			
		||||
        )
 | 
			
		||||
        r = agent_update(agent_130.agent_id)
 | 
			
		||||
        self.assertEqual(r, "not supported")
 | 
			
		||||
 | 
			
		||||
        # test __without__ code signing
 | 
			
		||||
        agent64_nosign = baker.make_recipe(
 | 
			
		||||
            "agents.agent",
 | 
			
		||||
            operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
 | 
			
		||||
            version="1.4.14",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        r = agent_update(agent64_nosign.agent_id)
 | 
			
		||||
        self.assertEqual(r, "created")
 | 
			
		||||
        action = PendingAction.objects.get(agent__agent_id=agent64_nosign.agent_id)
 | 
			
		||||
        self.assertEqual(action.action_type, "agentupdate")
 | 
			
		||||
        self.assertEqual(action.status, "pending")
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            action.details["url"],
 | 
			
		||||
            f"https://github.com/amidaware/rmmagent/releases/download/v{settings.LATEST_AGENT_VER}/winagent-v{settings.LATEST_AGENT_VER}.exe",
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            action.details["inno"], f"winagent-v{settings.LATEST_AGENT_VER}.exe"
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(action.details["version"], settings.LATEST_AGENT_VER)
 | 
			
		||||
        nats_cmd.assert_called_with(
 | 
			
		||||
            {
 | 
			
		||||
                "func": "agentupdate",
 | 
			
		||||
                "payload": {
 | 
			
		||||
                    "url": f"https://github.com/amidaware/rmmagent/releases/download/v{settings.LATEST_AGENT_VER}/winagent-v{settings.LATEST_AGENT_VER}.exe",
 | 
			
		||||
                    "version": settings.LATEST_AGENT_VER,
 | 
			
		||||
                    "inno": f"winagent-v{settings.LATEST_AGENT_VER}.exe",
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            wait=False,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # test __with__ code signing (64 bit)
 | 
			
		||||
        """ codesign = baker.make("core.CodeSignToken", token="testtoken123")
 | 
			
		||||
        agent64_sign = baker.make_recipe(
 | 
			
		||||
            "agents.agent",
 | 
			
		||||
            operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
 | 
			
		||||
            version="1.4.14",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        nats_cmd.return_value = "ok"
 | 
			
		||||
        get_exe.return_value = "https://exe.tacticalrmm.io"
 | 
			
		||||
        r = agent_update(agent64_sign.pk, codesign.token)  
 | 
			
		||||
        self.assertEqual(r, "created")
 | 
			
		||||
        nats_cmd.assert_called_with(
 | 
			
		||||
            {
 | 
			
		||||
                "func": "agentupdate",
 | 
			
		||||
                "payload": {
 | 
			
		||||
                    "url": f"https://exe.tacticalrmm.io/api/v1/winagents/?version={settings.LATEST_AGENT_VER}&arch=64&token=testtoken123",  
 | 
			
		||||
                    "version": settings.LATEST_AGENT_VER,
 | 
			
		||||
                    "inno": f"winagent-v{settings.LATEST_AGENT_VER}.exe",
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            wait=False,
 | 
			
		||||
        )
 | 
			
		||||
        action = PendingAction.objects.get(agent__pk=agent64_sign.pk)
 | 
			
		||||
        self.assertEqual(action.action_type, "agentupdate")
 | 
			
		||||
        self.assertEqual(action.status, "pending")
 | 
			
		||||
 | 
			
		||||
        # test __with__ code signing (32 bit)
 | 
			
		||||
        agent32_sign = baker.make_recipe(
 | 
			
		||||
            "agents.agent",
 | 
			
		||||
            operating_system="Windows 10 Pro, 32 bit (build 19041.450)",
 | 
			
		||||
            version="1.4.14",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        nats_cmd.return_value = "ok"
 | 
			
		||||
        get_exe.return_value = "https://exe.tacticalrmm.io"
 | 
			
		||||
        r = agent_update(agent32_sign.pk, codesign.token)  
 | 
			
		||||
        self.assertEqual(r, "created")
 | 
			
		||||
        nats_cmd.assert_called_with(
 | 
			
		||||
            {
 | 
			
		||||
                "func": "agentupdate",
 | 
			
		||||
                "payload": {
 | 
			
		||||
                    "url": f"https://exe.tacticalrmm.io/api/v1/winagents/?version={settings.LATEST_AGENT_VER}&arch=32&token=testtoken123",  
 | 
			
		||||
                    "version": settings.LATEST_AGENT_VER,
 | 
			
		||||
                    "inno": f"winagent-v{settings.LATEST_AGENT_VER}-x86.exe",
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            wait=False,
 | 
			
		||||
        )
 | 
			
		||||
        action = PendingAction.objects.get(agent__pk=agent32_sign.pk)
 | 
			
		||||
        self.assertEqual(action.action_type, "agentupdate")
 | 
			
		||||
        self.assertEqual(action.status, "pending") """
 | 
			
		||||
 | 
			
		||||
    @patch("agents.tasks.agent_update")
 | 
			
		||||
    @patch("agents.tasks.sleep", return_value=None)
 | 
			
		||||
    def test_auto_self_agent_update_task(self, mock_sleep, agent_update):
 | 
			
		||||
        baker.make_recipe(
 | 
			
		||||
            "agents.agent",
 | 
			
		||||
            operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
 | 
			
		||||
            version=settings.LATEST_AGENT_VER,
 | 
			
		||||
            _quantity=23,
 | 
			
		||||
        )
 | 
			
		||||
        baker.make_recipe(
 | 
			
		||||
            "agents.agent",
 | 
			
		||||
            operating_system="Windows 10 Pro, 64 bit (build 19041.450)",
 | 
			
		||||
            version="1.3.0",
 | 
			
		||||
            _quantity=33,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.coresettings.agent_auto_update = False
 | 
			
		||||
        self.coresettings.save(update_fields=["agent_auto_update"])
 | 
			
		||||
 | 
			
		||||
        r = auto_self_agent_update_task.s().apply()
 | 
			
		||||
        self.assertEqual(agent_update.call_count, 0)
 | 
			
		||||
 | 
			
		||||
        self.coresettings.agent_auto_update = True
 | 
			
		||||
        self.coresettings.save(update_fields=["agent_auto_update"])
 | 
			
		||||
 | 
			
		||||
        r = auto_self_agent_update_task.s().apply()
 | 
			
		||||
        self.assertEqual(agent_update.call_count, 33)
 | 
			
		||||
 | 
			
		||||
    def test_agent_history_prune_task(self):
 | 
			
		||||
        from .tasks import prune_agent_history
 | 
			
		||||
        from agents.tasks import prune_agent_history
 | 
			
		||||
 | 
			
		||||
        # setup data
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
							
								
								
									
										46
									
								
								api/tacticalrmm/agents/tests/test_mgmt_commands.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								api/tacticalrmm/agents/tests/test_mgmt_commands.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,46 @@
 | 
			
		||||
from unittest.mock import call, patch
 | 
			
		||||
 | 
			
		||||
from django.core.management import call_command
 | 
			
		||||
from model_bakery import baker
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.constants import AgentMonType, AgentPlat
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestBulkRestartAgents(TacticalTestCase):
 | 
			
		||||
    def setUp(self) -> None:
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        self.setup_coresettings()
 | 
			
		||||
        self.setup_base_instance()
 | 
			
		||||
 | 
			
		||||
    @patch("core.management.commands.bulk_restart_agents.sleep")
 | 
			
		||||
    @patch("agents.models.Agent.recover")
 | 
			
		||||
    @patch("core.management.commands.bulk_restart_agents.get_mesh_ws_url")
 | 
			
		||||
    def test_bulk_restart_agents_mgmt_cmd(
 | 
			
		||||
        self, get_mesh_ws_url, recover, mock_sleep
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        get_mesh_ws_url.return_value = "https://mesh.example.com/test"
 | 
			
		||||
 | 
			
		||||
        baker.make_recipe(
 | 
			
		||||
            "agents.online_agent",
 | 
			
		||||
            site=self.site1,
 | 
			
		||||
            monitoring_type=AgentMonType.SERVER,
 | 
			
		||||
            plat=AgentPlat.WINDOWS,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        baker.make_recipe(
 | 
			
		||||
            "agents.online_agent",
 | 
			
		||||
            site=self.site3,
 | 
			
		||||
            monitoring_type=AgentMonType.SERVER,
 | 
			
		||||
            plat=AgentPlat.LINUX,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        calls = [
 | 
			
		||||
            call("tacagent", "https://mesh.example.com/test", wait=False),
 | 
			
		||||
            call("mesh", "", wait=False),
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        call_command("bulk_restart_agents")
 | 
			
		||||
 | 
			
		||||
        recover.assert_has_calls(calls)
 | 
			
		||||
        mock_sleep.assert_called_with(10)
 | 
			
		||||
							
								
								
									
										63
									
								
								api/tacticalrmm/agents/tests/test_recovery.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								api/tacticalrmm/agents/tests/test_recovery.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,63 @@
 | 
			
		||||
from typing import TYPE_CHECKING
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
 | 
			
		||||
from model_bakery import baker
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.constants import AgentMonType, AgentPlat
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
    from clients.models import Client, Site
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestRecovery(TacticalTestCase):
 | 
			
		||||
    def setUp(self) -> None:
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        self.setup_coresettings()
 | 
			
		||||
        self.client1: "Client" = baker.make("clients.Client")
 | 
			
		||||
        self.site1: "Site" = baker.make("clients.Site", client=self.client1)
 | 
			
		||||
 | 
			
		||||
    @patch("agents.models.Agent.recover")
 | 
			
		||||
    @patch("agents.views.get_mesh_ws_url")
 | 
			
		||||
    def test_recover(self, get_mesh_ws_url, recover) -> None:
 | 
			
		||||
        get_mesh_ws_url.return_value = "https://mesh.example.com"
 | 
			
		||||
 | 
			
		||||
        agent = baker.make_recipe(
 | 
			
		||||
            "agents.online_agent",
 | 
			
		||||
            site=self.site1,
 | 
			
		||||
            monitoring_type=AgentMonType.SERVER,
 | 
			
		||||
            plat=AgentPlat.WINDOWS,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        url = f"/agents/{agent.agent_id}/recover/"
 | 
			
		||||
 | 
			
		||||
        # test successfull tacticalagent recovery
 | 
			
		||||
        data = {"mode": "tacagent"}
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        recover.assert_called_with("tacagent", "https://mesh.example.com", wait=False)
 | 
			
		||||
        get_mesh_ws_url.assert_called_once()
 | 
			
		||||
 | 
			
		||||
        # reset mocks
 | 
			
		||||
        recover.reset_mock()
 | 
			
		||||
        get_mesh_ws_url.reset_mock()
 | 
			
		||||
 | 
			
		||||
        # test successfull mesh agent recovery
 | 
			
		||||
        data = {"mode": "mesh"}
 | 
			
		||||
        recover.return_value = ("ok", False)
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        get_mesh_ws_url.assert_not_called()
 | 
			
		||||
        recover.assert_called_with("mesh", "")
 | 
			
		||||
 | 
			
		||||
        # reset mocks
 | 
			
		||||
        recover.reset_mock()
 | 
			
		||||
        get_mesh_ws_url.reset_mock()
 | 
			
		||||
 | 
			
		||||
        # test failed mesh agent recovery
 | 
			
		||||
        data = {"mode": "mesh"}
 | 
			
		||||
        recover.return_value = ("Unable to contact the agent", True)
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
from django.urls import path
 | 
			
		||||
 | 
			
		||||
from autotasks.views import GetAddAutoTasks
 | 
			
		||||
from checks.views import GetAddChecks
 | 
			
		||||
from django.urls import path
 | 
			
		||||
from logs.views import PendingActions
 | 
			
		||||
 | 
			
		||||
from . import views
 | 
			
		||||
@@ -14,6 +15,7 @@ urlpatterns = [
 | 
			
		||||
    path("<agent:agent_id>/wmi/", views.WMI.as_view()),
 | 
			
		||||
    path("<agent:agent_id>/recover/", views.recover),
 | 
			
		||||
    path("<agent:agent_id>/reboot/", views.Reboot.as_view()),
 | 
			
		||||
    path("<agent:agent_id>/shutdown/", views.Shutdown.as_view()),
 | 
			
		||||
    path("<agent:agent_id>/ping/", views.ping),
 | 
			
		||||
    # alias for checks get view
 | 
			
		||||
    path("<agent:agent_id>/checks/", GetAddChecks.as_view()),
 | 
			
		||||
@@ -40,4 +42,7 @@ urlpatterns = [
 | 
			
		||||
    path("versions/", views.get_agent_versions),
 | 
			
		||||
    path("update/", views.update_agents),
 | 
			
		||||
    path("installer/", views.install_agent),
 | 
			
		||||
    path("bulkrecovery/", views.bulk_agent_recovery),
 | 
			
		||||
    path("scripthistory/", views.ScriptRunHistory.as_view()),
 | 
			
		||||
    path("<agent:agent_id>/wol/", views.wol),
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,38 +1,28 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
import tempfile
 | 
			
		||||
import urllib.parse
 | 
			
		||||
from io import StringIO
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
 | 
			
		||||
from core.models import CodeSignToken
 | 
			
		||||
from core.utils import get_mesh_device_id, get_mesh_ws_url, get_core_settings
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.http import FileResponse
 | 
			
		||||
 | 
			
		||||
from core.utils import get_core_settings, get_mesh_device_id, get_mesh_ws_url
 | 
			
		||||
from tacticalrmm.constants import MeshAgentIdent
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_agent_url(arch: str, plat: str) -> str:
 | 
			
		||||
 | 
			
		||||
    if plat == "windows":
 | 
			
		||||
        endpoint = "winagents"
 | 
			
		||||
        dl_url = settings.DL_32 if arch == "32" else settings.DL_64
 | 
			
		||||
    else:
 | 
			
		||||
        endpoint = "linuxagents"
 | 
			
		||||
        dl_url = ""
 | 
			
		||||
 | 
			
		||||
    token = CodeSignToken.objects.first()
 | 
			
		||||
    if not token:
 | 
			
		||||
        return dl_url
 | 
			
		||||
 | 
			
		||||
    if token.is_valid:
 | 
			
		||||
        base_url = settings.EXE_GEN_URL + f"/api/v1/{endpoint}/?"
 | 
			
		||||
def get_agent_url(*, goarch: str, plat: str, token: str = "") -> str:
 | 
			
		||||
    ver = settings.LATEST_AGENT_VER
 | 
			
		||||
    if token:
 | 
			
		||||
        params = {
 | 
			
		||||
            "version": settings.LATEST_AGENT_VER,
 | 
			
		||||
            "arch": arch,
 | 
			
		||||
            "token": token.token,
 | 
			
		||||
            "version": ver,
 | 
			
		||||
            "arch": goarch,
 | 
			
		||||
            "token": token,
 | 
			
		||||
            "plat": plat,
 | 
			
		||||
            "api": settings.ALLOWED_HOSTS[0],
 | 
			
		||||
        }
 | 
			
		||||
        dl_url = base_url + urllib.parse.urlencode(params)
 | 
			
		||||
        return settings.AGENTS_URL + urllib.parse.urlencode(params)
 | 
			
		||||
 | 
			
		||||
    return dl_url
 | 
			
		||||
    return f"https://github.com/amidaware/rmmagent/releases/download/v{ver}/tacticalagent-v{ver}-{plat}-{goarch}.exe"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def generate_linux_install(
 | 
			
		||||
@@ -44,7 +34,6 @@ def generate_linux_install(
 | 
			
		||||
    api: str,
 | 
			
		||||
    download_url: str,
 | 
			
		||||
) -> FileResponse:
 | 
			
		||||
 | 
			
		||||
    match arch:
 | 
			
		||||
        case "amd64":
 | 
			
		||||
            arch_id = MeshAgentIdent.LINUX64
 | 
			
		||||
@@ -62,12 +51,10 @@ def generate_linux_install(
 | 
			
		||||
    uri = get_mesh_ws_url()
 | 
			
		||||
    mesh_id = asyncio.run(get_mesh_device_id(uri, core.mesh_device_group))
 | 
			
		||||
    mesh_dl = (
 | 
			
		||||
        f"{core.mesh_site}/meshagents?id={mesh_id}&installflags=0&meshinstall={arch_id}"
 | 
			
		||||
        f"{core.mesh_site}/meshagents?id={mesh_id}&installflags=2&meshinstall={arch_id}"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    sh = settings.LINUX_AGENT_SCRIPT
 | 
			
		||||
    with open(sh, "r") as f:
 | 
			
		||||
        text = f.read()
 | 
			
		||||
    text = Path(settings.LINUX_AGENT_SCRIPT).read_text()
 | 
			
		||||
 | 
			
		||||
    replace = {
 | 
			
		||||
        "agentDLChange": download_url,
 | 
			
		||||
@@ -82,11 +69,8 @@ def generate_linux_install(
 | 
			
		||||
    for i, j in replace.items():
 | 
			
		||||
        text = text.replace(i, j)
 | 
			
		||||
 | 
			
		||||
    with tempfile.NamedTemporaryFile() as fp:
 | 
			
		||||
        with open(fp.name, "w") as f:
 | 
			
		||||
            f.write(text)
 | 
			
		||||
            f.write("\n")
 | 
			
		||||
 | 
			
		||||
    text += "\n"
 | 
			
		||||
    with StringIO(text) as fp:
 | 
			
		||||
        return FileResponse(
 | 
			
		||||
            open(fp.name, "rb"), as_attachment=True, filename="linux_agent_install.sh"
 | 
			
		||||
            fp.read(), as_attachment=True, filename="linux_agent_install.sh"
 | 
			
		||||
        )
 | 
			
		||||
 
 | 
			
		||||
@@ -1,54 +1,71 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
import datetime as dt
 | 
			
		||||
import os
 | 
			
		||||
import random
 | 
			
		||||
import string
 | 
			
		||||
import time
 | 
			
		||||
from meshctrl.utils import get_login_token
 | 
			
		||||
from io import StringIO
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
 | 
			
		||||
from core.models import CodeSignToken
 | 
			
		||||
from core.utils import (
 | 
			
		||||
    get_mesh_ws_url,
 | 
			
		||||
    remove_mesh_agent,
 | 
			
		||||
    send_command_with_mesh,
 | 
			
		||||
    get_core_settings,
 | 
			
		||||
)
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.db.models import Q, Prefetch, Exists, Count, OuterRef
 | 
			
		||||
from django.db.models import Exists, OuterRef, Prefetch, Q
 | 
			
		||||
from django.http import HttpResponse
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from logs.models import AuditLog, DebugLog, PendingAction
 | 
			
		||||
from django.utils.dateparse import parse_datetime
 | 
			
		||||
from meshctrl.utils import get_login_token
 | 
			
		||||
from packaging import version as pyver
 | 
			
		||||
from rest_framework import serializers
 | 
			
		||||
from rest_framework.decorators import api_view, permission_classes
 | 
			
		||||
from rest_framework.exceptions import PermissionDenied
 | 
			
		||||
from rest_framework.permissions import IsAuthenticated
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
from scripts.models import Script
 | 
			
		||||
from scripts.tasks import handle_bulk_command_task, handle_bulk_script_task
 | 
			
		||||
from winupdate.serializers import WinUpdatePolicySerializer
 | 
			
		||||
from winupdate.tasks import bulk_check_for_updates_task, bulk_install_updates_task
 | 
			
		||||
from winupdate.models import WinUpdate
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.constants import AGENT_DEFER
 | 
			
		||||
from core.tasks import sync_mesh_perms_task
 | 
			
		||||
from core.utils import (
 | 
			
		||||
    get_core_settings,
 | 
			
		||||
    get_mesh_ws_url,
 | 
			
		||||
    remove_mesh_agent,
 | 
			
		||||
    token_is_valid,
 | 
			
		||||
    wake_on_lan,
 | 
			
		||||
)
 | 
			
		||||
from logs.models import AuditLog, DebugLog, PendingAction
 | 
			
		||||
from scripts.models import Script
 | 
			
		||||
from scripts.tasks import bulk_command_task, bulk_script_task
 | 
			
		||||
from tacticalrmm.constants import (
 | 
			
		||||
    AGENT_DEFER,
 | 
			
		||||
    AGENT_STATUS_OFFLINE,
 | 
			
		||||
    AGENT_STATUS_ONLINE,
 | 
			
		||||
    AGENT_TABLE_DEFER,
 | 
			
		||||
    AgentHistoryType,
 | 
			
		||||
    AgentMonType,
 | 
			
		||||
    AgentPlat,
 | 
			
		||||
    CustomFieldModel,
 | 
			
		||||
    DebugLogType,
 | 
			
		||||
    EvtLogNames,
 | 
			
		||||
    PAAction,
 | 
			
		||||
)
 | 
			
		||||
from tacticalrmm.helpers import date_is_in_past, notify_error
 | 
			
		||||
from tacticalrmm.permissions import (
 | 
			
		||||
    _has_perm_on_agent,
 | 
			
		||||
    _has_perm_on_client,
 | 
			
		||||
    _has_perm_on_site,
 | 
			
		||||
)
 | 
			
		||||
from tacticalrmm.utils import get_default_timezone, notify_error, reload_nats
 | 
			
		||||
from tacticalrmm.utils import get_default_timezone, reload_nats
 | 
			
		||||
from winupdate.models import WinUpdate, WinUpdatePolicy
 | 
			
		||||
from winupdate.serializers import WinUpdatePolicySerializer
 | 
			
		||||
from winupdate.tasks import bulk_check_for_updates_task, bulk_install_updates_task
 | 
			
		||||
 | 
			
		||||
from .models import Agent, AgentCustomField, AgentHistory, Note
 | 
			
		||||
from .permissions import (
 | 
			
		||||
    AgentHistoryPerms,
 | 
			
		||||
    AgentNotesPerms,
 | 
			
		||||
    AgentPerms,
 | 
			
		||||
    AgentWOLPerms,
 | 
			
		||||
    EvtLogPerms,
 | 
			
		||||
    InstallAgentPerms,
 | 
			
		||||
    ManageProcPerms,
 | 
			
		||||
    MeshPerms,
 | 
			
		||||
    PingAgentPerms,
 | 
			
		||||
    RebootAgentPerms,
 | 
			
		||||
    RecoverAgentPerms,
 | 
			
		||||
    RunBulkPerms,
 | 
			
		||||
@@ -64,7 +81,11 @@ from .serializers import (
 | 
			
		||||
    AgentSerializer,
 | 
			
		||||
    AgentTableSerializer,
 | 
			
		||||
)
 | 
			
		||||
from .tasks import run_script_email_results_task, send_agent_update_task
 | 
			
		||||
from .tasks import (
 | 
			
		||||
    bulk_recover_agents_task,
 | 
			
		||||
    run_script_email_results_task,
 | 
			
		||||
    send_agent_update_task,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetAgents(APIView):
 | 
			
		||||
@@ -78,7 +99,7 @@ class GetAgents(APIView):
 | 
			
		||||
 | 
			
		||||
        monitoring_type = request.query_params.get("monitoring_type", None)
 | 
			
		||||
        if monitoring_type:
 | 
			
		||||
            if monitoring_type in ["server", "workstation"]:
 | 
			
		||||
            if monitoring_type in AgentMonType.values:
 | 
			
		||||
                monitoring_type_filter = Q(monitoring_type=monitoring_type)
 | 
			
		||||
            else:
 | 
			
		||||
                return notify_error("monitoring type does not exist")
 | 
			
		||||
@@ -98,7 +119,7 @@ class GetAgents(APIView):
 | 
			
		||||
                Agent.objects.filter_by_role(request.user)  # type: ignore
 | 
			
		||||
                .filter(monitoring_type_filter)
 | 
			
		||||
                .filter(client_site_filter)
 | 
			
		||||
                .defer(*AGENT_DEFER)
 | 
			
		||||
                .defer(*AGENT_TABLE_DEFER)
 | 
			
		||||
                .select_related(
 | 
			
		||||
                    "site__server_policy",
 | 
			
		||||
                    "site__workstation_policy",
 | 
			
		||||
@@ -116,18 +137,17 @@ class GetAgents(APIView):
 | 
			
		||||
                        "checkresults",
 | 
			
		||||
                        queryset=CheckResult.objects.select_related("assigned_check"),
 | 
			
		||||
                    ),
 | 
			
		||||
                )
 | 
			
		||||
                .annotate(
 | 
			
		||||
                    pending_actions_count=Count(
 | 
			
		||||
                        "pendingactions", filter=Q(pendingactions__status="pending")
 | 
			
		||||
                    )
 | 
			
		||||
                    Prefetch(
 | 
			
		||||
                        "custom_fields",
 | 
			
		||||
                        queryset=AgentCustomField.objects.select_related("field"),
 | 
			
		||||
                    ),
 | 
			
		||||
                )
 | 
			
		||||
                .annotate(
 | 
			
		||||
                    has_patches_pending=Exists(
 | 
			
		||||
                        WinUpdate.objects.filter(
 | 
			
		||||
                            agent_id=OuterRef("pk"), action="approve", installed=False
 | 
			
		||||
                        )
 | 
			
		||||
                    )
 | 
			
		||||
                    ),
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            serializer = AgentTableSerializer(agents, many=True)
 | 
			
		||||
@@ -136,10 +156,10 @@ class GetAgents(APIView):
 | 
			
		||||
        else:
 | 
			
		||||
            agents = (
 | 
			
		||||
                Agent.objects.filter_by_role(request.user)  # type: ignore
 | 
			
		||||
                .select_related("site")
 | 
			
		||||
                .defer(*AGENT_DEFER)
 | 
			
		||||
                .select_related("site__client")
 | 
			
		||||
                .filter(monitoring_type_filter)
 | 
			
		||||
                .filter(client_site_filter)
 | 
			
		||||
                .only("agent_id", "hostname", "site")
 | 
			
		||||
            )
 | 
			
		||||
            serializer = AgentHostnameSerializer(agents, many=True)
 | 
			
		||||
 | 
			
		||||
@@ -149,18 +169,66 @@ class GetAgents(APIView):
 | 
			
		||||
class GetUpdateDeleteAgent(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, AgentPerms]
 | 
			
		||||
 | 
			
		||||
    class InputSerializer(serializers.ModelSerializer):
 | 
			
		||||
        class Meta:
 | 
			
		||||
            model = Agent
 | 
			
		||||
            fields = [
 | 
			
		||||
                "maintenance_mode",  # TODO separate this
 | 
			
		||||
                "policy",  # TODO separate this
 | 
			
		||||
                "block_policy_inheritance",  # TODO separate this
 | 
			
		||||
                "monitoring_type",
 | 
			
		||||
                "description",
 | 
			
		||||
                "overdue_email_alert",
 | 
			
		||||
                "overdue_text_alert",
 | 
			
		||||
                "overdue_dashboard_alert",
 | 
			
		||||
                "offline_time",
 | 
			
		||||
                "overdue_time",
 | 
			
		||||
                "check_interval",
 | 
			
		||||
                "time_zone",
 | 
			
		||||
                "site",
 | 
			
		||||
            ]
 | 
			
		||||
 | 
			
		||||
    # get agent details
 | 
			
		||||
    def get(self, request, agent_id):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=agent_id)
 | 
			
		||||
        from checks.models import Check, CheckResult
 | 
			
		||||
 | 
			
		||||
        agent = get_object_or_404(
 | 
			
		||||
            Agent.objects.select_related(
 | 
			
		||||
                "site__server_policy",
 | 
			
		||||
                "site__workstation_policy",
 | 
			
		||||
                "site__client__server_policy",
 | 
			
		||||
                "site__client__workstation_policy",
 | 
			
		||||
                "policy",
 | 
			
		||||
                "alert_template",
 | 
			
		||||
            ).prefetch_related(
 | 
			
		||||
                Prefetch(
 | 
			
		||||
                    "agentchecks",
 | 
			
		||||
                    queryset=Check.objects.select_related("script"),
 | 
			
		||||
                ),
 | 
			
		||||
                Prefetch(
 | 
			
		||||
                    "checkresults",
 | 
			
		||||
                    queryset=CheckResult.objects.select_related("assigned_check"),
 | 
			
		||||
                ),
 | 
			
		||||
                Prefetch(
 | 
			
		||||
                    "custom_fields",
 | 
			
		||||
                    queryset=AgentCustomField.objects.select_related("field"),
 | 
			
		||||
                ),
 | 
			
		||||
                Prefetch(
 | 
			
		||||
                    "winupdatepolicy",
 | 
			
		||||
                    queryset=WinUpdatePolicy.objects.select_related("agent", "policy"),
 | 
			
		||||
                ),
 | 
			
		||||
            ),
 | 
			
		||||
            agent_id=agent_id,
 | 
			
		||||
        )
 | 
			
		||||
        return Response(AgentSerializer(agent).data)
 | 
			
		||||
 | 
			
		||||
    # edit agent
 | 
			
		||||
    def put(self, request, agent_id):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=agent_id)
 | 
			
		||||
 | 
			
		||||
        a_serializer = AgentSerializer(instance=agent, data=request.data, partial=True)
 | 
			
		||||
        a_serializer.is_valid(raise_exception=True)
 | 
			
		||||
        a_serializer.save()
 | 
			
		||||
        s = self.InputSerializer(instance=agent, data=request.data, partial=True)
 | 
			
		||||
        s.is_valid(raise_exception=True)
 | 
			
		||||
        s.save()
 | 
			
		||||
 | 
			
		||||
        if "winupdatepolicy" in request.data.keys():
 | 
			
		||||
            policy = agent.winupdatepolicy.get()  # type: ignore
 | 
			
		||||
@@ -171,9 +239,7 @@ class GetUpdateDeleteAgent(APIView):
 | 
			
		||||
            p_serializer.save()
 | 
			
		||||
 | 
			
		||||
        if "custom_fields" in request.data.keys():
 | 
			
		||||
 | 
			
		||||
            for field in request.data["custom_fields"]:
 | 
			
		||||
 | 
			
		||||
                custom_field = field
 | 
			
		||||
                custom_field["agent"] = agent.pk
 | 
			
		||||
 | 
			
		||||
@@ -193,24 +259,33 @@ class GetUpdateDeleteAgent(APIView):
 | 
			
		||||
                    serializer.is_valid(raise_exception=True)
 | 
			
		||||
                    serializer.save()
 | 
			
		||||
 | 
			
		||||
        sync_mesh_perms_task.delay()
 | 
			
		||||
        return Response("The agent was updated successfully")
 | 
			
		||||
 | 
			
		||||
    # uninstall agent
 | 
			
		||||
    def delete(self, request, agent_id):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=agent_id)
 | 
			
		||||
 | 
			
		||||
        code = "foo"
 | 
			
		||||
        if agent.plat == "linux":
 | 
			
		||||
            with open(settings.LINUX_AGENT_SCRIPT, "r") as f:
 | 
			
		||||
                code = f.read()
 | 
			
		||||
        code = "foo"  # stub for windows
 | 
			
		||||
        if agent.plat == AgentPlat.LINUX:
 | 
			
		||||
            code = Path(settings.LINUX_AGENT_SCRIPT).read_text()
 | 
			
		||||
        elif agent.plat == AgentPlat.DARWIN:
 | 
			
		||||
            code = Path(settings.MAC_UNINSTALL).read_text()
 | 
			
		||||
 | 
			
		||||
        asyncio.run(agent.nats_cmd({"func": "uninstall", "code": code}, wait=False))
 | 
			
		||||
        name = agent.hostname
 | 
			
		||||
        mesh_id = agent.mesh_node_id
 | 
			
		||||
        agent.delete()
 | 
			
		||||
        reload_nats()
 | 
			
		||||
        uri = get_mesh_ws_url()
 | 
			
		||||
        asyncio.run(remove_mesh_agent(uri, mesh_id))
 | 
			
		||||
        try:
 | 
			
		||||
            uri = get_mesh_ws_url()
 | 
			
		||||
            asyncio.run(remove_mesh_agent(uri, mesh_id))
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            DebugLog.error(
 | 
			
		||||
                message=f"Unable to remove agent {name} from meshcentral database: {e}",
 | 
			
		||||
                log_type=DebugLogType.AGENT_ISSUES,
 | 
			
		||||
            )
 | 
			
		||||
        sync_mesh_perms_task.delay()
 | 
			
		||||
        return Response(f"{name} will now be uninstalled.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -226,7 +301,7 @@ class AgentProcesses(APIView):
 | 
			
		||||
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=agent_id)
 | 
			
		||||
        r = asyncio.run(agent.nats_cmd(data={"func": "procs"}, timeout=5))
 | 
			
		||||
        if r == "timeout" or r == "natsdown":
 | 
			
		||||
        if r in ("timeout", "natsdown"):
 | 
			
		||||
            return notify_error("Unable to contact the agent")
 | 
			
		||||
        return Response(r)
 | 
			
		||||
 | 
			
		||||
@@ -237,7 +312,7 @@ class AgentProcesses(APIView):
 | 
			
		||||
            agent.nats_cmd({"func": "killproc", "procpid": int(pid)}, timeout=15)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        if r == "timeout" or r == "natsdown":
 | 
			
		||||
        if r in ("timeout", "natsdown"):
 | 
			
		||||
            return notify_error("Unable to contact the agent")
 | 
			
		||||
        elif r != "ok":
 | 
			
		||||
            return notify_error(r)
 | 
			
		||||
@@ -253,13 +328,13 @@ class AgentMeshCentral(APIView):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=agent_id)
 | 
			
		||||
        core = get_core_settings()
 | 
			
		||||
 | 
			
		||||
        if not core.mesh_disable_auto_login:
 | 
			
		||||
            token = get_login_token(
 | 
			
		||||
                key=core.mesh_token, user=f"user//{core.mesh_username}"
 | 
			
		||||
            )
 | 
			
		||||
            token_param = f"login={token}&"
 | 
			
		||||
        else:
 | 
			
		||||
            token_param = ""
 | 
			
		||||
        user = (
 | 
			
		||||
            request.user.mesh_user_id
 | 
			
		||||
            if core.sync_mesh_with_trmm
 | 
			
		||||
            else f"user//{core.mesh_api_superuser}"
 | 
			
		||||
        )
 | 
			
		||||
        token = get_login_token(key=core.mesh_token, user=user)
 | 
			
		||||
        token_param = f"login={token}&"
 | 
			
		||||
 | 
			
		||||
        control = f"{core.mesh_site}/?{token_param}gotonode={agent.mesh_node_id}&viewmode=11&hide=31"
 | 
			
		||||
        terminal = f"{core.mesh_site}/?{token_param}gotonode={agent.mesh_node_id}&viewmode=12&hide=31"
 | 
			
		||||
@@ -297,9 +372,9 @@ class AgentMeshCentral(APIView):
 | 
			
		||||
@permission_classes([IsAuthenticated, AgentPerms])
 | 
			
		||||
def get_agent_versions(request):
 | 
			
		||||
    agents = (
 | 
			
		||||
        Agent.objects.filter_by_role(request.user)  # type: ignore
 | 
			
		||||
        .prefetch_related("site")
 | 
			
		||||
        .only("pk", "hostname")
 | 
			
		||||
        Agent.objects.defer(*AGENT_DEFER)
 | 
			
		||||
        .filter_by_role(request.user)  # type: ignore
 | 
			
		||||
        .select_related("site__client")
 | 
			
		||||
    )
 | 
			
		||||
    return Response(
 | 
			
		||||
        {
 | 
			
		||||
@@ -322,20 +397,22 @@ def update_agents(request):
 | 
			
		||||
        for i in q
 | 
			
		||||
        if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
 | 
			
		||||
    ]
 | 
			
		||||
    send_agent_update_task.delay(agent_ids=agent_ids)
 | 
			
		||||
 | 
			
		||||
    token, _ = token_is_valid()
 | 
			
		||||
    send_agent_update_task.delay(agent_ids=agent_ids, token=token, force=False)
 | 
			
		||||
    return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view(["GET"])
 | 
			
		||||
@permission_classes([IsAuthenticated, PingAgentPerms])
 | 
			
		||||
@permission_classes([IsAuthenticated, AgentPerms])
 | 
			
		||||
def ping(request, agent_id):
 | 
			
		||||
    agent = get_object_or_404(Agent, agent_id=agent_id)
 | 
			
		||||
    status = "offline"
 | 
			
		||||
    status = AGENT_STATUS_OFFLINE
 | 
			
		||||
    attempts = 0
 | 
			
		||||
    while 1:
 | 
			
		||||
        r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=2))
 | 
			
		||||
        if r == "pong":
 | 
			
		||||
            status = "online"
 | 
			
		||||
            status = AGENT_STATUS_ONLINE
 | 
			
		||||
            break
 | 
			
		||||
        else:
 | 
			
		||||
            attempts += 1
 | 
			
		||||
@@ -356,7 +433,7 @@ def get_event_log(request, agent_id, logtype, days):
 | 
			
		||||
        return demo_get_eventlog()
 | 
			
		||||
 | 
			
		||||
    agent = get_object_or_404(Agent, agent_id=agent_id)
 | 
			
		||||
    timeout = 180 if logtype == "Security" else 30
 | 
			
		||||
    timeout = 180 if logtype == EvtLogNames.SECURITY else 30
 | 
			
		||||
 | 
			
		||||
    data = {
 | 
			
		||||
        "func": "eventlog",
 | 
			
		||||
@@ -367,7 +444,7 @@ def get_event_log(request, agent_id, logtype, days):
 | 
			
		||||
        },
 | 
			
		||||
    }
 | 
			
		||||
    r = asyncio.run(agent.nats_cmd(data, timeout=timeout + 2))
 | 
			
		||||
    if r == "timeout" or r == "natsdown":
 | 
			
		||||
    if r in ("timeout", "natsdown"):
 | 
			
		||||
        return notify_error("Unable to contact the agent")
 | 
			
		||||
 | 
			
		||||
    return Response(r)
 | 
			
		||||
@@ -390,11 +467,12 @@ def send_raw_cmd(request, agent_id):
 | 
			
		||||
            "command": request.data["cmd"],
 | 
			
		||||
            "shell": shell,
 | 
			
		||||
        },
 | 
			
		||||
        "run_as_user": request.data["run_as_user"],
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    hist = AgentHistory.objects.create(
 | 
			
		||||
        agent=agent,
 | 
			
		||||
        type="cmd_run",
 | 
			
		||||
        type=AgentHistoryType.CMD_RUN,
 | 
			
		||||
        command=request.data["cmd"],
 | 
			
		||||
        username=request.user.username[:50],
 | 
			
		||||
    )
 | 
			
		||||
@@ -416,8 +494,22 @@ def send_raw_cmd(request, agent_id):
 | 
			
		||||
    return Response(r)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Shutdown(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, RebootAgentPerms]
 | 
			
		||||
 | 
			
		||||
    # shutdown
 | 
			
		||||
    def post(self, request, agent_id):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=agent_id)
 | 
			
		||||
        r = asyncio.run(agent.nats_cmd({"func": "shutdown"}, timeout=10))
 | 
			
		||||
        if r != "ok":
 | 
			
		||||
            return notify_error("Unable to contact the agent")
 | 
			
		||||
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Reboot(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, RebootAgentPerms]
 | 
			
		||||
 | 
			
		||||
    # reboot now
 | 
			
		||||
    def post(self, request, agent_id):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=agent_id)
 | 
			
		||||
@@ -430,12 +522,17 @@ class Reboot(APIView):
 | 
			
		||||
    # reboot later
 | 
			
		||||
    def patch(self, request, agent_id):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=agent_id)
 | 
			
		||||
        if agent.is_posix:
 | 
			
		||||
            return notify_error(f"Not currently implemented for {agent.plat}")
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            obj = dt.datetime.strptime(request.data["datetime"], "%Y-%m-%dT%H:%M:%S")
 | 
			
		||||
            obj = dt.datetime.strptime(request.data["datetime"], "%Y-%m-%dT%H:%M")
 | 
			
		||||
        except Exception:
 | 
			
		||||
            return notify_error("Invalid date")
 | 
			
		||||
 | 
			
		||||
        if date_is_in_past(datetime_obj=obj, agent_tz=agent.timezone):
 | 
			
		||||
            return notify_error("Date cannot be set in the past")
 | 
			
		||||
 | 
			
		||||
        task_name = "TacticalRMM_SchedReboot_" + "".join(
 | 
			
		||||
            random.choice(string.ascii_letters) for _ in range(10)
 | 
			
		||||
        )
 | 
			
		||||
@@ -471,7 +568,7 @@ class Reboot(APIView):
 | 
			
		||||
 | 
			
		||||
        details = {"taskname": task_name, "time": str(obj)}
 | 
			
		||||
        PendingAction.objects.create(
 | 
			
		||||
            agent=agent, action_type="schedreboot", details=details
 | 
			
		||||
            agent=agent, action_type=PAAction.SCHED_REBOOT, details=details
 | 
			
		||||
        )
 | 
			
		||||
        nice_time = dt.datetime.strftime(obj, "%B %d, %Y at %I:%M %p")
 | 
			
		||||
        return Response(
 | 
			
		||||
@@ -482,34 +579,69 @@ class Reboot(APIView):
 | 
			
		||||
@api_view(["POST"])
 | 
			
		||||
@permission_classes([IsAuthenticated, InstallAgentPerms])
 | 
			
		||||
def install_agent(request):
 | 
			
		||||
    from knox.models import AuthToken
 | 
			
		||||
 | 
			
		||||
    from accounts.models import User
 | 
			
		||||
    from agents.utils import get_agent_url
 | 
			
		||||
    from knox.models import AuthToken
 | 
			
		||||
    from core.utils import token_is_valid
 | 
			
		||||
 | 
			
		||||
    insecure = getattr(settings, "TRMM_INSECURE", False)
 | 
			
		||||
 | 
			
		||||
    if insecure and request.data["installMethod"] in {"exe", "powershell"}:
 | 
			
		||||
        return notify_error(
 | 
			
		||||
            "Not available in insecure mode. Please use the 'Manual' method."
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    # TODO rework this ghetto validation hack
 | 
			
		||||
    # https://github.com/amidaware/tacticalrmm/issues/1461
 | 
			
		||||
    try:
 | 
			
		||||
        int(request.data["expires"])
 | 
			
		||||
    except ValueError:
 | 
			
		||||
        return notify_error("Please enter a valid number of hours")
 | 
			
		||||
 | 
			
		||||
    client_id = request.data["client"]
 | 
			
		||||
    site_id = request.data["site"]
 | 
			
		||||
    version = settings.LATEST_AGENT_VER
 | 
			
		||||
    arch = request.data["arch"]
 | 
			
		||||
    goarch = request.data["goarch"]
 | 
			
		||||
    plat = request.data["plat"]
 | 
			
		||||
 | 
			
		||||
    if not _has_perm_on_site(request.user, site_id):
 | 
			
		||||
        raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
    inno = (
 | 
			
		||||
        f"winagent-v{version}.exe" if arch == "64" else f"winagent-v{version}-x86.exe"
 | 
			
		||||
    )
 | 
			
		||||
    if request.data["installMethod"] == "linux":
 | 
			
		||||
        plat = "linux"
 | 
			
		||||
    else:
 | 
			
		||||
        plat = "windows"
 | 
			
		||||
    codesign_token, is_valid = token_is_valid()
 | 
			
		||||
 | 
			
		||||
    download_url = get_agent_url(arch, plat)
 | 
			
		||||
    if request.data["installMethod"] in {"bash", "mac"} and not is_valid:
 | 
			
		||||
        return notify_error(
 | 
			
		||||
            "Linux/Mac agents require code signing. Please see https://docs.tacticalrmm.com/code_signing/ for more info."
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    inno = f"tacticalagent-v{version}-{plat}-{goarch}"
 | 
			
		||||
    if plat == AgentPlat.WINDOWS:
 | 
			
		||||
        inno += ".exe"
 | 
			
		||||
 | 
			
		||||
    download_url = get_agent_url(goarch=goarch, plat=plat, token=codesign_token)
 | 
			
		||||
 | 
			
		||||
    installer_user = User.objects.filter(is_installer_user=True).first()
 | 
			
		||||
 | 
			
		||||
    _, token = AuthToken.objects.create(
 | 
			
		||||
        user=installer_user, expiry=dt.timedelta(hours=request.data["expires"])
 | 
			
		||||
        user=installer_user, expiry=dt.timedelta(hours=int(request.data["expires"]))
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    install_flags = [
 | 
			
		||||
        "-m",
 | 
			
		||||
        "install",
 | 
			
		||||
        "--api",
 | 
			
		||||
        request.data["api"],
 | 
			
		||||
        "--client-id",
 | 
			
		||||
        client_id,
 | 
			
		||||
        "--site-id",
 | 
			
		||||
        site_id,
 | 
			
		||||
        "--agent-type",
 | 
			
		||||
        request.data["agenttype"],
 | 
			
		||||
        "--auth",
 | 
			
		||||
        token,
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    if request.data["installMethod"] == "exe":
 | 
			
		||||
        from tacticalrmm.utils import generate_winagent_exe
 | 
			
		||||
 | 
			
		||||
@@ -520,82 +652,68 @@ def install_agent(request):
 | 
			
		||||
            rdp=request.data["rdp"],
 | 
			
		||||
            ping=request.data["ping"],
 | 
			
		||||
            power=request.data["power"],
 | 
			
		||||
            arch=arch,
 | 
			
		||||
            goarch=goarch,
 | 
			
		||||
            token=token,
 | 
			
		||||
            api=request.data["api"],
 | 
			
		||||
            file_name=request.data["fileName"],
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    elif request.data["installMethod"] == "linux":
 | 
			
		||||
        # TODO
 | 
			
		||||
        # linux agents are in beta for now, only available for sponsors for testing
 | 
			
		||||
        # remove this after it's out of beta
 | 
			
		||||
 | 
			
		||||
        code_token = CodeSignToken.objects.first()
 | 
			
		||||
        if not code_token:
 | 
			
		||||
            return notify_error("Missing code signing token")
 | 
			
		||||
 | 
			
		||||
        if not code_token.is_valid:
 | 
			
		||||
            return notify_error("Code signing token is not valid")
 | 
			
		||||
 | 
			
		||||
    elif request.data["installMethod"] == "bash":
 | 
			
		||||
        from agents.utils import generate_linux_install
 | 
			
		||||
 | 
			
		||||
        return generate_linux_install(
 | 
			
		||||
            client=str(client_id),
 | 
			
		||||
            site=str(site_id),
 | 
			
		||||
            agent_type=request.data["agenttype"],
 | 
			
		||||
            arch=arch,
 | 
			
		||||
            arch=goarch,
 | 
			
		||||
            token=token,
 | 
			
		||||
            api=request.data["api"],
 | 
			
		||||
            download_url=download_url,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    elif request.data["installMethod"] == "manual":
 | 
			
		||||
        cmd = [
 | 
			
		||||
            inno,
 | 
			
		||||
            "/VERYSILENT",
 | 
			
		||||
            "/SUPPRESSMSGBOXES",
 | 
			
		||||
            "&&",
 | 
			
		||||
            "ping",
 | 
			
		||||
            "127.0.0.1",
 | 
			
		||||
            "-n",
 | 
			
		||||
            "5",
 | 
			
		||||
            "&&",
 | 
			
		||||
            r'"C:\Program Files\TacticalAgent\tacticalrmm.exe"',
 | 
			
		||||
            "-m",
 | 
			
		||||
            "install",
 | 
			
		||||
            "--api",
 | 
			
		||||
            request.data["api"],
 | 
			
		||||
            "--client-id",
 | 
			
		||||
            client_id,
 | 
			
		||||
            "--site-id",
 | 
			
		||||
            site_id,
 | 
			
		||||
            "--agent-type",
 | 
			
		||||
            request.data["agenttype"],
 | 
			
		||||
            "--auth",
 | 
			
		||||
            token,
 | 
			
		||||
        ]
 | 
			
		||||
    elif request.data["installMethod"] in {"manual", "mac"}:
 | 
			
		||||
        resp = {}
 | 
			
		||||
        if request.data["installMethod"] == "manual":
 | 
			
		||||
            cmd = [
 | 
			
		||||
                inno,
 | 
			
		||||
                "/VERYSILENT",
 | 
			
		||||
                "/SUPPRESSMSGBOXES",
 | 
			
		||||
                "&&",
 | 
			
		||||
                "ping",
 | 
			
		||||
                "127.0.0.1",
 | 
			
		||||
                "-n",
 | 
			
		||||
                "5",
 | 
			
		||||
                "&&",
 | 
			
		||||
                r'"C:\Program Files\TacticalAgent\tacticalrmm.exe"',
 | 
			
		||||
            ] + install_flags
 | 
			
		||||
 | 
			
		||||
        if int(request.data["rdp"]):
 | 
			
		||||
            cmd.append("--rdp")
 | 
			
		||||
        if int(request.data["ping"]):
 | 
			
		||||
            cmd.append("--ping")
 | 
			
		||||
        if int(request.data["power"]):
 | 
			
		||||
            cmd.append("--power")
 | 
			
		||||
            if int(request.data["rdp"]):
 | 
			
		||||
                cmd.append("--rdp")
 | 
			
		||||
            if int(request.data["ping"]):
 | 
			
		||||
                cmd.append("--ping")
 | 
			
		||||
            if int(request.data["power"]):
 | 
			
		||||
                cmd.append("--power")
 | 
			
		||||
 | 
			
		||||
        resp = {
 | 
			
		||||
            "cmd": " ".join(str(i) for i in cmd),
 | 
			
		||||
            "url": download_url,
 | 
			
		||||
        }
 | 
			
		||||
            if insecure:
 | 
			
		||||
                cmd.append("--insecure")
 | 
			
		||||
 | 
			
		||||
            resp["cmd"] = " ".join(str(i) for i in cmd)
 | 
			
		||||
        else:
 | 
			
		||||
            install_flags.insert(0, f"sudo ./{inno}")
 | 
			
		||||
            cmd = install_flags.copy()
 | 
			
		||||
            dl = f"curl -L -o {inno} '{download_url}'"
 | 
			
		||||
            resp["cmd"] = (
 | 
			
		||||
                dl + f" && chmod +x {inno} && " + " ".join(str(i) for i in cmd)
 | 
			
		||||
            )
 | 
			
		||||
            if insecure:
 | 
			
		||||
                resp["cmd"] += " --insecure"
 | 
			
		||||
 | 
			
		||||
        resp["url"] = download_url
 | 
			
		||||
 | 
			
		||||
        return Response(resp)
 | 
			
		||||
 | 
			
		||||
    elif request.data["installMethod"] == "powershell":
 | 
			
		||||
 | 
			
		||||
        ps = os.path.join(settings.BASE_DIR, "core/installer.ps1")
 | 
			
		||||
 | 
			
		||||
        with open(ps, "r") as f:
 | 
			
		||||
            text = f.read()
 | 
			
		||||
        text = Path(settings.BASE_DIR / "core" / "installer.ps1").read_text()
 | 
			
		||||
 | 
			
		||||
        replace_dict = {
 | 
			
		||||
            "innosetupchange": inno,
 | 
			
		||||
@@ -613,54 +731,31 @@ def install_agent(request):
 | 
			
		||||
        for i, j in replace_dict.items():
 | 
			
		||||
            text = text.replace(i, j)
 | 
			
		||||
 | 
			
		||||
        file_name = "rmm-installer.ps1"
 | 
			
		||||
        ps1 = os.path.join(settings.EXE_DIR, file_name)
 | 
			
		||||
 | 
			
		||||
        if os.path.exists(ps1):
 | 
			
		||||
            try:
 | 
			
		||||
                os.remove(ps1)
 | 
			
		||||
            except Exception as e:
 | 
			
		||||
                DebugLog.error(message=str(e))
 | 
			
		||||
 | 
			
		||||
        with open(ps1, "w") as f:
 | 
			
		||||
            f.write(text)
 | 
			
		||||
 | 
			
		||||
        if settings.DEBUG:
 | 
			
		||||
            with open(ps1, "r") as f:
 | 
			
		||||
                response = HttpResponse(f.read(), content_type="text/plain")
 | 
			
		||||
                response["Content-Disposition"] = f"inline; filename={file_name}"
 | 
			
		||||
                return response
 | 
			
		||||
        else:
 | 
			
		||||
            response = HttpResponse()
 | 
			
		||||
            response["Content-Disposition"] = f"attachment; filename={file_name}"
 | 
			
		||||
            response["X-Accel-Redirect"] = f"/private/exe/{file_name}"
 | 
			
		||||
        with StringIO(text) as fp:
 | 
			
		||||
            response = HttpResponse(fp.read(), content_type="text/plain")
 | 
			
		||||
            response["Content-Disposition"] = "attachment; filename=rmm-installer.ps1"
 | 
			
		||||
            return response
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view(["POST"])
 | 
			
		||||
@permission_classes([IsAuthenticated, RecoverAgentPerms])
 | 
			
		||||
def recover(request, agent_id):
 | 
			
		||||
    agent = get_object_or_404(Agent, agent_id=agent_id)
 | 
			
		||||
def recover(request, agent_id: str) -> Response:
 | 
			
		||||
    agent: Agent = get_object_or_404(
 | 
			
		||||
        Agent.objects.defer(*AGENT_DEFER), agent_id=agent_id
 | 
			
		||||
    )
 | 
			
		||||
    mode = request.data["mode"]
 | 
			
		||||
 | 
			
		||||
    if mode == "tacagent":
 | 
			
		||||
        if agent.is_posix:
 | 
			
		||||
            cmd = "systemctl restart tacticalagent.service"
 | 
			
		||||
            shell = 3
 | 
			
		||||
        else:
 | 
			
		||||
            cmd = "net stop tacticalrmm & taskkill /F /IM tacticalrmm.exe & net start tacticalrmm"
 | 
			
		||||
            shell = 1
 | 
			
		||||
        uri = get_mesh_ws_url()
 | 
			
		||||
        asyncio.run(send_command_with_mesh(cmd, uri, agent.mesh_node_id, shell, 0))
 | 
			
		||||
        agent.recover(mode, uri, wait=False)
 | 
			
		||||
        return Response("Recovery will be attempted shortly")
 | 
			
		||||
 | 
			
		||||
    elif mode == "mesh":
 | 
			
		||||
        data = {"func": "recover", "payload": {"mode": mode}}
 | 
			
		||||
        r = asyncio.run(agent.nats_cmd(data, timeout=20))
 | 
			
		||||
        if r == "ok":
 | 
			
		||||
            return Response("Successfully completed recovery")
 | 
			
		||||
        r, err = agent.recover(mode, "")
 | 
			
		||||
        if err:
 | 
			
		||||
            return notify_error(f"Unable to complete recovery: {r}")
 | 
			
		||||
 | 
			
		||||
    return notify_error("Something went wrong")
 | 
			
		||||
    return Response("Successfully completed recovery")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view(["POST"])
 | 
			
		||||
@@ -670,6 +765,8 @@ def run_script(request, agent_id):
 | 
			
		||||
    script = get_object_or_404(Script, pk=request.data["script"])
 | 
			
		||||
    output = request.data["output"]
 | 
			
		||||
    args = request.data["args"]
 | 
			
		||||
    run_as_user: bool = request.data["run_as_user"]
 | 
			
		||||
    env_vars: list[str] = request.data["env_vars"]
 | 
			
		||||
    req_timeout = int(request.data["timeout"]) + 3
 | 
			
		||||
 | 
			
		||||
    AuditLog.audit_script_run(
 | 
			
		||||
@@ -681,7 +778,7 @@ def run_script(request, agent_id):
 | 
			
		||||
 | 
			
		||||
    hist = AgentHistory.objects.create(
 | 
			
		||||
        agent=agent,
 | 
			
		||||
        type="script_run",
 | 
			
		||||
        type=AgentHistoryType.SCRIPT_RUN,
 | 
			
		||||
        script=script,
 | 
			
		||||
        username=request.user.username[:50],
 | 
			
		||||
    )
 | 
			
		||||
@@ -694,6 +791,8 @@ def run_script(request, agent_id):
 | 
			
		||||
            timeout=req_timeout,
 | 
			
		||||
            wait=True,
 | 
			
		||||
            history_pk=history_pk,
 | 
			
		||||
            run_as_user=run_as_user,
 | 
			
		||||
            env_vars=env_vars,
 | 
			
		||||
        )
 | 
			
		||||
        return Response(r)
 | 
			
		||||
 | 
			
		||||
@@ -707,6 +806,9 @@ def run_script(request, agent_id):
 | 
			
		||||
            nats_timeout=req_timeout,
 | 
			
		||||
            emails=emails,
 | 
			
		||||
            args=args,
 | 
			
		||||
            history_pk=history_pk,
 | 
			
		||||
            run_as_user=run_as_user,
 | 
			
		||||
            env_vars=env_vars,
 | 
			
		||||
        )
 | 
			
		||||
    elif output == "collector":
 | 
			
		||||
        from core.models import CustomField
 | 
			
		||||
@@ -717,15 +819,17 @@ def run_script(request, agent_id):
 | 
			
		||||
            timeout=req_timeout,
 | 
			
		||||
            wait=True,
 | 
			
		||||
            history_pk=history_pk,
 | 
			
		||||
            run_as_user=run_as_user,
 | 
			
		||||
            env_vars=env_vars,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        custom_field = CustomField.objects.get(pk=request.data["custom_field"])
 | 
			
		||||
 | 
			
		||||
        if custom_field.model == "agent":
 | 
			
		||||
        if custom_field.model == CustomFieldModel.AGENT:
 | 
			
		||||
            field = custom_field.get_or_create_field_value(agent)
 | 
			
		||||
        elif custom_field.model == "client":
 | 
			
		||||
        elif custom_field.model == CustomFieldModel.CLIENT:
 | 
			
		||||
            field = custom_field.get_or_create_field_value(agent.client)
 | 
			
		||||
        elif custom_field.model == "site":
 | 
			
		||||
        elif custom_field.model == CustomFieldModel.SITE:
 | 
			
		||||
            field = custom_field.get_or_create_field_value(agent.site)
 | 
			
		||||
        else:
 | 
			
		||||
            return notify_error("Custom Field was invalid")
 | 
			
		||||
@@ -745,13 +849,20 @@ def run_script(request, agent_id):
 | 
			
		||||
            timeout=req_timeout,
 | 
			
		||||
            wait=True,
 | 
			
		||||
            history_pk=history_pk,
 | 
			
		||||
            run_as_user=run_as_user,
 | 
			
		||||
            env_vars=env_vars,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        Note.objects.create(agent=agent, user=request.user, note=r)
 | 
			
		||||
        return Response(r)
 | 
			
		||||
    else:
 | 
			
		||||
        agent.run_script(
 | 
			
		||||
            scriptpk=script.pk, args=args, timeout=req_timeout, history_pk=history_pk
 | 
			
		||||
            scriptpk=script.pk,
 | 
			
		||||
            args=args,
 | 
			
		||||
            timeout=req_timeout,
 | 
			
		||||
            history_pk=history_pk,
 | 
			
		||||
            run_as_user=run_as_user,
 | 
			
		||||
            env_vars=env_vars,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    return Response(f"{script.name} will now be run on {agent.hostname}")
 | 
			
		||||
@@ -853,19 +964,21 @@ def bulk(request):
 | 
			
		||||
        return notify_error("Something went wrong")
 | 
			
		||||
 | 
			
		||||
    if request.data["monType"] == "servers":
 | 
			
		||||
        q = q.filter(monitoring_type="server")
 | 
			
		||||
        q = q.filter(monitoring_type=AgentMonType.SERVER)
 | 
			
		||||
    elif request.data["monType"] == "workstations":
 | 
			
		||||
        q = q.filter(monitoring_type="workstation")
 | 
			
		||||
        q = q.filter(monitoring_type=AgentMonType.WORKSTATION)
 | 
			
		||||
 | 
			
		||||
    if request.data["osType"] == "windows":
 | 
			
		||||
        q = q.filter(plat="windows")
 | 
			
		||||
    elif request.data["osType"] == "linux":
 | 
			
		||||
        q = q.filter(plat="linux")
 | 
			
		||||
    if request.data["osType"] == AgentPlat.WINDOWS:
 | 
			
		||||
        q = q.filter(plat=AgentPlat.WINDOWS)
 | 
			
		||||
    elif request.data["osType"] == AgentPlat.LINUX:
 | 
			
		||||
        q = q.filter(plat=AgentPlat.LINUX)
 | 
			
		||||
    elif request.data["osType"] == AgentPlat.DARWIN:
 | 
			
		||||
        q = q.filter(plat=AgentPlat.DARWIN)
 | 
			
		||||
 | 
			
		||||
    agents: list[int] = [agent.pk for agent in q]
 | 
			
		||||
 | 
			
		||||
    if not agents:
 | 
			
		||||
        return notify_error("No agents where found meeting the selected criteria")
 | 
			
		||||
        return notify_error("No agents were found meeting the selected criteria")
 | 
			
		||||
 | 
			
		||||
    AuditLog.audit_bulk_action(
 | 
			
		||||
        request.user,
 | 
			
		||||
@@ -880,29 +993,32 @@ def bulk(request):
 | 
			
		||||
        else:
 | 
			
		||||
            shell = request.data["shell"]
 | 
			
		||||
 | 
			
		||||
        handle_bulk_command_task.delay(
 | 
			
		||||
            agents,
 | 
			
		||||
            request.data["cmd"],
 | 
			
		||||
            shell,
 | 
			
		||||
            request.data["timeout"],
 | 
			
		||||
            request.user.username[:50],
 | 
			
		||||
            run_on_offline=request.data["offlineAgents"],
 | 
			
		||||
        bulk_command_task.delay(
 | 
			
		||||
            agent_pks=agents,
 | 
			
		||||
            cmd=request.data["cmd"],
 | 
			
		||||
            shell=shell,
 | 
			
		||||
            timeout=request.data["timeout"],
 | 
			
		||||
            username=request.user.username[:50],
 | 
			
		||||
            run_as_user=request.data["run_as_user"],
 | 
			
		||||
        )
 | 
			
		||||
        return Response(f"Command will now be run on {len(agents)} agents")
 | 
			
		||||
 | 
			
		||||
    elif request.data["mode"] == "script":
 | 
			
		||||
        script = get_object_or_404(Script, pk=request.data["script"])
 | 
			
		||||
        handle_bulk_script_task.delay(
 | 
			
		||||
            script.pk,
 | 
			
		||||
            agents,
 | 
			
		||||
            request.data["args"],
 | 
			
		||||
            request.data["timeout"],
 | 
			
		||||
            request.user.username[:50],
 | 
			
		||||
 | 
			
		||||
        bulk_script_task.delay(
 | 
			
		||||
            script_pk=script.pk,
 | 
			
		||||
            agent_pks=agents,
 | 
			
		||||
            args=request.data["args"],
 | 
			
		||||
            timeout=request.data["timeout"],
 | 
			
		||||
            username=request.user.username[:50],
 | 
			
		||||
            run_as_user=request.data["run_as_user"],
 | 
			
		||||
            env_vars=request.data["env_vars"],
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        return Response(f"{script.name} will now be run on {len(agents)} agents")
 | 
			
		||||
 | 
			
		||||
    elif request.data["mode"] == "patch":
 | 
			
		||||
 | 
			
		||||
        if request.data["patchMode"] == "install":
 | 
			
		||||
            bulk_install_updates_task.delay(agents)
 | 
			
		||||
            return Response(
 | 
			
		||||
@@ -918,7 +1034,6 @@ def bulk(request):
 | 
			
		||||
@api_view(["POST"])
 | 
			
		||||
@permission_classes([IsAuthenticated, AgentPerms])
 | 
			
		||||
def agent_maintenance(request):
 | 
			
		||||
 | 
			
		||||
    if request.data["type"] == "Client":
 | 
			
		||||
        if not _has_perm_on_client(request.user, request.data["id"]):
 | 
			
		||||
            raise PermissionDenied()
 | 
			
		||||
@@ -945,10 +1060,17 @@ def agent_maintenance(request):
 | 
			
		||||
    if count:
 | 
			
		||||
        action = "disabled" if not request.data["action"] else "enabled"
 | 
			
		||||
        return Response(f"Maintenance mode has been {action} on {count} agents")
 | 
			
		||||
    else:
 | 
			
		||||
        return Response(
 | 
			
		||||
            f"No agents have been put in maintenance mode. You might not have permissions to the resources."
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    return Response(
 | 
			
		||||
        "No agents have been put in maintenance mode. You might not have permissions to the resources."
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view(["GET"])
 | 
			
		||||
@permission_classes([IsAuthenticated, RecoverAgentPerms])
 | 
			
		||||
def bulk_agent_recovery(request):
 | 
			
		||||
    bulk_recover_agents_task.delay()
 | 
			
		||||
    return Response("Agents will now be recovered")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class WMI(APIView):
 | 
			
		||||
@@ -973,3 +1095,113 @@ class AgentHistoryView(APIView):
 | 
			
		||||
            history = AgentHistory.objects.filter_by_role(request.user)  # type: ignore
 | 
			
		||||
        ctx = {"default_tz": get_default_timezone()}
 | 
			
		||||
        return Response(AgentHistorySerializer(history, many=True, context=ctx).data)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ScriptRunHistory(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, AgentHistoryPerms]
 | 
			
		||||
 | 
			
		||||
    class OutputSerializer(serializers.ModelSerializer):
 | 
			
		||||
        script_name = serializers.ReadOnlyField(source="script.name")
 | 
			
		||||
        agent_id = serializers.ReadOnlyField(source="agent.agent_id")
 | 
			
		||||
 | 
			
		||||
        class Meta:
 | 
			
		||||
            model = AgentHistory
 | 
			
		||||
            fields = (
 | 
			
		||||
                "id",
 | 
			
		||||
                "time",
 | 
			
		||||
                "username",
 | 
			
		||||
                "script",
 | 
			
		||||
                "script_results",
 | 
			
		||||
                "agent",
 | 
			
		||||
                "script_name",
 | 
			
		||||
                "agent_id",
 | 
			
		||||
            )
 | 
			
		||||
            read_only_fields = fields
 | 
			
		||||
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
        date_range_filter = Q()
 | 
			
		||||
        script_name_filter = Q()
 | 
			
		||||
 | 
			
		||||
        start = request.query_params.get("start", None)
 | 
			
		||||
        end = request.query_params.get("end", None)
 | 
			
		||||
        limit = request.query_params.get("limit", None)
 | 
			
		||||
        script_name = request.query_params.get("scriptname", None)
 | 
			
		||||
        if start and end:
 | 
			
		||||
            start_dt = parse_datetime(start)
 | 
			
		||||
            end_dt = parse_datetime(end) + djangotime.timedelta(days=1)
 | 
			
		||||
            date_range_filter = Q(time__range=[start_dt, end_dt])
 | 
			
		||||
 | 
			
		||||
        if script_name:
 | 
			
		||||
            script_name_filter = Q(script__name=script_name)
 | 
			
		||||
 | 
			
		||||
        AGENT_R_DEFER = (
 | 
			
		||||
            "agent__wmi_detail",
 | 
			
		||||
            "agent__services",
 | 
			
		||||
            "agent__created_by",
 | 
			
		||||
            "agent__created_time",
 | 
			
		||||
            "agent__modified_by",
 | 
			
		||||
            "agent__modified_time",
 | 
			
		||||
            "agent__disks",
 | 
			
		||||
            "agent__operating_system",
 | 
			
		||||
            "agent__mesh_node_id",
 | 
			
		||||
            "agent__description",
 | 
			
		||||
            "agent__patches_last_installed",
 | 
			
		||||
            "agent__time_zone",
 | 
			
		||||
            "agent__alert_template_id",
 | 
			
		||||
            "agent__policy_id",
 | 
			
		||||
            "agent__site_id",
 | 
			
		||||
            "agent__version",
 | 
			
		||||
            "agent__plat",
 | 
			
		||||
            "agent__goarch",
 | 
			
		||||
            "agent__hostname",
 | 
			
		||||
            "agent__last_seen",
 | 
			
		||||
            "agent__public_ip",
 | 
			
		||||
            "agent__total_ram",
 | 
			
		||||
            "agent__boot_time",
 | 
			
		||||
            "agent__logged_in_username",
 | 
			
		||||
            "agent__last_logged_in_user",
 | 
			
		||||
            "agent__monitoring_type",
 | 
			
		||||
            "agent__overdue_email_alert",
 | 
			
		||||
            "agent__overdue_text_alert",
 | 
			
		||||
            "agent__overdue_dashboard_alert",
 | 
			
		||||
            "agent__offline_time",
 | 
			
		||||
            "agent__overdue_time",
 | 
			
		||||
            "agent__check_interval",
 | 
			
		||||
            "agent__needs_reboot",
 | 
			
		||||
            "agent__choco_installed",
 | 
			
		||||
            "agent__maintenance_mode",
 | 
			
		||||
            "agent__block_policy_inheritance",
 | 
			
		||||
        )
 | 
			
		||||
        hists = (
 | 
			
		||||
            AgentHistory.objects.filter(type=AgentHistoryType.SCRIPT_RUN)
 | 
			
		||||
            .select_related("agent")
 | 
			
		||||
            .select_related("script")
 | 
			
		||||
            .defer(*AGENT_R_DEFER)
 | 
			
		||||
            .filter(date_range_filter)
 | 
			
		||||
            .filter(script_name_filter)
 | 
			
		||||
            .order_by("-time")
 | 
			
		||||
        )
 | 
			
		||||
        if limit:
 | 
			
		||||
            try:
 | 
			
		||||
                lim = int(limit)
 | 
			
		||||
            except KeyError:
 | 
			
		||||
                return notify_error("Invalid limit")
 | 
			
		||||
            hists = hists[:lim]
 | 
			
		||||
 | 
			
		||||
        ret = self.OutputSerializer(hists, many=True).data
 | 
			
		||||
        return Response(ret)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view(["POST"])
 | 
			
		||||
@permission_classes([IsAuthenticated, AgentWOLPerms])
 | 
			
		||||
def wol(request, agent_id):
 | 
			
		||||
    agent = get_object_or_404(
 | 
			
		||||
        Agent.objects.defer(*AGENT_DEFER),
 | 
			
		||||
        agent_id=agent_id,
 | 
			
		||||
    )
 | 
			
		||||
    try:
 | 
			
		||||
        uri = get_mesh_ws_url()
 | 
			
		||||
        asyncio.run(wake_on_lan(uri=uri, mesh_node_id=agent.mesh_node_id))
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        return notify_error(str(e))
 | 
			
		||||
    return Response(f"Wake-on-LAN sent to {agent.hostname}")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
# Generated by Django 4.0.3 on 2022-04-07 17:28
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def delete_alerts_without_agent(apps, schema):
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,23 @@
 | 
			
		||||
# Generated by Django 4.0.5 on 2022-06-29 07:57
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('alerts', '0011_alter_alert_agent'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='alert',
 | 
			
		||||
            name='action_retcode',
 | 
			
		||||
            field=models.BigIntegerField(blank=True, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='alert',
 | 
			
		||||
            name='resolved_action_retcode',
 | 
			
		||||
            field=models.BigIntegerField(blank=True, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,36 @@
 | 
			
		||||
# Generated by Django 4.1.3 on 2022-11-26 20:22
 | 
			
		||||
 | 
			
		||||
import django.contrib.postgres.fields
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("alerts", "0012_alter_alert_action_retcode_and_more"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name="alerttemplate",
 | 
			
		||||
            name="action_env_vars",
 | 
			
		||||
            field=django.contrib.postgres.fields.ArrayField(
 | 
			
		||||
                base_field=models.TextField(blank=True, null=True),
 | 
			
		||||
                blank=True,
 | 
			
		||||
                default=list,
 | 
			
		||||
                null=True,
 | 
			
		||||
                size=None,
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name="alerttemplate",
 | 
			
		||||
            name="resolved_action_env_vars",
 | 
			
		||||
            field=django.contrib.postgres.fields.ArrayField(
 | 
			
		||||
                base_field=models.TextField(blank=True, null=True),
 | 
			
		||||
                blank=True,
 | 
			
		||||
                default=list,
 | 
			
		||||
                null=True,
 | 
			
		||||
                size=None,
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -1,14 +1,22 @@
 | 
			
		||||
from __future__ import annotations
 | 
			
		||||
 | 
			
		||||
import re
 | 
			
		||||
from typing import TYPE_CHECKING, Union, Optional, Dict, Any, List, cast
 | 
			
		||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union, cast
 | 
			
		||||
 | 
			
		||||
from django.contrib.postgres.fields import ArrayField
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.db.models.fields import BooleanField, PositiveIntegerField
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from logs.models import BaseAuditModel, DebugLog
 | 
			
		||||
 | 
			
		||||
from logs.models import BaseAuditModel, DebugLog
 | 
			
		||||
from tacticalrmm.constants import (
 | 
			
		||||
    AgentHistoryType,
 | 
			
		||||
    AgentMonType,
 | 
			
		||||
    AlertSeverity,
 | 
			
		||||
    AlertType,
 | 
			
		||||
    CheckType,
 | 
			
		||||
    DebugLogType,
 | 
			
		||||
)
 | 
			
		||||
from tacticalrmm.models import PermissionQuerySet
 | 
			
		||||
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
@@ -18,20 +26,6 @@ if TYPE_CHECKING:
 | 
			
		||||
    from clients.models import Client, Site
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
SEVERITY_CHOICES = [
 | 
			
		||||
    ("info", "Informational"),
 | 
			
		||||
    ("warning", "Warning"),
 | 
			
		||||
    ("error", "Error"),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
ALERT_TYPE_CHOICES = [
 | 
			
		||||
    ("availability", "Availability"),
 | 
			
		||||
    ("check", "Check"),
 | 
			
		||||
    ("task", "Task"),
 | 
			
		||||
    ("custom", "Custom"),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Alert(models.Model):
 | 
			
		||||
    objects = PermissionQuerySet.as_manager()
 | 
			
		||||
 | 
			
		||||
@@ -57,7 +51,7 @@ class Alert(models.Model):
 | 
			
		||||
        blank=True,
 | 
			
		||||
    )
 | 
			
		||||
    alert_type = models.CharField(
 | 
			
		||||
        max_length=20, choices=ALERT_TYPE_CHOICES, default="availability"
 | 
			
		||||
        max_length=20, choices=AlertType.choices, default=AlertType.AVAILABILITY
 | 
			
		||||
    )
 | 
			
		||||
    message = models.TextField(null=True, blank=True)
 | 
			
		||||
    alert_time = models.DateTimeField(auto_now_add=True, null=True, blank=True)
 | 
			
		||||
@@ -65,7 +59,9 @@ class Alert(models.Model):
 | 
			
		||||
    snooze_until = models.DateTimeField(null=True, blank=True)
 | 
			
		||||
    resolved = models.BooleanField(default=False)
 | 
			
		||||
    resolved_on = models.DateTimeField(null=True, blank=True)
 | 
			
		||||
    severity = models.CharField(max_length=30, choices=SEVERITY_CHOICES, default="info")
 | 
			
		||||
    severity = models.CharField(
 | 
			
		||||
        max_length=30, choices=AlertSeverity.choices, default=AlertSeverity.INFO
 | 
			
		||||
    )
 | 
			
		||||
    email_sent = models.DateTimeField(null=True, blank=True)
 | 
			
		||||
    resolved_email_sent = models.DateTimeField(null=True, blank=True)
 | 
			
		||||
    sms_sent = models.DateTimeField(null=True, blank=True)
 | 
			
		||||
@@ -74,21 +70,21 @@ class Alert(models.Model):
 | 
			
		||||
    action_run = models.DateTimeField(null=True, blank=True)
 | 
			
		||||
    action_stdout = models.TextField(null=True, blank=True)
 | 
			
		||||
    action_stderr = models.TextField(null=True, blank=True)
 | 
			
		||||
    action_retcode = models.IntegerField(null=True, blank=True)
 | 
			
		||||
    action_retcode = models.BigIntegerField(null=True, blank=True)
 | 
			
		||||
    action_execution_time = models.CharField(max_length=100, null=True, blank=True)
 | 
			
		||||
    resolved_action_run = models.DateTimeField(null=True, blank=True)
 | 
			
		||||
    resolved_action_stdout = models.TextField(null=True, blank=True)
 | 
			
		||||
    resolved_action_stderr = models.TextField(null=True, blank=True)
 | 
			
		||||
    resolved_action_retcode = models.IntegerField(null=True, blank=True)
 | 
			
		||||
    resolved_action_retcode = models.BigIntegerField(null=True, blank=True)
 | 
			
		||||
    resolved_action_execution_time = models.CharField(
 | 
			
		||||
        max_length=100, null=True, blank=True
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def __str__(self) -> str:
 | 
			
		||||
        return self.message
 | 
			
		||||
        return f"{self.alert_type} - {self.message}"
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def assigned_agent(self) -> "Agent":
 | 
			
		||||
    def assigned_agent(self) -> "Optional[Agent]":
 | 
			
		||||
        return self.agent
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
@@ -111,7 +107,7 @@ class Alert(models.Model):
 | 
			
		||||
        cls, agent: Agent, skip_create: bool = False
 | 
			
		||||
    ) -> Optional[Alert]:
 | 
			
		||||
        if not cls.objects.filter(
 | 
			
		||||
            agent=agent, alert_type="availability", resolved=False
 | 
			
		||||
            agent=agent, alert_type=AlertType.AVAILABILITY, resolved=False
 | 
			
		||||
        ).exists():
 | 
			
		||||
            if skip_create:
 | 
			
		||||
                return None
 | 
			
		||||
@@ -120,8 +116,8 @@ class Alert(models.Model):
 | 
			
		||||
                Alert,
 | 
			
		||||
                cls.objects.create(
 | 
			
		||||
                    agent=agent,
 | 
			
		||||
                    alert_type="availability",
 | 
			
		||||
                    severity="error",
 | 
			
		||||
                    alert_type=AlertType.AVAILABILITY,
 | 
			
		||||
                    severity=AlertSeverity.ERROR,
 | 
			
		||||
                    message=f"{agent.hostname} in {agent.client.name}\\{agent.site.name} is overdue.",
 | 
			
		||||
                    hidden=True,
 | 
			
		||||
                ),
 | 
			
		||||
@@ -131,12 +127,12 @@ class Alert(models.Model):
 | 
			
		||||
                return cast(
 | 
			
		||||
                    Alert,
 | 
			
		||||
                    cls.objects.get(
 | 
			
		||||
                        agent=agent, alert_type="availability", resolved=False
 | 
			
		||||
                        agent=agent, alert_type=AlertType.AVAILABILITY, resolved=False
 | 
			
		||||
                    ),
 | 
			
		||||
                )
 | 
			
		||||
            except cls.MultipleObjectsReturned:
 | 
			
		||||
                alerts = cls.objects.filter(
 | 
			
		||||
                    agent=agent, alert_type="availability", resolved=False
 | 
			
		||||
                    agent=agent, alert_type=AlertType.AVAILABILITY, resolved=False
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
                last_alert = cast(Alert, alerts.last())
 | 
			
		||||
@@ -158,7 +154,6 @@ class Alert(models.Model):
 | 
			
		||||
        alert_severity: Optional[str] = None,
 | 
			
		||||
        skip_create: bool = False,
 | 
			
		||||
    ) -> "Optional[Alert]":
 | 
			
		||||
 | 
			
		||||
        # need to pass agent if the check is a policy
 | 
			
		||||
        if not cls.objects.filter(
 | 
			
		||||
            assigned_check=check,
 | 
			
		||||
@@ -173,11 +168,18 @@ class Alert(models.Model):
 | 
			
		||||
                cls.objects.create(
 | 
			
		||||
                    assigned_check=check,
 | 
			
		||||
                    agent=agent,
 | 
			
		||||
                    alert_type="check",
 | 
			
		||||
                    severity=check.alert_severity
 | 
			
		||||
                    if check.check_type
 | 
			
		||||
                    not in ["memory", "cpuload", "diskspace", "script"]
 | 
			
		||||
                    else alert_severity,
 | 
			
		||||
                    alert_type=AlertType.CHECK,
 | 
			
		||||
                    severity=(
 | 
			
		||||
                        check.alert_severity
 | 
			
		||||
                        if check.check_type
 | 
			
		||||
                        not in {
 | 
			
		||||
                            CheckType.MEMORY,
 | 
			
		||||
                            CheckType.CPU_LOAD,
 | 
			
		||||
                            CheckType.DISK_SPACE,
 | 
			
		||||
                            CheckType.SCRIPT,
 | 
			
		||||
                        }
 | 
			
		||||
                        else alert_severity
 | 
			
		||||
                    ),
 | 
			
		||||
                    message=f"{agent.hostname} has a {check.check_type} check: {check.readable_desc} that failed.",
 | 
			
		||||
                    hidden=True,
 | 
			
		||||
                ),
 | 
			
		||||
@@ -216,7 +218,6 @@ class Alert(models.Model):
 | 
			
		||||
        agent: "Agent",
 | 
			
		||||
        skip_create: bool = False,
 | 
			
		||||
    ) -> "Optional[Alert]":
 | 
			
		||||
 | 
			
		||||
        if not cls.objects.filter(
 | 
			
		||||
            assigned_task=task,
 | 
			
		||||
            agent=agent,
 | 
			
		||||
@@ -230,7 +231,7 @@ class Alert(models.Model):
 | 
			
		||||
                cls.objects.create(
 | 
			
		||||
                    assigned_task=task,
 | 
			
		||||
                    agent=agent,
 | 
			
		||||
                    alert_type="task",
 | 
			
		||||
                    alert_type=AlertType.TASK,
 | 
			
		||||
                    severity=task.alert_severity,
 | 
			
		||||
                    message=f"{agent.hostname} has task: {task.name} that failed.",
 | 
			
		||||
                    hidden=True,
 | 
			
		||||
@@ -268,7 +269,7 @@ class Alert(models.Model):
 | 
			
		||||
    def handle_alert_failure(
 | 
			
		||||
        cls, instance: Union[Agent, TaskResult, CheckResult]
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        from agents.models import Agent
 | 
			
		||||
        from agents.models import Agent, AgentHistory
 | 
			
		||||
        from autotasks.models import TaskResult
 | 
			
		||||
        from checks.models import CheckResult
 | 
			
		||||
 | 
			
		||||
@@ -296,14 +297,14 @@ class Alert(models.Model):
 | 
			
		||||
            dashboard_alert = instance.overdue_dashboard_alert
 | 
			
		||||
            alert_template = instance.alert_template
 | 
			
		||||
            maintenance_mode = instance.maintenance_mode
 | 
			
		||||
            alert_severity = "error"
 | 
			
		||||
            alert_severity = AlertSeverity.ERROR
 | 
			
		||||
            agent = instance
 | 
			
		||||
            dashboard_severities = [AlertSeverity.ERROR]
 | 
			
		||||
            email_severities = [AlertSeverity.ERROR]
 | 
			
		||||
            text_severities = [AlertSeverity.ERROR]
 | 
			
		||||
 | 
			
		||||
            # set alert_template settings
 | 
			
		||||
            if alert_template:
 | 
			
		||||
                dashboard_severities = ["error"]
 | 
			
		||||
                email_severities = ["error"]
 | 
			
		||||
                text_severities = ["error"]
 | 
			
		||||
                always_dashboard = alert_template.agent_always_alert
 | 
			
		||||
                always_email = alert_template.agent_always_email
 | 
			
		||||
                always_text = alert_template.agent_always_text
 | 
			
		||||
@@ -327,16 +328,34 @@ class Alert(models.Model):
 | 
			
		||||
            alert_severity = (
 | 
			
		||||
                instance.assigned_check.alert_severity
 | 
			
		||||
                if instance.assigned_check.check_type
 | 
			
		||||
                not in ["memory", "cpuload", "diskspace", "script"]
 | 
			
		||||
                not in {
 | 
			
		||||
                    CheckType.MEMORY,
 | 
			
		||||
                    CheckType.CPU_LOAD,
 | 
			
		||||
                    CheckType.DISK_SPACE,
 | 
			
		||||
                    CheckType.SCRIPT,
 | 
			
		||||
                }
 | 
			
		||||
                else instance.alert_severity
 | 
			
		||||
            )
 | 
			
		||||
            agent = instance.agent
 | 
			
		||||
 | 
			
		||||
            # set alert_template settings
 | 
			
		||||
            if alert_template:
 | 
			
		||||
                dashboard_severities = alert_template.check_dashboard_alert_severity
 | 
			
		||||
                email_severities = alert_template.check_email_alert_severity
 | 
			
		||||
                text_severities = alert_template.check_text_alert_severity
 | 
			
		||||
                dashboard_severities = (
 | 
			
		||||
                    alert_template.check_dashboard_alert_severity
 | 
			
		||||
                    or [
 | 
			
		||||
                        AlertSeverity.ERROR,
 | 
			
		||||
                        AlertSeverity.WARNING,
 | 
			
		||||
                        AlertSeverity.INFO,
 | 
			
		||||
                    ]
 | 
			
		||||
                )
 | 
			
		||||
                email_severities = alert_template.check_email_alert_severity or [
 | 
			
		||||
                    AlertSeverity.ERROR,
 | 
			
		||||
                    AlertSeverity.WARNING,
 | 
			
		||||
                ]
 | 
			
		||||
                text_severities = alert_template.check_text_alert_severity or [
 | 
			
		||||
                    AlertSeverity.ERROR,
 | 
			
		||||
                    AlertSeverity.WARNING,
 | 
			
		||||
                ]
 | 
			
		||||
                always_dashboard = alert_template.check_always_alert
 | 
			
		||||
                always_email = alert_template.check_always_email
 | 
			
		||||
                always_text = alert_template.check_always_text
 | 
			
		||||
@@ -359,9 +378,18 @@ class Alert(models.Model):
 | 
			
		||||
 | 
			
		||||
            # set alert_template settings
 | 
			
		||||
            if alert_template:
 | 
			
		||||
                dashboard_severities = alert_template.task_dashboard_alert_severity
 | 
			
		||||
                email_severities = alert_template.task_email_alert_severity
 | 
			
		||||
                text_severities = alert_template.task_text_alert_severity
 | 
			
		||||
                dashboard_severities = alert_template.task_dashboard_alert_severity or [
 | 
			
		||||
                    AlertSeverity.ERROR,
 | 
			
		||||
                    AlertSeverity.WARNING,
 | 
			
		||||
                ]
 | 
			
		||||
                email_severities = alert_template.task_email_alert_severity or [
 | 
			
		||||
                    AlertSeverity.ERROR,
 | 
			
		||||
                    AlertSeverity.WARNING,
 | 
			
		||||
                ]
 | 
			
		||||
                text_severities = alert_template.task_text_alert_severity or [
 | 
			
		||||
                    AlertSeverity.ERROR,
 | 
			
		||||
                    AlertSeverity.WARNING,
 | 
			
		||||
                ]
 | 
			
		||||
                always_dashboard = alert_template.task_always_alert
 | 
			
		||||
                always_email = alert_template.task_always_email
 | 
			
		||||
                always_text = alert_template.task_always_text
 | 
			
		||||
@@ -384,7 +412,6 @@ class Alert(models.Model):
 | 
			
		||||
 | 
			
		||||
        # create alert in dashboard if enabled
 | 
			
		||||
        if dashboard_alert or always_dashboard:
 | 
			
		||||
 | 
			
		||||
            # check if alert template is set and specific severities are configured
 | 
			
		||||
            if (
 | 
			
		||||
                not alert_template
 | 
			
		||||
@@ -397,7 +424,6 @@ class Alert(models.Model):
 | 
			
		||||
 | 
			
		||||
        # send email if enabled
 | 
			
		||||
        if email_alert or always_email:
 | 
			
		||||
 | 
			
		||||
            # check if alert template is set and specific severities are configured
 | 
			
		||||
            if (
 | 
			
		||||
                not alert_template
 | 
			
		||||
@@ -412,7 +438,6 @@ class Alert(models.Model):
 | 
			
		||||
 | 
			
		||||
        # send text if enabled
 | 
			
		||||
        if text_alert or always_text:
 | 
			
		||||
 | 
			
		||||
            # check if alert template is set and specific severities are configured
 | 
			
		||||
            if (
 | 
			
		||||
                not alert_template
 | 
			
		||||
@@ -429,13 +454,22 @@ class Alert(models.Model):
 | 
			
		||||
            and run_script_action
 | 
			
		||||
            and not alert.action_run
 | 
			
		||||
        ):
 | 
			
		||||
            hist = AgentHistory.objects.create(
 | 
			
		||||
                agent=agent,
 | 
			
		||||
                type=AgentHistoryType.SCRIPT_RUN,
 | 
			
		||||
                script=alert_template.action,
 | 
			
		||||
                username="alert-action-failure",
 | 
			
		||||
            )
 | 
			
		||||
            r = agent.run_script(
 | 
			
		||||
                scriptpk=alert_template.action.pk,
 | 
			
		||||
                args=alert.parse_script_args(alert_template.action_args),
 | 
			
		||||
                timeout=alert_template.action_timeout,
 | 
			
		||||
                wait=True,
 | 
			
		||||
                history_pk=hist.pk,
 | 
			
		||||
                full=True,
 | 
			
		||||
                run_on_any=True,
 | 
			
		||||
                run_as_user=False,
 | 
			
		||||
                env_vars=alert_template.action_env_vars,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            # command was successful
 | 
			
		||||
@@ -449,7 +483,7 @@ class Alert(models.Model):
 | 
			
		||||
            else:
 | 
			
		||||
                DebugLog.error(
 | 
			
		||||
                    agent=agent,
 | 
			
		||||
                    log_type="scripting",
 | 
			
		||||
                    log_type=DebugLogType.SCRIPTING,
 | 
			
		||||
                    message=f"Failure action: {alert_template.action.name} failed to run on any agent for {agent.hostname}({agent.pk}) failure alert",
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
@@ -457,7 +491,7 @@ class Alert(models.Model):
 | 
			
		||||
    def handle_alert_resolve(
 | 
			
		||||
        cls, instance: Union[Agent, TaskResult, CheckResult]
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        from agents.models import Agent
 | 
			
		||||
        from agents.models import Agent, AgentHistory
 | 
			
		||||
        from autotasks.models import TaskResult
 | 
			
		||||
        from checks.models import CheckResult
 | 
			
		||||
 | 
			
		||||
@@ -551,13 +585,22 @@ class Alert(models.Model):
 | 
			
		||||
            and run_script_action
 | 
			
		||||
            and not alert.resolved_action_run
 | 
			
		||||
        ):
 | 
			
		||||
            hist = AgentHistory.objects.create(
 | 
			
		||||
                agent=agent,
 | 
			
		||||
                type=AgentHistoryType.SCRIPT_RUN,
 | 
			
		||||
                script=alert_template.action,
 | 
			
		||||
                username="alert-action-resolved",
 | 
			
		||||
            )
 | 
			
		||||
            r = agent.run_script(
 | 
			
		||||
                scriptpk=alert_template.resolved_action.pk,
 | 
			
		||||
                args=alert.parse_script_args(alert_template.resolved_action_args),
 | 
			
		||||
                timeout=alert_template.resolved_action_timeout,
 | 
			
		||||
                wait=True,
 | 
			
		||||
                history_pk=hist.pk,
 | 
			
		||||
                full=True,
 | 
			
		||||
                run_on_any=True,
 | 
			
		||||
                run_as_user=False,
 | 
			
		||||
                env_vars=alert_template.resolved_action_env_vars,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            # command was successful
 | 
			
		||||
@@ -573,22 +616,20 @@ class Alert(models.Model):
 | 
			
		||||
            else:
 | 
			
		||||
                DebugLog.error(
 | 
			
		||||
                    agent=agent,
 | 
			
		||||
                    log_type="scripting",
 | 
			
		||||
                    log_type=DebugLogType.SCRIPTING,
 | 
			
		||||
                    message=f"Resolved action: {alert_template.action.name} failed to run on any agent for {agent.hostname}({agent.pk}) resolved alert",
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
    def parse_script_args(self, args: List[str]) -> List[str]:
 | 
			
		||||
 | 
			
		||||
        if not args:
 | 
			
		||||
            return []
 | 
			
		||||
 | 
			
		||||
        temp_args = list()
 | 
			
		||||
        temp_args = []
 | 
			
		||||
        # pattern to match for injection
 | 
			
		||||
        pattern = re.compile(".*\\{\\{alert\\.(.*)\\}\\}.*")
 | 
			
		||||
 | 
			
		||||
        for arg in args:
 | 
			
		||||
            match = pattern.match(arg)
 | 
			
		||||
            if match:
 | 
			
		||||
            if match := pattern.match(arg):
 | 
			
		||||
                name = match.group(1)
 | 
			
		||||
 | 
			
		||||
                # check if attr exists and isn't a function
 | 
			
		||||
@@ -599,8 +640,10 @@ class Alert(models.Model):
 | 
			
		||||
 | 
			
		||||
                try:
 | 
			
		||||
                    temp_args.append(re.sub("\\{\\{.*\\}\\}", value, arg))
 | 
			
		||||
                except re.error:
 | 
			
		||||
                    temp_args.append(re.sub("\\{\\{.*\\}\\}", re.escape(value), arg))
 | 
			
		||||
                except Exception as e:
 | 
			
		||||
                    DebugLog.error(log_type="scripting", message=str(e))
 | 
			
		||||
                    DebugLog.error(log_type=DebugLogType.SCRIPTING, message=str(e))
 | 
			
		||||
                    continue
 | 
			
		||||
 | 
			
		||||
            else:
 | 
			
		||||
@@ -626,6 +669,12 @@ class AlertTemplate(BaseAuditModel):
 | 
			
		||||
        blank=True,
 | 
			
		||||
        default=list,
 | 
			
		||||
    )
 | 
			
		||||
    action_env_vars = ArrayField(
 | 
			
		||||
        models.TextField(null=True, blank=True),
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        default=list,
 | 
			
		||||
    )
 | 
			
		||||
    action_timeout = models.PositiveIntegerField(default=15)
 | 
			
		||||
    resolved_action = models.ForeignKey(
 | 
			
		||||
        "scripts.Script",
 | 
			
		||||
@@ -640,6 +689,12 @@ class AlertTemplate(BaseAuditModel):
 | 
			
		||||
        blank=True,
 | 
			
		||||
        default=list,
 | 
			
		||||
    )
 | 
			
		||||
    resolved_action_env_vars = ArrayField(
 | 
			
		||||
        models.TextField(null=True, blank=True),
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        default=list,
 | 
			
		||||
    )
 | 
			
		||||
    resolved_action_timeout = models.PositiveIntegerField(default=15)
 | 
			
		||||
 | 
			
		||||
    # overrides the global recipients
 | 
			
		||||
@@ -670,17 +725,17 @@ class AlertTemplate(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
    # check alert settings
 | 
			
		||||
    check_email_alert_severity = ArrayField(
 | 
			
		||||
        models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
 | 
			
		||||
        models.CharField(max_length=25, blank=True, choices=AlertSeverity.choices),
 | 
			
		||||
        blank=True,
 | 
			
		||||
        default=list,
 | 
			
		||||
    )
 | 
			
		||||
    check_text_alert_severity = ArrayField(
 | 
			
		||||
        models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
 | 
			
		||||
        models.CharField(max_length=25, blank=True, choices=AlertSeverity.choices),
 | 
			
		||||
        blank=True,
 | 
			
		||||
        default=list,
 | 
			
		||||
    )
 | 
			
		||||
    check_dashboard_alert_severity = ArrayField(
 | 
			
		||||
        models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
 | 
			
		||||
        models.CharField(max_length=25, blank=True, choices=AlertSeverity.choices),
 | 
			
		||||
        blank=True,
 | 
			
		||||
        default=list,
 | 
			
		||||
    )
 | 
			
		||||
@@ -694,17 +749,17 @@ class AlertTemplate(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
    # task alert settings
 | 
			
		||||
    task_email_alert_severity = ArrayField(
 | 
			
		||||
        models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
 | 
			
		||||
        models.CharField(max_length=25, blank=True, choices=AlertSeverity.choices),
 | 
			
		||||
        blank=True,
 | 
			
		||||
        default=list,
 | 
			
		||||
    )
 | 
			
		||||
    task_text_alert_severity = ArrayField(
 | 
			
		||||
        models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
 | 
			
		||||
        models.CharField(max_length=25, blank=True, choices=AlertSeverity.choices),
 | 
			
		||||
        blank=True,
 | 
			
		||||
        default=list,
 | 
			
		||||
    )
 | 
			
		||||
    task_dashboard_alert_severity = ArrayField(
 | 
			
		||||
        models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES),
 | 
			
		||||
        models.CharField(max_length=25, blank=True, choices=AlertSeverity.choices),
 | 
			
		||||
        blank=True,
 | 
			
		||||
        default=list,
 | 
			
		||||
    )
 | 
			
		||||
@@ -738,9 +793,9 @@ class AlertTemplate(BaseAuditModel):
 | 
			
		||||
            agent in self.excluded_agents.all()
 | 
			
		||||
            or agent.site in self.excluded_sites.all()
 | 
			
		||||
            or agent.client in self.excluded_clients.all()
 | 
			
		||||
            or agent.monitoring_type == "workstation"
 | 
			
		||||
            or agent.monitoring_type == AgentMonType.WORKSTATION
 | 
			
		||||
            and self.exclude_workstations
 | 
			
		||||
            or agent.monitoring_type == "server"
 | 
			
		||||
            or agent.monitoring_type == AgentMonType.SERVER
 | 
			
		||||
            and self.exclude_servers
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,8 @@
 | 
			
		||||
from typing import TYPE_CHECKING
 | 
			
		||||
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from rest_framework import permissions
 | 
			
		||||
from typing import TYPE_CHECKING
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.permissions import _has_perm, _has_perm_on_agent
 | 
			
		||||
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
@@ -30,7 +32,7 @@ def _has_perm_on_alert(user: "User", id: int) -> bool:
 | 
			
		||||
 | 
			
		||||
class AlertPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view) -> bool:
 | 
			
		||||
        if r.method == "GET" or r.method == "PATCH":
 | 
			
		||||
        if r.method in ("GET", "PATCH"):
 | 
			
		||||
            if "pk" in view.kwargs.keys():
 | 
			
		||||
                return _has_perm(r, "can_list_alerts") and _has_perm_on_alert(
 | 
			
		||||
                    r.user, view.kwargs["pk"]
 | 
			
		||||
@@ -50,5 +52,5 @@ class AlertTemplatePerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view) -> bool:
 | 
			
		||||
        if r.method == "GET":
 | 
			
		||||
            return _has_perm(r, "can_list_alerttemplates")
 | 
			
		||||
        else:
 | 
			
		||||
            return _has_perm(r, "can_manage_alerttemplates")
 | 
			
		||||
 | 
			
		||||
        return _has_perm(r, "can_manage_alerttemplates")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,13 @@
 | 
			
		||||
from automation.serializers import PolicySerializer
 | 
			
		||||
from clients.serializers import ClientMinimumSerializer, SiteMinimumSerializer
 | 
			
		||||
from rest_framework.fields import SerializerMethodField
 | 
			
		||||
from rest_framework.serializers import ModelSerializer, ReadOnlyField
 | 
			
		||||
 | 
			
		||||
from automation.serializers import PolicySerializer
 | 
			
		||||
from clients.serializers import ClientMinimumSerializer, SiteMinimumSerializer
 | 
			
		||||
 | 
			
		||||
from .models import Alert, AlertTemplate
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AlertSerializer(ModelSerializer):
 | 
			
		||||
 | 
			
		||||
    hostname = ReadOnlyField(source="assigned_agent.hostname")
 | 
			
		||||
    agent_id = ReadOnlyField(source="assigned_agent.agent_id")
 | 
			
		||||
    client = ReadOnlyField(source="client.name")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,10 @@
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from .models import Alert
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from tacticalrmm.celery import app
 | 
			
		||||
 | 
			
		||||
from .models import Alert
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def unsnooze_alerts() -> str:
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +1,18 @@
 | 
			
		||||
from datetime import datetime, timedelta
 | 
			
		||||
from datetime import timedelta
 | 
			
		||||
from itertools import cycle
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
 | 
			
		||||
from alerts.tasks import cache_agents_alert_template
 | 
			
		||||
from core.utils import get_core_settings
 | 
			
		||||
from core.tasks import cache_db_fields_task, handle_resolved_stuff
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from model_bakery import baker, seq
 | 
			
		||||
 | 
			
		||||
from alerts.tasks import cache_agents_alert_template
 | 
			
		||||
from autotasks.models import TaskResult
 | 
			
		||||
from core.tasks import cache_db_fields_task, resolve_alerts_task
 | 
			
		||||
from core.utils import get_core_settings
 | 
			
		||||
from tacticalrmm.constants import AgentMonType, AlertSeverity, AlertType, CheckStatus
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
 | 
			
		||||
from autotasks.models import TaskResult
 | 
			
		||||
from .models import Alert, AlertTemplate
 | 
			
		||||
from .serializers import (
 | 
			
		||||
    AlertSerializer,
 | 
			
		||||
@@ -27,6 +28,7 @@ class TestAlertsViews(TacticalTestCase):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        self.setup_coresettings()
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    def test_get_alerts(self):
 | 
			
		||||
        url = "/alerts/"
 | 
			
		||||
 | 
			
		||||
@@ -38,15 +40,15 @@ class TestAlertsViews(TacticalTestCase):
 | 
			
		||||
        alerts = baker.make(
 | 
			
		||||
            "alerts.Alert",
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            alert_time=seq(datetime.now(), timedelta(days=15)),
 | 
			
		||||
            severity="warning",
 | 
			
		||||
            alert_time=seq(djangotime.now(), timedelta(days=15)),
 | 
			
		||||
            severity=AlertSeverity.WARNING,
 | 
			
		||||
            _quantity=3,
 | 
			
		||||
        )
 | 
			
		||||
        baker.make(
 | 
			
		||||
            "alerts.Alert",
 | 
			
		||||
            assigned_check=check,
 | 
			
		||||
            alert_time=seq(datetime.now(), timedelta(days=15)),
 | 
			
		||||
            severity="error",
 | 
			
		||||
            alert_time=seq(djangotime.now(), timedelta(days=15)),
 | 
			
		||||
            severity=AlertSeverity.ERROR,
 | 
			
		||||
            _quantity=7,
 | 
			
		||||
        )
 | 
			
		||||
        baker.make(
 | 
			
		||||
@@ -54,7 +56,7 @@ class TestAlertsViews(TacticalTestCase):
 | 
			
		||||
            assigned_task=task,
 | 
			
		||||
            snoozed=True,
 | 
			
		||||
            snooze_until=djangotime.now(),
 | 
			
		||||
            alert_time=seq(datetime.now(), timedelta(days=15)),
 | 
			
		||||
            alert_time=seq(djangotime.now(), timedelta(days=15)),
 | 
			
		||||
            _quantity=2,
 | 
			
		||||
        )
 | 
			
		||||
        baker.make(
 | 
			
		||||
@@ -62,7 +64,7 @@ class TestAlertsViews(TacticalTestCase):
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            resolved=True,
 | 
			
		||||
            resolved_on=djangotime.now(),
 | 
			
		||||
            alert_time=seq(datetime.now(), timedelta(days=15)),
 | 
			
		||||
            alert_time=seq(djangotime.now(), timedelta(days=15)),
 | 
			
		||||
            _quantity=9,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@@ -70,8 +72,8 @@ class TestAlertsViews(TacticalTestCase):
 | 
			
		||||
        data = {"top": 3}
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEquals(resp.data["alerts"], AlertSerializer(alerts, many=True).data)
 | 
			
		||||
        self.assertEquals(resp.data["alerts_count"], 10)
 | 
			
		||||
        self.assertEqual(resp.data["alerts"], AlertSerializer(alerts, many=True).data)
 | 
			
		||||
        self.assertEqual(resp.data["alerts_count"], 10)
 | 
			
		||||
 | 
			
		||||
        # test filter data
 | 
			
		||||
        # test data and result counts
 | 
			
		||||
@@ -119,13 +121,14 @@ class TestAlertsViews(TacticalTestCase):
 | 
			
		||||
            self.assertEqual(len(resp.data), req["count"])
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("patch", url)
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def test_add_alert(self):
 | 
			
		||||
        url = "/alerts/"
 | 
			
		||||
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        data = {
 | 
			
		||||
            "alert_time": datetime.now(),
 | 
			
		||||
            "alert_time": djangotime.now(),
 | 
			
		||||
            "agent": agent.id,
 | 
			
		||||
            "severity": "warning",
 | 
			
		||||
            "alert_type": "availability",
 | 
			
		||||
@@ -362,7 +365,7 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
        not_snoozed = baker.make(
 | 
			
		||||
            "alerts.Alert",
 | 
			
		||||
            snoozed=True,
 | 
			
		||||
            snooze_until=seq(datetime.now(), timedelta(days=15)),
 | 
			
		||||
            snooze_until=seq(djangotime.now(), timedelta(days=15)),
 | 
			
		||||
            _quantity=5,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@@ -370,7 +373,7 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
        snoozed = baker.make(
 | 
			
		||||
            "alerts.Alert",
 | 
			
		||||
            snoozed=True,
 | 
			
		||||
            snooze_until=seq(datetime.now(), timedelta(days=-15)),
 | 
			
		||||
            snooze_until=seq(djangotime.now(), timedelta(days=-15)),
 | 
			
		||||
            _quantity=5,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@@ -388,11 +391,12 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_agent_gets_correct_alert_template(self):
 | 
			
		||||
 | 
			
		||||
        core = get_core_settings()
 | 
			
		||||
        # setup data
 | 
			
		||||
        workstation = baker.make_recipe("agents.agent", monitoring_type="workstation")
 | 
			
		||||
        server = baker.make_recipe("agents.agent", monitoring_type="server")
 | 
			
		||||
        workstation = baker.make_recipe(
 | 
			
		||||
            "agents.agent", monitoring_type=AgentMonType.WORKSTATION
 | 
			
		||||
        )
 | 
			
		||||
        server = baker.make_recipe("agents.agent", monitoring_type=AgentMonType.SERVER)
 | 
			
		||||
 | 
			
		||||
        policy = baker.make("automation.Policy", active=True)
 | 
			
		||||
 | 
			
		||||
@@ -409,15 +413,15 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
        core.server_policy = policy
 | 
			
		||||
        core.save()
 | 
			
		||||
 | 
			
		||||
        self.assertEquals(server.set_alert_template().pk, alert_templates[0].pk)
 | 
			
		||||
        self.assertEquals(workstation.set_alert_template().pk, alert_templates[0].pk)
 | 
			
		||||
        self.assertEqual(server.set_alert_template().pk, alert_templates[0].pk)
 | 
			
		||||
        self.assertEqual(workstation.set_alert_template().pk, alert_templates[0].pk)
 | 
			
		||||
 | 
			
		||||
        # assign second Alert Template to as default alert template
 | 
			
		||||
        core.alert_template = alert_templates[1]
 | 
			
		||||
        core.save()
 | 
			
		||||
 | 
			
		||||
        self.assertEquals(workstation.set_alert_template().pk, alert_templates[1].pk)
 | 
			
		||||
        self.assertEquals(server.set_alert_template().pk, alert_templates[1].pk)
 | 
			
		||||
        self.assertEqual(workstation.set_alert_template().pk, alert_templates[1].pk)
 | 
			
		||||
        self.assertEqual(server.set_alert_template().pk, alert_templates[1].pk)
 | 
			
		||||
 | 
			
		||||
        # assign third Alert Template to client
 | 
			
		||||
        workstation.client.alert_template = alert_templates[2]
 | 
			
		||||
@@ -425,8 +429,8 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
        workstation.client.save()
 | 
			
		||||
        server.client.save()
 | 
			
		||||
 | 
			
		||||
        self.assertEquals(workstation.set_alert_template().pk, alert_templates[2].pk)
 | 
			
		||||
        self.assertEquals(server.set_alert_template().pk, alert_templates[2].pk)
 | 
			
		||||
        self.assertEqual(workstation.set_alert_template().pk, alert_templates[2].pk)
 | 
			
		||||
        self.assertEqual(server.set_alert_template().pk, alert_templates[2].pk)
 | 
			
		||||
 | 
			
		||||
        # apply policy to client and should override
 | 
			
		||||
        workstation.client.workstation_policy = policy
 | 
			
		||||
@@ -434,8 +438,8 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
        workstation.client.save()
 | 
			
		||||
        server.client.save()
 | 
			
		||||
 | 
			
		||||
        self.assertEquals(workstation.set_alert_template().pk, alert_templates[0].pk)
 | 
			
		||||
        self.assertEquals(server.set_alert_template().pk, alert_templates[0].pk)
 | 
			
		||||
        self.assertEqual(workstation.set_alert_template().pk, alert_templates[0].pk)
 | 
			
		||||
        self.assertEqual(server.set_alert_template().pk, alert_templates[0].pk)
 | 
			
		||||
 | 
			
		||||
        # assign fouth Alert Template to site
 | 
			
		||||
        workstation.site.alert_template = alert_templates[3]
 | 
			
		||||
@@ -443,8 +447,8 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
        workstation.site.save()
 | 
			
		||||
        server.site.save()
 | 
			
		||||
 | 
			
		||||
        self.assertEquals(workstation.set_alert_template().pk, alert_templates[3].pk)
 | 
			
		||||
        self.assertEquals(server.set_alert_template().pk, alert_templates[3].pk)
 | 
			
		||||
        self.assertEqual(workstation.set_alert_template().pk, alert_templates[3].pk)
 | 
			
		||||
        self.assertEqual(server.set_alert_template().pk, alert_templates[3].pk)
 | 
			
		||||
 | 
			
		||||
        # apply policy to site
 | 
			
		||||
        workstation.site.workstation_policy = policy
 | 
			
		||||
@@ -452,8 +456,8 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
        workstation.site.save()
 | 
			
		||||
        server.site.save()
 | 
			
		||||
 | 
			
		||||
        self.assertEquals(workstation.set_alert_template().pk, alert_templates[0].pk)
 | 
			
		||||
        self.assertEquals(server.set_alert_template().pk, alert_templates[0].pk)
 | 
			
		||||
        self.assertEqual(workstation.set_alert_template().pk, alert_templates[0].pk)
 | 
			
		||||
        self.assertEqual(server.set_alert_template().pk, alert_templates[0].pk)
 | 
			
		||||
 | 
			
		||||
        # apply policy to agents
 | 
			
		||||
        workstation.policy = policy
 | 
			
		||||
@@ -461,35 +465,35 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
        workstation.save()
 | 
			
		||||
        server.save()
 | 
			
		||||
 | 
			
		||||
        self.assertEquals(workstation.set_alert_template().pk, alert_templates[0].pk)
 | 
			
		||||
        self.assertEquals(server.set_alert_template().pk, alert_templates[0].pk)
 | 
			
		||||
        self.assertEqual(workstation.set_alert_template().pk, alert_templates[0].pk)
 | 
			
		||||
        self.assertEqual(server.set_alert_template().pk, alert_templates[0].pk)
 | 
			
		||||
 | 
			
		||||
        # test disabling alert template
 | 
			
		||||
        alert_templates[0].is_active = False
 | 
			
		||||
        alert_templates[0].save()
 | 
			
		||||
 | 
			
		||||
        self.assertEquals(workstation.set_alert_template().pk, alert_templates[3].pk)
 | 
			
		||||
        self.assertEquals(server.set_alert_template().pk, alert_templates[3].pk)
 | 
			
		||||
        self.assertEqual(workstation.set_alert_template().pk, alert_templates[3].pk)
 | 
			
		||||
        self.assertEqual(server.set_alert_template().pk, alert_templates[3].pk)
 | 
			
		||||
 | 
			
		||||
        # test policy exclusions
 | 
			
		||||
        alert_templates[3].excluded_agents.set([workstation.pk])
 | 
			
		||||
 | 
			
		||||
        self.assertEquals(workstation.set_alert_template().pk, alert_templates[2].pk)
 | 
			
		||||
        self.assertEquals(server.set_alert_template().pk, alert_templates[3].pk)
 | 
			
		||||
        self.assertEqual(workstation.set_alert_template().pk, alert_templates[2].pk)
 | 
			
		||||
        self.assertEqual(server.set_alert_template().pk, alert_templates[3].pk)
 | 
			
		||||
 | 
			
		||||
        # test workstation exclusions
 | 
			
		||||
        alert_templates[2].exclude_workstations = True
 | 
			
		||||
        alert_templates[2].save()
 | 
			
		||||
 | 
			
		||||
        self.assertEquals(workstation.set_alert_template().pk, alert_templates[1].pk)
 | 
			
		||||
        self.assertEquals(server.set_alert_template().pk, alert_templates[3].pk)
 | 
			
		||||
        self.assertEqual(workstation.set_alert_template().pk, alert_templates[1].pk)
 | 
			
		||||
        self.assertEqual(server.set_alert_template().pk, alert_templates[3].pk)
 | 
			
		||||
 | 
			
		||||
        # test server exclusions
 | 
			
		||||
        alert_templates[3].exclude_servers = True
 | 
			
		||||
        alert_templates[3].save()
 | 
			
		||||
 | 
			
		||||
        self.assertEquals(workstation.set_alert_template().pk, alert_templates[1].pk)
 | 
			
		||||
        self.assertEquals(server.set_alert_template().pk, alert_templates[2].pk)
 | 
			
		||||
        self.assertEqual(workstation.set_alert_template().pk, alert_templates[1].pk)
 | 
			
		||||
        self.assertEqual(server.set_alert_template().pk, alert_templates[2].pk)
 | 
			
		||||
 | 
			
		||||
    @patch("agents.tasks.sleep")
 | 
			
		||||
    @patch("core.models.CoreSettings.send_mail")
 | 
			
		||||
@@ -523,7 +527,7 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
        # call outages task and no alert should be created
 | 
			
		||||
        agent_outages_task()
 | 
			
		||||
 | 
			
		||||
        self.assertEquals(Alert.objects.count(), 0)
 | 
			
		||||
        self.assertEqual(Alert.objects.count(), 0)
 | 
			
		||||
 | 
			
		||||
        # set overdue_dashboard_alert and alert should be created
 | 
			
		||||
        agent_dashboard_alert.overdue_dashboard_alert = True
 | 
			
		||||
@@ -574,22 +578,22 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
        agent_outages_task()
 | 
			
		||||
 | 
			
		||||
        # should have created 6 alerts
 | 
			
		||||
        self.assertEquals(Alert.objects.count(), 6)
 | 
			
		||||
        self.assertEqual(Alert.objects.count(), 6)
 | 
			
		||||
 | 
			
		||||
        # other specific agents should have created alerts
 | 
			
		||||
        self.assertEquals(Alert.objects.filter(agent=agent_dashboard_alert).count(), 1)
 | 
			
		||||
        self.assertEquals(Alert.objects.filter(agent=agent_text_alert).count(), 1)
 | 
			
		||||
        self.assertEquals(Alert.objects.filter(agent=agent_email_alert).count(), 1)
 | 
			
		||||
        self.assertEquals(Alert.objects.filter(agent=agent_template_email).count(), 1)
 | 
			
		||||
        self.assertEquals(
 | 
			
		||||
        self.assertEqual(Alert.objects.filter(agent=agent_dashboard_alert).count(), 1)
 | 
			
		||||
        self.assertEqual(Alert.objects.filter(agent=agent_text_alert).count(), 1)
 | 
			
		||||
        self.assertEqual(Alert.objects.filter(agent=agent_email_alert).count(), 1)
 | 
			
		||||
        self.assertEqual(Alert.objects.filter(agent=agent_template_email).count(), 1)
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            Alert.objects.filter(agent=agent_template_dashboard).count(), 1
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEquals(Alert.objects.filter(agent=agent_template_text).count(), 1)
 | 
			
		||||
        self.assertEquals(Alert.objects.filter(agent=agent_template_blank).count(), 0)
 | 
			
		||||
        self.assertEqual(Alert.objects.filter(agent=agent_template_text).count(), 1)
 | 
			
		||||
        self.assertEqual(Alert.objects.filter(agent=agent_template_blank).count(), 0)
 | 
			
		||||
 | 
			
		||||
        # check if email and text tasks were called
 | 
			
		||||
        self.assertEquals(outage_email.call_count, 2)
 | 
			
		||||
        self.assertEquals(outage_sms.call_count, 2)
 | 
			
		||||
        self.assertEqual(outage_email.call_count, 2)
 | 
			
		||||
        self.assertEqual(outage_sms.call_count, 2)
 | 
			
		||||
 | 
			
		||||
        outage_sms.assert_any_call(
 | 
			
		||||
            pk=Alert.objects.get(agent=agent_text_alert).pk, alert_interval=None
 | 
			
		||||
@@ -630,7 +634,7 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        # calling agent outage task again shouldn't create duplicate alerts and won't send alerts
 | 
			
		||||
        agent_outages_task()
 | 
			
		||||
        self.assertEquals(Alert.objects.count(), 6)
 | 
			
		||||
        self.assertEqual(Alert.objects.count(), 6)
 | 
			
		||||
 | 
			
		||||
        # test periodic notification
 | 
			
		||||
        # change email/text sent to sometime in the past
 | 
			
		||||
@@ -674,8 +678,6 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
        agent_template_email = Agent.objects.get(pk=agent_template_email.pk)
 | 
			
		||||
 | 
			
		||||
        # have the two agents checkin
 | 
			
		||||
        url = "/api/v3/checkin/"
 | 
			
		||||
 | 
			
		||||
        agent_template_text.version = settings.LATEST_AGENT_VER
 | 
			
		||||
        agent_template_text.last_seen = djangotime.now()
 | 
			
		||||
        agent_template_text.save()
 | 
			
		||||
@@ -685,7 +687,7 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
        agent_template_email.save()
 | 
			
		||||
 | 
			
		||||
        cache_db_fields_task()
 | 
			
		||||
        handle_resolved_stuff()
 | 
			
		||||
        resolve_alerts_task()
 | 
			
		||||
 | 
			
		||||
        recovery_sms.assert_called_with(
 | 
			
		||||
            pk=Alert.objects.get(agent=agent_template_text).pk
 | 
			
		||||
@@ -753,7 +755,7 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
            "alerts.AlertTemplate",
 | 
			
		||||
            is_active=True,
 | 
			
		||||
            check_always_email=True,
 | 
			
		||||
            check_email_alert_severity=["warning"],
 | 
			
		||||
            check_email_alert_severity=[AlertSeverity.WARNING],
 | 
			
		||||
        )
 | 
			
		||||
        agent_template_email.client.alert_template = alert_template_email
 | 
			
		||||
        agent_template_email.client.save()
 | 
			
		||||
@@ -764,8 +766,12 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
            is_active=True,
 | 
			
		||||
            check_always_alert=True,
 | 
			
		||||
            check_always_text=True,
 | 
			
		||||
            check_dashboard_alert_severity=["info", "warning", "error"],
 | 
			
		||||
            check_text_alert_severity=["error"],
 | 
			
		||||
            check_dashboard_alert_severity=[
 | 
			
		||||
                AlertSeverity.INFO,
 | 
			
		||||
                AlertSeverity.WARNING,
 | 
			
		||||
                AlertSeverity.ERROR,
 | 
			
		||||
            ],
 | 
			
		||||
            check_text_alert_severity=[AlertSeverity.ERROR],
 | 
			
		||||
        )
 | 
			
		||||
        agent_template_dashboard_text.client.alert_template = (
 | 
			
		||||
            alert_template_dashboard_text
 | 
			
		||||
@@ -789,7 +795,7 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
            "checks.CheckResult",
 | 
			
		||||
            assigned_check=check_agent,
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            alert_severity="warning",
 | 
			
		||||
            alert_severity=AlertSeverity.WARNING,
 | 
			
		||||
        )
 | 
			
		||||
        check_template_email = baker.make_recipe(
 | 
			
		||||
            "checks.cpuload_check", agent=agent_template_email
 | 
			
		||||
@@ -840,8 +846,8 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # test agent with check that has alert settings
 | 
			
		||||
        check_agent_result.alert_severity = "warning"
 | 
			
		||||
        check_agent_result.status = "failing"
 | 
			
		||||
        check_agent_result.alert_severity = AlertSeverity.WARNING
 | 
			
		||||
        check_agent_result.status = CheckStatus.FAILING
 | 
			
		||||
 | 
			
		||||
        Alert.handle_alert_failure(check_agent_result)
 | 
			
		||||
 | 
			
		||||
@@ -902,7 +908,7 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
        outage_sms.assert_not_called
 | 
			
		||||
 | 
			
		||||
        # update check alert severity to error
 | 
			
		||||
        check_template_dashboard_text_result.alert_severity = "error"
 | 
			
		||||
        check_template_dashboard_text_result.alert_severity = AlertSeverity.ERROR
 | 
			
		||||
        check_template_dashboard_text_result.save()
 | 
			
		||||
 | 
			
		||||
        # now should trigger alert
 | 
			
		||||
@@ -942,7 +948,7 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
        send_email.assert_not_called()
 | 
			
		||||
        send_sms.assert_not_called()
 | 
			
		||||
 | 
			
		||||
        self.assertEquals(
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            Alert.objects.filter(assigned_check=check_template_email).count(), 1
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@@ -1061,7 +1067,7 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
            "alerts.AlertTemplate",
 | 
			
		||||
            is_active=True,
 | 
			
		||||
            task_always_email=True,
 | 
			
		||||
            task_email_alert_severity=["warning"],
 | 
			
		||||
            task_email_alert_severity=[AlertSeverity.WARNING],
 | 
			
		||||
        )
 | 
			
		||||
        agent_template_email.client.alert_template = alert_template_email
 | 
			
		||||
        agent_template_email.client.save()
 | 
			
		||||
@@ -1072,8 +1078,12 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
            is_active=True,
 | 
			
		||||
            task_always_alert=True,
 | 
			
		||||
            task_always_text=True,
 | 
			
		||||
            task_dashboard_alert_severity=["info", "warning", "error"],
 | 
			
		||||
            task_text_alert_severity=["error"],
 | 
			
		||||
            task_dashboard_alert_severity=[
 | 
			
		||||
                AlertSeverity.INFO,
 | 
			
		||||
                AlertSeverity.WARNING,
 | 
			
		||||
                AlertSeverity.ERROR,
 | 
			
		||||
            ],
 | 
			
		||||
            task_text_alert_severity=[AlertSeverity.ERROR],
 | 
			
		||||
        )
 | 
			
		||||
        agent_template_dashboard_text.client.alert_template = (
 | 
			
		||||
            alert_template_dashboard_text
 | 
			
		||||
@@ -1092,7 +1102,7 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
            email_alert=True,
 | 
			
		||||
            text_alert=True,
 | 
			
		||||
            dashboard_alert=True,
 | 
			
		||||
            alert_severity="warning",
 | 
			
		||||
            alert_severity=AlertSeverity.WARNING,
 | 
			
		||||
        )
 | 
			
		||||
        task_agent_result = baker.make(
 | 
			
		||||
            "autotasks.TaskResult", agent=agent, task=task_agent
 | 
			
		||||
@@ -1100,7 +1110,7 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
        task_template_email = baker.make(
 | 
			
		||||
            "autotasks.AutomatedTask",
 | 
			
		||||
            agent=agent_template_email,
 | 
			
		||||
            alert_severity="warning",
 | 
			
		||||
            alert_severity=AlertSeverity.WARNING,
 | 
			
		||||
        )
 | 
			
		||||
        task_template_email_result = baker.make(
 | 
			
		||||
            "autotasks.TaskResult", agent=agent_template_email, task=task_template_email
 | 
			
		||||
@@ -1108,7 +1118,7 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
        task_template_dashboard_text = baker.make(
 | 
			
		||||
            "autotasks.AutomatedTask",
 | 
			
		||||
            agent=agent_template_dashboard_text,
 | 
			
		||||
            alert_severity="info",
 | 
			
		||||
            alert_severity=AlertSeverity.INFO,
 | 
			
		||||
        )
 | 
			
		||||
        task_template_dashboard_text_result = baker.make(
 | 
			
		||||
            "autotasks.TaskResult",
 | 
			
		||||
@@ -1118,13 +1128,15 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
        task_template_blank = baker.make(
 | 
			
		||||
            "autotasks.AutomatedTask",
 | 
			
		||||
            agent=agent_template_blank,
 | 
			
		||||
            alert_severity="error",
 | 
			
		||||
            alert_severity=AlertSeverity.ERROR,
 | 
			
		||||
        )
 | 
			
		||||
        task_template_blank_result = baker.make(
 | 
			
		||||
            "autotasks.TaskResult", agent=agent_template_blank, task=task_template_blank
 | 
			
		||||
        )
 | 
			
		||||
        task_no_settings = baker.make(
 | 
			
		||||
            "autotasks.AutomatedTask", agent=agent_no_settings, alert_severity="warning"
 | 
			
		||||
            "autotasks.AutomatedTask",
 | 
			
		||||
            agent=agent_no_settings,
 | 
			
		||||
            alert_severity=AlertSeverity.WARNING,
 | 
			
		||||
        )
 | 
			
		||||
        task_no_settings_result = baker.make(
 | 
			
		||||
            "autotasks.TaskResult", agent=agent_no_settings, task=task_no_settings
 | 
			
		||||
@@ -1202,7 +1214,7 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
        outage_sms.assert_not_called
 | 
			
		||||
 | 
			
		||||
        # update task alert seveity to error
 | 
			
		||||
        task_template_dashboard_text.alert_severity = "error"
 | 
			
		||||
        task_template_dashboard_text.alert_severity = AlertSeverity.ERROR
 | 
			
		||||
        task_template_dashboard_text.save()
 | 
			
		||||
 | 
			
		||||
        # now should trigger alert
 | 
			
		||||
@@ -1244,7 +1256,7 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
        send_email.assert_not_called()
 | 
			
		||||
        send_sms.assert_not_called()
 | 
			
		||||
 | 
			
		||||
        self.assertEquals(
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            Alert.objects.filter(assigned_task=task_template_email).count(), 1
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@@ -1360,7 +1372,7 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
    def test_alert_actions(
 | 
			
		||||
        self, recovery_sms, recovery_email, outage_email, outage_sms, nats_cmd
 | 
			
		||||
    ):
 | 
			
		||||
 | 
			
		||||
        from agents.models import AgentHistory
 | 
			
		||||
        from agents.tasks import agent_outages_task
 | 
			
		||||
 | 
			
		||||
        # Setup cmd mock
 | 
			
		||||
@@ -1386,9 +1398,12 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
            agent_script_actions=False,
 | 
			
		||||
            action=failure_action,
 | 
			
		||||
            action_timeout=30,
 | 
			
		||||
            action_args=["hello", "world"],
 | 
			
		||||
            action_env_vars=["hello=world", "foo=bar"],
 | 
			
		||||
            resolved_action=resolved_action,
 | 
			
		||||
            resolved_action_timeout=35,
 | 
			
		||||
            resolved_action_args=["nice_arg"],
 | 
			
		||||
            resolved_action_env_vars=["resolved=action", "env=vars"],
 | 
			
		||||
        )
 | 
			
		||||
        agent.client.alert_template = alert_template
 | 
			
		||||
        agent.client.save()
 | 
			
		||||
@@ -1409,8 +1424,13 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
        data = {
 | 
			
		||||
            "func": "runscriptfull",
 | 
			
		||||
            "timeout": 30,
 | 
			
		||||
            "script_args": [],
 | 
			
		||||
            "script_args": ["hello", "world"],
 | 
			
		||||
            "payload": {"code": failure_action.code, "shell": failure_action.shell},
 | 
			
		||||
            "run_as_user": False,
 | 
			
		||||
            "env_vars": ["hello=world", "foo=bar"],
 | 
			
		||||
            "id": AgentHistory.objects.last().pk,  # type: ignore
 | 
			
		||||
            "nushell_enable_config": settings.NUSHELL_ENABLE_CONFIG,
 | 
			
		||||
            "deno_default_permissions": settings.DENO_DEFAULT_PERMISSIONS,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        nats_cmd.assert_called_with(data, timeout=30, wait=True)
 | 
			
		||||
@@ -1431,7 +1451,7 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
        agent.save()
 | 
			
		||||
 | 
			
		||||
        cache_db_fields_task()
 | 
			
		||||
        handle_resolved_stuff()
 | 
			
		||||
        resolve_alerts_task()
 | 
			
		||||
 | 
			
		||||
        # this is what data should be
 | 
			
		||||
        data = {
 | 
			
		||||
@@ -1439,6 +1459,11 @@ class TestAlertTasks(TacticalTestCase):
 | 
			
		||||
            "timeout": 35,
 | 
			
		||||
            "script_args": ["nice_arg"],
 | 
			
		||||
            "payload": {"code": resolved_action.code, "shell": resolved_action.shell},
 | 
			
		||||
            "run_as_user": False,
 | 
			
		||||
            "env_vars": ["resolved=action", "env=vars"],
 | 
			
		||||
            "id": AgentHistory.objects.last().pk,  # type: ignore
 | 
			
		||||
            "nushell_enable_config": settings.NUSHELL_ENABLE_CONFIG,
 | 
			
		||||
            "deno_default_permissions": settings.DENO_DEFAULT_PERMISSIONS,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        nats_cmd.assert_called_with(data, timeout=35, wait=True)
 | 
			
		||||
@@ -1509,22 +1534,25 @@ class TestAlertPermissions(TacticalTestCase):
 | 
			
		||||
        tasks = baker.make("autotasks.AutomatedTask", agent=cycle(agents), _quantity=3)
 | 
			
		||||
        baker.make(
 | 
			
		||||
            "alerts.Alert",
 | 
			
		||||
            alert_type="task",
 | 
			
		||||
            alert_type=AlertType.TASK,
 | 
			
		||||
            agent=cycle(agents),
 | 
			
		||||
            assigned_task=cycle(tasks),
 | 
			
		||||
            _quantity=3,
 | 
			
		||||
        )
 | 
			
		||||
        baker.make(
 | 
			
		||||
            "alerts.Alert",
 | 
			
		||||
            alert_type="check",
 | 
			
		||||
            alert_type=AlertType.CHECK,
 | 
			
		||||
            agent=cycle(agents),
 | 
			
		||||
            assigned_check=cycle(checks),
 | 
			
		||||
            _quantity=3,
 | 
			
		||||
        )
 | 
			
		||||
        baker.make(
 | 
			
		||||
            "alerts.Alert", alert_type="availability", agent=cycle(agents), _quantity=3
 | 
			
		||||
            "alerts.Alert",
 | 
			
		||||
            alert_type=AlertType.AVAILABILITY,
 | 
			
		||||
            agent=cycle(agents),
 | 
			
		||||
            _quantity=3,
 | 
			
		||||
        )
 | 
			
		||||
        baker.make("alerts.Alert", alert_type="custom", _quantity=4)
 | 
			
		||||
        baker.make("alerts.Alert", alert_type=AlertType.CUSTOM, _quantity=4)
 | 
			
		||||
 | 
			
		||||
        # test super user access
 | 
			
		||||
        r = self.check_authorized_superuser("patch", f"{base_url}/")
 | 
			
		||||
@@ -1568,22 +1596,27 @@ class TestAlertPermissions(TacticalTestCase):
 | 
			
		||||
        tasks = baker.make("autotasks.AutomatedTask", agent=cycle(agents), _quantity=3)
 | 
			
		||||
        alert_tasks = baker.make(
 | 
			
		||||
            "alerts.Alert",
 | 
			
		||||
            alert_type="task",
 | 
			
		||||
            alert_type=AlertType.TASK,
 | 
			
		||||
            agent=cycle(agents),
 | 
			
		||||
            assigned_task=cycle(tasks),
 | 
			
		||||
            _quantity=3,
 | 
			
		||||
        )
 | 
			
		||||
        alert_checks = baker.make(
 | 
			
		||||
            "alerts.Alert",
 | 
			
		||||
            alert_type="check",
 | 
			
		||||
            alert_type=AlertType.CHECK,
 | 
			
		||||
            agent=cycle(agents),
 | 
			
		||||
            assigned_check=cycle(checks),
 | 
			
		||||
            _quantity=3,
 | 
			
		||||
        )
 | 
			
		||||
        alert_agents = baker.make(
 | 
			
		||||
            "alerts.Alert", alert_type="availability", agent=cycle(agents), _quantity=3
 | 
			
		||||
            "alerts.Alert",
 | 
			
		||||
            alert_type=AlertType.AVAILABILITY,
 | 
			
		||||
            agent=cycle(agents),
 | 
			
		||||
            _quantity=3,
 | 
			
		||||
        )
 | 
			
		||||
        alert_custom = baker.make(
 | 
			
		||||
            "alerts.Alert", alert_type=AlertType.CUSTOM, _quantity=4
 | 
			
		||||
        )
 | 
			
		||||
        alert_custom = baker.make("alerts.Alert", alert_type="custom", _quantity=4)
 | 
			
		||||
 | 
			
		||||
        # alert task url
 | 
			
		||||
        task_url = f"{base_url}/{alert_tasks[0].id}/"  # for agent
 | 
			
		||||
@@ -1604,8 +1637,7 @@ class TestAlertPermissions(TacticalTestCase):
 | 
			
		||||
            unauthorized_task_url,
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        for method in ["get", "put", "delete"]:
 | 
			
		||||
 | 
			
		||||
        for method in ("get", "put", "delete"):
 | 
			
		||||
            # test superuser access
 | 
			
		||||
            for url in authorized_urls:
 | 
			
		||||
                self.check_authorized_superuser(method, url)
 | 
			
		||||
@@ -1660,7 +1692,7 @@ class TestAlertPermissions(TacticalTestCase):
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        alerts = baker.make(
 | 
			
		||||
            "alerts.Alert",
 | 
			
		||||
            alert_type="availability",
 | 
			
		||||
            alert_type=AlertType.AVAILABILITY,
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            resolved=False,
 | 
			
		||||
            _quantity=3,
 | 
			
		||||
@@ -1674,7 +1706,7 @@ class TestAlertPermissions(TacticalTestCase):
 | 
			
		||||
        # make sure only 1 alert is not resolved
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            Alert.objects.filter(
 | 
			
		||||
                alert_type="availability", agent=agent, resolved=False
 | 
			
		||||
                alert_type=AlertType.AVAILABILITY, agent=agent, resolved=False
 | 
			
		||||
            ).count(),
 | 
			
		||||
            1,
 | 
			
		||||
        )
 | 
			
		||||
@@ -1684,7 +1716,7 @@ class TestAlertPermissions(TacticalTestCase):
 | 
			
		||||
        check = baker.make_recipe("checks.diskspace_check", agent=agent)
 | 
			
		||||
        alerts = baker.make(
 | 
			
		||||
            "alerts.Alert",
 | 
			
		||||
            alert_type="check",
 | 
			
		||||
            alert_type=AlertType.CHECK,
 | 
			
		||||
            assigned_check=check,
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            resolved=False,
 | 
			
		||||
@@ -1699,7 +1731,7 @@ class TestAlertPermissions(TacticalTestCase):
 | 
			
		||||
        # make sure only 1 alert is not resolved
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            Alert.objects.filter(
 | 
			
		||||
                alert_type="check", agent=agent, resolved=False
 | 
			
		||||
                alert_type=AlertType.CHECK, agent=agent, resolved=False
 | 
			
		||||
            ).count(),
 | 
			
		||||
            1,
 | 
			
		||||
        )
 | 
			
		||||
@@ -1709,7 +1741,7 @@ class TestAlertPermissions(TacticalTestCase):
 | 
			
		||||
        task = baker.make("autotasks.AutomatedTask", agent=agent)
 | 
			
		||||
        alerts = baker.make(
 | 
			
		||||
            "alerts.Alert",
 | 
			
		||||
            alert_type="task",
 | 
			
		||||
            alert_type=AlertType.TASK,
 | 
			
		||||
            assigned_task=task,
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            resolved=False,
 | 
			
		||||
@@ -1724,7 +1756,7 @@ class TestAlertPermissions(TacticalTestCase):
 | 
			
		||||
        # make sure only 1 alert is not resolved
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            Alert.objects.filter(
 | 
			
		||||
                alert_type="task", agent=agent, resolved=False
 | 
			
		||||
                alert_type=AlertType.TASK, agent=agent, resolved=False
 | 
			
		||||
            ).count(),
 | 
			
		||||
            1,
 | 
			
		||||
        )
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ from rest_framework.permissions import IsAuthenticated
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.utils import notify_error
 | 
			
		||||
from tacticalrmm.helpers import notify_error
 | 
			
		||||
 | 
			
		||||
from .models import Alert, AlertTemplate
 | 
			
		||||
from .permissions import AlertPerms, AlertTemplatePerms
 | 
			
		||||
@@ -23,15 +23,18 @@ class GetAddAlerts(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated, AlertPerms]
 | 
			
		||||
 | 
			
		||||
    def patch(self, request):
 | 
			
		||||
 | 
			
		||||
        # top 10 alerts for dashboard icon
 | 
			
		||||
        if "top" in request.data.keys():
 | 
			
		||||
            alerts = Alert.objects.filter(
 | 
			
		||||
                resolved=False, snoozed=False, hidden=False
 | 
			
		||||
            ).order_by("alert_time")[: int(request.data["top"])]
 | 
			
		||||
            count = Alert.objects.filter(
 | 
			
		||||
                resolved=False, snoozed=False, hidden=False
 | 
			
		||||
            ).count()
 | 
			
		||||
            alerts = (
 | 
			
		||||
                Alert.objects.filter_by_role(request.user)
 | 
			
		||||
                .filter(resolved=False, snoozed=False, hidden=False)
 | 
			
		||||
                .order_by("alert_time")[: int(request.data["top"])]
 | 
			
		||||
            )
 | 
			
		||||
            count = (
 | 
			
		||||
                Alert.objects.filter_by_role(request.user)
 | 
			
		||||
                .filter(resolved=False, snoozed=False, hidden=False)
 | 
			
		||||
                .count()
 | 
			
		||||
            )
 | 
			
		||||
            return Response(
 | 
			
		||||
                {
 | 
			
		||||
                    "alerts_count": count,
 | 
			
		||||
@@ -41,13 +44,13 @@ class GetAddAlerts(APIView):
 | 
			
		||||
 | 
			
		||||
        elif any(
 | 
			
		||||
            key
 | 
			
		||||
            in [
 | 
			
		||||
            in (
 | 
			
		||||
                "timeFilter",
 | 
			
		||||
                "clientFilter",
 | 
			
		||||
                "severityFilter",
 | 
			
		||||
                "resolvedFilter",
 | 
			
		||||
                "snoozedFilter",
 | 
			
		||||
            ]
 | 
			
		||||
            )
 | 
			
		||||
            for key in request.data.keys()
 | 
			
		||||
        ):
 | 
			
		||||
            clientFilter = Q()
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										0
									
								
								api/tacticalrmm/apiv3/tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								api/tacticalrmm/apiv3/tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -1,11 +1,8 @@
 | 
			
		||||
import json
 | 
			
		||||
import os
 | 
			
		||||
 | 
			
		||||
from autotasks.models import TaskResult
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from model_bakery import baker
 | 
			
		||||
 | 
			
		||||
from autotasks.models import TaskResult
 | 
			
		||||
from tacticalrmm.constants import CustomFieldModel, CustomFieldType, TaskStatus
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -62,7 +59,7 @@ class TestAPIv3(TacticalTestCase):
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(r.data["check_interval"], 20)
 | 
			
		||||
        self.assertEquals(len(r.data["checks"]), 2)
 | 
			
		||||
        self.assertEqual(len(r.data["checks"]), 2)
 | 
			
		||||
 | 
			
		||||
        url = "/api/v3/Maj34ACb324j234asdj2n34kASDjh34-DESKTOPTEST123/checkrunner/"
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
@@ -70,24 +67,6 @@ class TestAPIv3(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    def test_sysinfo(self):
 | 
			
		||||
        # TODO replace this with golang wmi sample data
 | 
			
		||||
 | 
			
		||||
        url = "/api/v3/sysinfo/"
 | 
			
		||||
        with open(
 | 
			
		||||
            os.path.join(
 | 
			
		||||
                settings.BASE_DIR, "tacticalrmm/test_data/wmi_python_agent.json"
 | 
			
		||||
            )
 | 
			
		||||
        ) as f:
 | 
			
		||||
            wmi_py = json.load(f)
 | 
			
		||||
 | 
			
		||||
        payload = {"agent_id": self.agent.agent_id, "sysinfo": wmi_py}
 | 
			
		||||
 | 
			
		||||
        r = self.client.patch(url, payload, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("patch", url)
 | 
			
		||||
 | 
			
		||||
    def test_checkrunner_interval(self):
 | 
			
		||||
        url = f"/api/v3/{self.agent.agent_id}/checkinterval/"
 | 
			
		||||
        r = self.client.get(url, format="json")
 | 
			
		||||
@@ -98,9 +77,7 @@ class TestAPIv3(TacticalTestCase):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # add check to agent with check interval set
 | 
			
		||||
        check = baker.make_recipe(
 | 
			
		||||
            "checks.ping_check", agent=self.agent, run_interval=30
 | 
			
		||||
        )
 | 
			
		||||
        baker.make_recipe("checks.ping_check", agent=self.agent, run_interval=30)
 | 
			
		||||
 | 
			
		||||
        r = self.client.get(url, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
@@ -110,7 +87,7 @@ class TestAPIv3(TacticalTestCase):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # minimum check run interval is 15 seconds
 | 
			
		||||
        check = baker.make_recipe("checks.ping_check", agent=self.agent, run_interval=5)
 | 
			
		||||
        baker.make_recipe("checks.ping_check", agent=self.agent, run_interval=5)
 | 
			
		||||
 | 
			
		||||
        r = self.client.get(url, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
@@ -137,8 +114,6 @@ class TestAPIv3(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(len(r.json()["checks"]), 15)
 | 
			
		||||
 | 
			
		||||
    def test_task_runner_get(self):
 | 
			
		||||
        from autotasks.serializers import TaskGOGetSerializer
 | 
			
		||||
 | 
			
		||||
        r = self.client.get("/api/v3/500/asdf9df9dfdf/taskrunner/")
 | 
			
		||||
        self.assertEqual(r.status_code, 404)
 | 
			
		||||
 | 
			
		||||
@@ -152,8 +127,15 @@ class TestAPIv3(TacticalTestCase):
 | 
			
		||||
                "script": script.id,
 | 
			
		||||
                "script_args": ["test"],
 | 
			
		||||
                "timeout": 30,
 | 
			
		||||
                "env_vars": ["hello=world", "foo=bar"],
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                "type": "script",
 | 
			
		||||
                "script": 3,
 | 
			
		||||
                "script_args": [],
 | 
			
		||||
                "timeout": 30,
 | 
			
		||||
                "env_vars": ["hello=world", "foo=bar"],
 | 
			
		||||
            },
 | 
			
		||||
            {"type": "script", "script": 3, "script_args": [], "timeout": 30},
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
@@ -163,7 +145,6 @@ class TestAPIv3(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(TaskGOGetSerializer(task).data, r.data)
 | 
			
		||||
 | 
			
		||||
    def test_task_runner_results(self):
 | 
			
		||||
        from agents.models import AgentCustomField
 | 
			
		||||
@@ -188,7 +169,9 @@ class TestAPIv3(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        r = self.client.patch(url, data)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertTrue(TaskResult.objects.get(pk=task_result.pk).status == "passing")
 | 
			
		||||
        self.assertTrue(
 | 
			
		||||
            TaskResult.objects.get(pk=task_result.pk).status == TaskStatus.PASSING
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # test failing task
 | 
			
		||||
        data = {
 | 
			
		||||
@@ -200,15 +183,28 @@ class TestAPIv3(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        r = self.client.patch(url, data)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertTrue(TaskResult.objects.get(pk=task_result.pk).status == "failing")
 | 
			
		||||
        self.assertTrue(
 | 
			
		||||
            TaskResult.objects.get(pk=task_result.pk).status == TaskStatus.FAILING
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # test collector task
 | 
			
		||||
        text = baker.make("core.CustomField", model="agent", type="text", name="Test")
 | 
			
		||||
        text = baker.make(
 | 
			
		||||
            "core.CustomField",
 | 
			
		||||
            model=CustomFieldModel.AGENT,
 | 
			
		||||
            type=CustomFieldType.TEXT,
 | 
			
		||||
            name="Test",
 | 
			
		||||
        )
 | 
			
		||||
        boolean = baker.make(
 | 
			
		||||
            "core.CustomField", model="agent", type="checkbox", name="Test1"
 | 
			
		||||
            "core.CustomField",
 | 
			
		||||
            model=CustomFieldModel.AGENT,
 | 
			
		||||
            type=CustomFieldType.CHECKBOX,
 | 
			
		||||
            name="Test1",
 | 
			
		||||
        )
 | 
			
		||||
        multiple = baker.make(
 | 
			
		||||
            "core.CustomField", model="agent", type="multiple", name="Test2"
 | 
			
		||||
            "core.CustomField",
 | 
			
		||||
            model=CustomFieldModel.AGENT,
 | 
			
		||||
            type=CustomFieldType.MULTIPLE,
 | 
			
		||||
            name="Test2",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # test text fields
 | 
			
		||||
@@ -225,7 +221,9 @@ class TestAPIv3(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        r = self.client.patch(url, data)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertTrue(TaskResult.objects.get(pk=task_result.pk).status == "failing")
 | 
			
		||||
        self.assertTrue(
 | 
			
		||||
            TaskResult.objects.get(pk=task_result.pk).status == TaskStatus.FAILING
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # test saving to text field
 | 
			
		||||
        data = {
 | 
			
		||||
@@ -237,7 +235,9 @@ class TestAPIv3(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        r = self.client.patch(url, data)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(TaskResult.objects.get(pk=task_result.pk).status, "passing")
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            TaskResult.objects.get(pk=task_result.pk).status, TaskStatus.PASSING
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            AgentCustomField.objects.get(field=text, agent=task.agent).value,
 | 
			
		||||
            "the last line",
 | 
			
		||||
@@ -256,7 +256,9 @@ class TestAPIv3(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        r = self.client.patch(url, data)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(TaskResult.objects.get(pk=task_result.pk).status, "passing")
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            TaskResult.objects.get(pk=task_result.pk).status, TaskStatus.PASSING
 | 
			
		||||
        )
 | 
			
		||||
        self.assertTrue(
 | 
			
		||||
            AgentCustomField.objects.get(field=boolean, agent=task.agent).value
 | 
			
		||||
        )
 | 
			
		||||
@@ -274,7 +276,9 @@ class TestAPIv3(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        r = self.client.patch(url, data)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(TaskResult.objects.get(pk=task_result.pk).status, "passing")
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            TaskResult.objects.get(pk=task_result.pk).status, TaskStatus.PASSING
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            AgentCustomField.objects.get(field=multiple, agent=task.agent).value,
 | 
			
		||||
            ["this", "is", "an", "array"],
 | 
			
		||||
@@ -290,8 +294,16 @@ class TestAPIv3(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        r = self.client.patch(url, data)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(TaskResult.objects.get(pk=task_result.pk).status, "passing")
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            TaskResult.objects.get(pk=task_result.pk).status, TaskStatus.PASSING
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            AgentCustomField.objects.get(field=multiple, agent=task.agent).value,
 | 
			
		||||
            ["this"],
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_get_agent_config(self):
 | 
			
		||||
        agent = baker.make_recipe("agents.online_agent")
 | 
			
		||||
        url = f"/api/v3/{agent.agent_id}/config/"
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
@@ -9,7 +9,6 @@ urlpatterns = [
 | 
			
		||||
    path("<str:agentid>/checkinterval/", views.CheckRunnerInterval.as_view()),
 | 
			
		||||
    path("<int:pk>/<str:agentid>/taskrunner/", views.TaskRunner.as_view()),
 | 
			
		||||
    path("meshexe/", views.MeshExe.as_view()),
 | 
			
		||||
    path("sysinfo/", views.SysInfo.as_view()),
 | 
			
		||||
    path("newagent/", views.NewAgent.as_view()),
 | 
			
		||||
    path("software/", views.Software.as_view()),
 | 
			
		||||
    path("installer/", views.Installer.as_view()),
 | 
			
		||||
@@ -20,4 +19,5 @@ urlpatterns = [
 | 
			
		||||
    path("superseded/", views.SupersededWinUpdate.as_view()),
 | 
			
		||||
    path("<int:pk>/chocoresult/", views.ChocoResult.as_view()),
 | 
			
		||||
    path("<int:pk>/<str:agentid>/histresult/", views.AgentHistoryResult.as_view()),
 | 
			
		||||
    path("<str:agentid>/config/", views.AgentConfig.as_view()),
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										33
									
								
								api/tacticalrmm/apiv3/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								api/tacticalrmm/apiv3/utils.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
			
		||||
import random
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.structs import AgentCheckInConfig
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_agent_config() -> AgentCheckInConfig:
 | 
			
		||||
    return AgentCheckInConfig(
 | 
			
		||||
        checkin_hello=random.randint(*getattr(settings, "CHECKIN_HELLO", (30, 60))),
 | 
			
		||||
        checkin_agentinfo=random.randint(
 | 
			
		||||
            *getattr(settings, "CHECKIN_AGENTINFO", (200, 400))
 | 
			
		||||
        ),
 | 
			
		||||
        checkin_winsvc=random.randint(
 | 
			
		||||
            *getattr(settings, "CHECKIN_WINSVC", (2400, 3000))
 | 
			
		||||
        ),
 | 
			
		||||
        checkin_pubip=random.randint(*getattr(settings, "CHECKIN_PUBIP", (300, 500))),
 | 
			
		||||
        checkin_disks=random.randint(*getattr(settings, "CHECKIN_DISKS", (1000, 2000))),
 | 
			
		||||
        checkin_sw=random.randint(*getattr(settings, "CHECKIN_SW", (2800, 3500))),
 | 
			
		||||
        checkin_wmi=random.randint(*getattr(settings, "CHECKIN_WMI", (3000, 4000))),
 | 
			
		||||
        checkin_syncmesh=random.randint(
 | 
			
		||||
            *getattr(settings, "CHECKIN_SYNCMESH", (800, 1200))
 | 
			
		||||
        ),
 | 
			
		||||
        limit_data=getattr(settings, "LIMIT_DATA", False),
 | 
			
		||||
        install_nushell=getattr(settings, "INSTALL_NUSHELL", False),
 | 
			
		||||
        install_nushell_version=getattr(settings, "INSTALL_NUSHELL_VERSION", ""),
 | 
			
		||||
        install_nushell_url=getattr(settings, "INSTALL_NUSHELL_URL", ""),
 | 
			
		||||
        nushell_enable_config=getattr(settings, "NUSHELL_ENABLE_CONFIG", False),
 | 
			
		||||
        install_deno=getattr(settings, "INSTALL_DENO", False),
 | 
			
		||||
        install_deno_version=getattr(settings, "INSTALL_DENO_VERSION", ""),
 | 
			
		||||
        install_deno_url=getattr(settings, "INSTALL_DENO_URL", ""),
 | 
			
		||||
        deno_default_permissions=getattr(settings, "DENO_DEFAULT_PERMISSIONS", ""),
 | 
			
		||||
    )
 | 
			
		||||
@@ -1,40 +1,63 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
import time
 | 
			
		||||
 | 
			
		||||
from accounts.models import User
 | 
			
		||||
from agents.models import Agent, AgentHistory
 | 
			
		||||
from agents.serializers import AgentHistorySerializer
 | 
			
		||||
from autotasks.models import AutomatedTask, TaskResult
 | 
			
		||||
from autotasks.serializers import TaskGOGetSerializer, TaskResultSerializer
 | 
			
		||||
from checks.models import Check, CheckResult
 | 
			
		||||
from checks.serializers import CheckRunnerGetSerializer
 | 
			
		||||
from core.utils import get_core_settings
 | 
			
		||||
from core.utils import download_mesh_agent, get_mesh_device_id, get_mesh_ws_url
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.db.models import Prefetch
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from logs.models import DebugLog, PendingAction
 | 
			
		||||
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.views import APIView
 | 
			
		||||
from software.models import InstalledSoftware
 | 
			
		||||
from winupdate.models import WinUpdate, WinUpdatePolicy
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.constants import MeshAgentIdent
 | 
			
		||||
from tacticalrmm.utils import notify_error, reload_nats
 | 
			
		||||
from accounts.models import User
 | 
			
		||||
from agents.models import Agent, AgentHistory
 | 
			
		||||
from agents.serializers import AgentHistorySerializer
 | 
			
		||||
from apiv3.utils import get_agent_config
 | 
			
		||||
from autotasks.models import AutomatedTask, TaskResult
 | 
			
		||||
from autotasks.serializers import TaskGOGetSerializer, TaskResultSerializer
 | 
			
		||||
from checks.constants import CHECK_DEFER, CHECK_RESULT_DEFER
 | 
			
		||||
from checks.models import Check, CheckResult
 | 
			
		||||
from checks.serializers import CheckRunnerGetSerializer
 | 
			
		||||
from core.tasks import sync_mesh_perms_task
 | 
			
		||||
from core.utils import (
 | 
			
		||||
    download_mesh_agent,
 | 
			
		||||
    get_core_settings,
 | 
			
		||||
    get_mesh_device_id,
 | 
			
		||||
    get_mesh_ws_url,
 | 
			
		||||
    get_meshagent_url,
 | 
			
		||||
)
 | 
			
		||||
from logs.models import DebugLog, PendingAction
 | 
			
		||||
from software.models import InstalledSoftware
 | 
			
		||||
from tacticalrmm.constants import (
 | 
			
		||||
    AGENT_DEFER,
 | 
			
		||||
    TRMM_MAX_REQUEST_SIZE,
 | 
			
		||||
    AgentHistoryType,
 | 
			
		||||
    AgentMonType,
 | 
			
		||||
    AgentPlat,
 | 
			
		||||
    AuditActionType,
 | 
			
		||||
    AuditObjType,
 | 
			
		||||
    CheckStatus,
 | 
			
		||||
    DebugLogType,
 | 
			
		||||
    GoArch,
 | 
			
		||||
    MeshAgentIdent,
 | 
			
		||||
    PAStatus,
 | 
			
		||||
)
 | 
			
		||||
from tacticalrmm.helpers import make_random_password, notify_error
 | 
			
		||||
from tacticalrmm.utils import reload_nats
 | 
			
		||||
from winupdate.models import WinUpdate, WinUpdatePolicy
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CheckIn(APIView):
 | 
			
		||||
 | 
			
		||||
    authentication_classes = [TokenAuthentication]
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
    # called once during tacticalagent windows service startup
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
			
		||||
        agent = get_object_or_404(
 | 
			
		||||
            Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"]
 | 
			
		||||
        )
 | 
			
		||||
        if not agent.choco_installed:
 | 
			
		||||
            asyncio.run(agent.nats_cmd({"func": "installchoco"}, wait=False))
 | 
			
		||||
 | 
			
		||||
@@ -47,7 +70,9 @@ class SyncMeshNodeID(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
			
		||||
        agent = get_object_or_404(
 | 
			
		||||
            Agent.objects.defer(*AGENT_DEFER), 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"])
 | 
			
		||||
@@ -60,7 +85,9 @@ class Choco(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
			
		||||
        agent = get_object_or_404(
 | 
			
		||||
            Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"]
 | 
			
		||||
        )
 | 
			
		||||
        agent.choco_installed = request.data["installed"]
 | 
			
		||||
        agent.save(update_fields=["choco_installed"])
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
@@ -71,7 +98,9 @@ class WinUpdates(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
    def put(self, request):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
			
		||||
        agent = get_object_or_404(
 | 
			
		||||
            Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        needs_reboot: bool = request.data["needs_reboot"]
 | 
			
		||||
        agent.needs_reboot = needs_reboot
 | 
			
		||||
@@ -89,7 +118,7 @@ class WinUpdates(APIView):
 | 
			
		||||
            asyncio.run(agent.nats_cmd({"func": "rebootnow"}, wait=False))
 | 
			
		||||
            DebugLog.info(
 | 
			
		||||
                agent=agent,
 | 
			
		||||
                log_type="windows_updates",
 | 
			
		||||
                log_type=DebugLogType.WIN_UPDATES,
 | 
			
		||||
                message=f"{agent.hostname} is rebooting after updates were installed.",
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
@@ -97,7 +126,9 @@ class WinUpdates(APIView):
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
    def patch(self, request):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
			
		||||
        agent = get_object_or_404(
 | 
			
		||||
            Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"]
 | 
			
		||||
        )
 | 
			
		||||
        u = agent.winupdates.filter(guid=request.data["guid"]).last()  # type: ignore
 | 
			
		||||
        if not u:
 | 
			
		||||
            raise WinUpdate.DoesNotExist
 | 
			
		||||
@@ -124,8 +155,14 @@ class WinUpdates(APIView):
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
			
		||||
        updates = request.data["wua_updates"]
 | 
			
		||||
        if not updates:
 | 
			
		||||
            return notify_error("Empty payload")
 | 
			
		||||
 | 
			
		||||
        agent = get_object_or_404(
 | 
			
		||||
            Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        for update in updates:
 | 
			
		||||
            if agent.winupdates.filter(guid=update["guid"]).exists():  # type: ignore
 | 
			
		||||
                u = agent.winupdates.filter(guid=update["guid"]).last()  # type: ignore
 | 
			
		||||
@@ -164,7 +201,9 @@ class SupersededWinUpdate(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
			
		||||
        agent = get_object_or_404(
 | 
			
		||||
            Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"]
 | 
			
		||||
        )
 | 
			
		||||
        updates = agent.winupdates.filter(guid=request.data["guid"])  # type: ignore
 | 
			
		||||
        for u in updates:
 | 
			
		||||
            u.delete()
 | 
			
		||||
@@ -177,12 +216,19 @@ class RunChecks(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, agentid):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=agentid)
 | 
			
		||||
        agent = get_object_or_404(
 | 
			
		||||
            Agent.objects.defer(*AGENT_DEFER).prefetch_related(
 | 
			
		||||
                Prefetch("agentchecks", queryset=Check.objects.select_related("script"))
 | 
			
		||||
            ),
 | 
			
		||||
            agent_id=agentid,
 | 
			
		||||
        )
 | 
			
		||||
        checks = agent.get_checks_with_policies(exclude_overridden=True)
 | 
			
		||||
        ret = {
 | 
			
		||||
            "agent": agent.pk,
 | 
			
		||||
            "check_interval": agent.check_interval,
 | 
			
		||||
            "checks": CheckRunnerGetSerializer(checks, many=True).data,
 | 
			
		||||
            "checks": CheckRunnerGetSerializer(
 | 
			
		||||
                checks, context={"agent": agent}, many=True
 | 
			
		||||
            ).data,
 | 
			
		||||
        }
 | 
			
		||||
        return Response(ret)
 | 
			
		||||
 | 
			
		||||
@@ -192,7 +238,12 @@ class CheckRunner(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, agentid):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=agentid)
 | 
			
		||||
        agent = get_object_or_404(
 | 
			
		||||
            Agent.objects.defer(*AGENT_DEFER).prefetch_related(
 | 
			
		||||
                Prefetch("agentchecks", queryset=Check.objects.select_related("script"))
 | 
			
		||||
            ),
 | 
			
		||||
            agent_id=agentid,
 | 
			
		||||
        )
 | 
			
		||||
        checks = agent.get_checks_with_policies(exclude_overridden=True)
 | 
			
		||||
 | 
			
		||||
        run_list = [
 | 
			
		||||
@@ -206,9 +257,7 @@ class CheckRunner(APIView):
 | 
			
		||||
                check.check_result.last_run
 | 
			
		||||
                < djangotime.now()
 | 
			
		||||
                - djangotime.timedelta(
 | 
			
		||||
                    seconds=check.run_interval
 | 
			
		||||
                    if check.run_interval
 | 
			
		||||
                    else agent.check_interval
 | 
			
		||||
                    seconds=check.run_interval or agent.check_interval
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
        ]
 | 
			
		||||
@@ -216,30 +265,38 @@ class CheckRunner(APIView):
 | 
			
		||||
        ret = {
 | 
			
		||||
            "agent": agent.pk,
 | 
			
		||||
            "check_interval": agent.check_run_interval(),
 | 
			
		||||
            "checks": CheckRunnerGetSerializer(run_list, many=True).data,
 | 
			
		||||
            "checks": CheckRunnerGetSerializer(
 | 
			
		||||
                run_list, context={"agent": agent}, many=True
 | 
			
		||||
            ).data,
 | 
			
		||||
        }
 | 
			
		||||
        return Response(ret)
 | 
			
		||||
 | 
			
		||||
    def patch(self, request):
 | 
			
		||||
        check = get_object_or_404(Check, pk=request.data["id"])
 | 
			
		||||
 | 
			
		||||
        if "agent_id" not in request.data.keys():
 | 
			
		||||
            return notify_error("Agent upgrade required")
 | 
			
		||||
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
			
		||||
        check = get_object_or_404(
 | 
			
		||||
            Check.objects.defer(*CHECK_DEFER),
 | 
			
		||||
            pk=request.data["id"],
 | 
			
		||||
        )
 | 
			
		||||
        agent = get_object_or_404(
 | 
			
		||||
            Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # check check result or create if doesn't exist
 | 
			
		||||
        try:
 | 
			
		||||
            check_result = CheckResult.objects.get(assigned_check=check, agent=agent)
 | 
			
		||||
        except CheckResult.DoesNotExist:
 | 
			
		||||
            check_result = CheckResult(assigned_check=check, agent=agent)
 | 
			
		||||
        # get check result or create if doesn't exist
 | 
			
		||||
        check_result, created = CheckResult.objects.defer(
 | 
			
		||||
            *CHECK_RESULT_DEFER
 | 
			
		||||
        ).get_or_create(
 | 
			
		||||
            assigned_check=check,
 | 
			
		||||
            agent=agent,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        check_result.last_run = djangotime.now()
 | 
			
		||||
        check_result.save()
 | 
			
		||||
        if created:
 | 
			
		||||
            check_result.save()
 | 
			
		||||
 | 
			
		||||
        status = check_result.handle_check(request.data)
 | 
			
		||||
        if status == "failing" and check.assignedtasks.exists():  # type: ignore
 | 
			
		||||
            for task in check.assignedtasks.all():  # type: ignore
 | 
			
		||||
        status = check_result.handle_check(request.data, check, agent)
 | 
			
		||||
        if status == CheckStatus.FAILING and check.assignedtasks.exists():
 | 
			
		||||
            for task in check.assignedtasks.all():
 | 
			
		||||
                if task.enabled:
 | 
			
		||||
                    if task.policy:
 | 
			
		||||
                        task.run_win_task(agent)
 | 
			
		||||
@@ -254,7 +311,10 @@ class CheckRunnerInterval(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, agentid):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=agentid)
 | 
			
		||||
        agent = get_object_or_404(
 | 
			
		||||
            Agent.objects.defer(*AGENT_DEFER).prefetch_related("agentchecks"),
 | 
			
		||||
            agent_id=agentid,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        return Response(
 | 
			
		||||
            {"agent": agent.pk, "check_interval": agent.check_run_interval()}
 | 
			
		||||
@@ -266,19 +326,34 @@ class TaskRunner(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, pk, agentid):
 | 
			
		||||
        _ = get_object_or_404(Agent, agent_id=agentid)
 | 
			
		||||
        agent = get_object_or_404(Agent.objects.defer(*AGENT_DEFER), agent_id=agentid)
 | 
			
		||||
        task = get_object_or_404(AutomatedTask, pk=pk)
 | 
			
		||||
        return Response(TaskGOGetSerializer(task).data)
 | 
			
		||||
        return Response(TaskGOGetSerializer(task, context={"agent": agent}).data)
 | 
			
		||||
 | 
			
		||||
    def patch(self, request, pk, agentid):
 | 
			
		||||
        from alerts.models import Alert
 | 
			
		||||
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=agentid)
 | 
			
		||||
        task = get_object_or_404(AutomatedTask, pk=pk)
 | 
			
		||||
        agent = get_object_or_404(
 | 
			
		||||
            Agent.objects.defer(*AGENT_DEFER),
 | 
			
		||||
            agent_id=agentid,
 | 
			
		||||
        )
 | 
			
		||||
        task = get_object_or_404(
 | 
			
		||||
            AutomatedTask.objects.select_related("custom_field"), pk=pk
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # check check result or create if doesn't exist
 | 
			
		||||
        content_length = request.META.get("CONTENT_LENGTH")
 | 
			
		||||
        if content_length and int(content_length) > TRMM_MAX_REQUEST_SIZE:
 | 
			
		||||
            request.data["stdout"] = ""
 | 
			
		||||
            request.data["stderr"] = "Content truncated due to excessive request size."
 | 
			
		||||
            request.data["retcode"] = 1
 | 
			
		||||
 | 
			
		||||
        # get task result or create if doesn't exist
 | 
			
		||||
        try:
 | 
			
		||||
            task_result = TaskResult.objects.get(task=task, agent=agent)
 | 
			
		||||
            task_result = (
 | 
			
		||||
                TaskResult.objects.select_related("agent")
 | 
			
		||||
                .defer("agent__services", "agent__wmi_detail")
 | 
			
		||||
                .get(task=task, agent=agent)
 | 
			
		||||
            )
 | 
			
		||||
            serializer = TaskResultSerializer(
 | 
			
		||||
                data=request.data, instance=task_result, partial=True
 | 
			
		||||
            )
 | 
			
		||||
@@ -290,7 +365,7 @@ class TaskRunner(APIView):
 | 
			
		||||
 | 
			
		||||
        AgentHistory.objects.create(
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            type="task_run",
 | 
			
		||||
            type=AgentHistoryType.TASK_RUN,
 | 
			
		||||
            command=task.name,
 | 
			
		||||
            script_results=request.data,
 | 
			
		||||
        )
 | 
			
		||||
@@ -298,14 +373,15 @@ class TaskRunner(APIView):
 | 
			
		||||
        # check if task is a collector and update the custom field
 | 
			
		||||
        if task.custom_field:
 | 
			
		||||
            if not task_result.stderr:
 | 
			
		||||
 | 
			
		||||
                task_result.save_collector_results()
 | 
			
		||||
 | 
			
		||||
                status = "passing"
 | 
			
		||||
                status = CheckStatus.PASSING
 | 
			
		||||
            else:
 | 
			
		||||
                status = "failing"
 | 
			
		||||
                status = CheckStatus.FAILING
 | 
			
		||||
        else:
 | 
			
		||||
            status = "failing" if task_result.retcode != 0 else "passing"
 | 
			
		||||
            status = (
 | 
			
		||||
                CheckStatus.FAILING if task_result.retcode != 0 else CheckStatus.PASSING
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        if task_result:
 | 
			
		||||
            task_result.status = status
 | 
			
		||||
@@ -314,7 +390,7 @@ class TaskRunner(APIView):
 | 
			
		||||
            task_result.status = status
 | 
			
		||||
            task.save(update_fields=["status"])
 | 
			
		||||
 | 
			
		||||
        if status == "passing":
 | 
			
		||||
        if status == CheckStatus.PASSING:
 | 
			
		||||
            if Alert.create_or_return_task_alert(task, agent=agent, skip_create=True):
 | 
			
		||||
                Alert.handle_alert_resolve(task_result)
 | 
			
		||||
        else:
 | 
			
		||||
@@ -323,47 +399,39 @@ class TaskRunner(APIView):
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SysInfo(APIView):
 | 
			
		||||
    authentication_classes = [TokenAuthentication]
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
    def patch(self, request):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
			
		||||
 | 
			
		||||
        if not isinstance(request.data["sysinfo"], dict):
 | 
			
		||||
            return notify_error("err")
 | 
			
		||||
 | 
			
		||||
        agent.wmi_detail = request.data["sysinfo"]
 | 
			
		||||
        agent.save(update_fields=["wmi_detail"])
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MeshExe(APIView):
 | 
			
		||||
    """Sends the mesh exe to the installer"""
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        match request.data:
 | 
			
		||||
            case {"arch": "64", "plat": "windows"}:
 | 
			
		||||
                arch = MeshAgentIdent.WIN64
 | 
			
		||||
            case {"arch": "32", "plat": "windows"}:
 | 
			
		||||
                arch = MeshAgentIdent.WIN32
 | 
			
		||||
            case {"goarch": GoArch.AMD64, "plat": AgentPlat.WINDOWS}:
 | 
			
		||||
                ident = MeshAgentIdent.WIN64
 | 
			
		||||
            case {"goarch": GoArch.i386, "plat": AgentPlat.WINDOWS}:
 | 
			
		||||
                ident = MeshAgentIdent.WIN32
 | 
			
		||||
            case {"goarch": GoArch.AMD64, "plat": AgentPlat.DARWIN} | {
 | 
			
		||||
                "goarch": GoArch.ARM64,
 | 
			
		||||
                "plat": AgentPlat.DARWIN,
 | 
			
		||||
            }:
 | 
			
		||||
                ident = MeshAgentIdent.DARWIN_UNIVERSAL
 | 
			
		||||
            case _:
 | 
			
		||||
                return notify_error("Arch not specified")
 | 
			
		||||
                return notify_error("Arch not supported")
 | 
			
		||||
 | 
			
		||||
        core = get_core_settings()
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            uri = get_mesh_ws_url()
 | 
			
		||||
            mesh_id = asyncio.run(get_mesh_device_id(uri, core.mesh_device_group))
 | 
			
		||||
            mesh_device_id: str = asyncio.run(
 | 
			
		||||
                get_mesh_device_id(uri, core.mesh_device_group)
 | 
			
		||||
            )
 | 
			
		||||
        except:
 | 
			
		||||
            return notify_error("Unable to connect to mesh to get group id information")
 | 
			
		||||
 | 
			
		||||
        if settings.DOCKER_BUILD:
 | 
			
		||||
            dl_url = f"{settings.MESH_WS_URL.replace('ws://', 'http://')}/meshagents?id={arch}&meshid={mesh_id}&installflags=0"
 | 
			
		||||
        else:
 | 
			
		||||
            dl_url = (
 | 
			
		||||
                f"{core.mesh_site}/meshagents?id={arch}&meshid={mesh_id}&installflags=0"
 | 
			
		||||
            )
 | 
			
		||||
        dl_url = get_meshagent_url(
 | 
			
		||||
            ident=ident,
 | 
			
		||||
            plat=request.data["plat"],
 | 
			
		||||
            mesh_site=core.mesh_site,
 | 
			
		||||
            mesh_device_id=mesh_device_id,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            return download_mesh_agent(dl_url)
 | 
			
		||||
@@ -398,12 +466,12 @@ class NewAgent(APIView):
 | 
			
		||||
        user = User.objects.create_user(  # type: ignore
 | 
			
		||||
            username=request.data["agent_id"],
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            password=User.objects.make_random_password(60),  # type: ignore
 | 
			
		||||
            password=make_random_password(len=60),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        token = Token.objects.create(user=user)
 | 
			
		||||
 | 
			
		||||
        if agent.monitoring_type == "workstation":
 | 
			
		||||
        if agent.monitoring_type == AgentMonType.WORKSTATION:
 | 
			
		||||
            WinUpdatePolicy(agent=agent, run_time_days=[5, 6]).save()
 | 
			
		||||
        else:
 | 
			
		||||
            WinUpdatePolicy(agent=agent).save()
 | 
			
		||||
@@ -414,14 +482,15 @@ class NewAgent(APIView):
 | 
			
		||||
        AuditLog.objects.create(
 | 
			
		||||
            username=request.user,
 | 
			
		||||
            agent=agent.hostname,
 | 
			
		||||
            object_type="agent",
 | 
			
		||||
            action="agent_install",
 | 
			
		||||
            object_type=AuditObjType.AGENT,
 | 
			
		||||
            action=AuditActionType.AGENT_INSTALL,
 | 
			
		||||
            message=f"{request.user} installed new agent {agent.hostname}",
 | 
			
		||||
            after_value=Agent.serialize(agent),
 | 
			
		||||
            debug_info={"ip": request._client_ip},
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        ret = {"pk": agent.pk, "token": token.key}
 | 
			
		||||
        sync_mesh_perms_task.delay()
 | 
			
		||||
        return Response(ret)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -452,7 +521,10 @@ class Installer(APIView):
 | 
			
		||||
            return notify_error("Invalid data")
 | 
			
		||||
 | 
			
		||||
        ver = request.data["version"]
 | 
			
		||||
        if pyver.parse(ver) < pyver.parse(settings.LATEST_AGENT_VER):
 | 
			
		||||
        if (
 | 
			
		||||
            pyver.parse(ver) < pyver.parse(settings.LATEST_AGENT_VER)
 | 
			
		||||
            and "-dev" not in settings.LATEST_AGENT_VER
 | 
			
		||||
        ):
 | 
			
		||||
            return notify_error(
 | 
			
		||||
                f"Old installer detected (version {ver} ). Latest version is {settings.LATEST_AGENT_VER} Please generate a new installer from the RMM"
 | 
			
		||||
            )
 | 
			
		||||
@@ -487,7 +559,7 @@ class ChocoResult(APIView):
 | 
			
		||||
 | 
			
		||||
        action.details["output"] = results
 | 
			
		||||
        action.details["installed"] = installed
 | 
			
		||||
        action.status = "completed"
 | 
			
		||||
        action.status = PAStatus.COMPLETED
 | 
			
		||||
        action.save(update_fields=["details", "status"])
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
@@ -497,9 +569,28 @@ class AgentHistoryResult(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
    def patch(self, request, agentid, pk):
 | 
			
		||||
        _ = get_object_or_404(Agent, agent_id=agentid)
 | 
			
		||||
        hist = get_object_or_404(AgentHistory, pk=pk)
 | 
			
		||||
        content_length = request.META.get("CONTENT_LENGTH")
 | 
			
		||||
        if content_length and int(content_length) > TRMM_MAX_REQUEST_SIZE:
 | 
			
		||||
 | 
			
		||||
            request.data["script_results"]["stdout"] = ""
 | 
			
		||||
            request.data["script_results"][
 | 
			
		||||
                "stderr"
 | 
			
		||||
            ] = "Content truncated due to excessive request size."
 | 
			
		||||
            request.data["script_results"]["retcode"] = 1
 | 
			
		||||
 | 
			
		||||
        hist = get_object_or_404(
 | 
			
		||||
            AgentHistory.objects.filter(agent__agent_id=agentid), pk=pk
 | 
			
		||||
        )
 | 
			
		||||
        s = AgentHistorySerializer(instance=hist, data=request.data, partial=True)
 | 
			
		||||
        s.is_valid(raise_exception=True)
 | 
			
		||||
        s.save()
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AgentConfig(APIView):
 | 
			
		||||
    authentication_classes = [TokenAuthentication]
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, agentid):
 | 
			
		||||
        ret = get_agent_config()
 | 
			
		||||
        return Response(ret._to_dict())
 | 
			
		||||
 
 | 
			
		||||
@@ -1,16 +1,21 @@
 | 
			
		||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional
 | 
			
		||||
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from django.db import models
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from clients.models import Client, Site
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from logs.models import BaseAuditModel
 | 
			
		||||
 | 
			
		||||
from typing import Optional, Dict, Any, List, TYPE_CHECKING
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.constants import CORESETTINGS_CACHE_KEY
 | 
			
		||||
from tacticalrmm.constants import (
 | 
			
		||||
    CORESETTINGS_CACHE_KEY,
 | 
			
		||||
    AgentMonType,
 | 
			
		||||
    AgentPlat,
 | 
			
		||||
    CheckType,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
    from checks.models import Check
 | 
			
		||||
    from autotasks.models import AutomatedTask
 | 
			
		||||
    from checks.models import Check
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Policy(BaseAuditModel):
 | 
			
		||||
@@ -42,7 +47,7 @@ class Policy(BaseAuditModel):
 | 
			
		||||
        old_policy: Optional[Policy] = (
 | 
			
		||||
            type(self).objects.get(pk=self.pk) if self.pk else None
 | 
			
		||||
        )
 | 
			
		||||
        super(Policy, self).save(old_model=old_policy, *args, **kwargs)
 | 
			
		||||
        super().save(old_model=old_policy, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
        # check if alert template was changes and cache on agents
 | 
			
		||||
        if old_policy:
 | 
			
		||||
@@ -63,10 +68,7 @@ class Policy(BaseAuditModel):
 | 
			
		||||
        cache.delete_many_pattern("site_server_*")
 | 
			
		||||
        cache.delete_many_pattern("agent_*")
 | 
			
		||||
 | 
			
		||||
        super(Policy, self).delete(
 | 
			
		||||
            *args,
 | 
			
		||||
            **kwargs,
 | 
			
		||||
        )
 | 
			
		||||
        super().delete(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def __str__(self) -> str:
 | 
			
		||||
        return self.name
 | 
			
		||||
@@ -123,7 +125,7 @@ class Policy(BaseAuditModel):
 | 
			
		||||
                .exclude(id__in=excluded_agents_ids)
 | 
			
		||||
                .exclude(site_id__in=excluded_sites_ids)
 | 
			
		||||
                .exclude(site__client_id__in=excluded_clients_ids)
 | 
			
		||||
                .filter(monitoring_type="server")
 | 
			
		||||
                .filter(monitoring_type=AgentMonType.SERVER)
 | 
			
		||||
                .only("id")
 | 
			
		||||
                .values_list("id", flat=True)
 | 
			
		||||
            )
 | 
			
		||||
@@ -136,7 +138,7 @@ class Policy(BaseAuditModel):
 | 
			
		||||
                .exclude(id__in=excluded_agents_ids)
 | 
			
		||||
                .exclude(site_id__in=excluded_sites_ids)
 | 
			
		||||
                .exclude(site__client_id__in=excluded_clients_ids)
 | 
			
		||||
                .filter(monitoring_type="workstation")
 | 
			
		||||
                .filter(monitoring_type=AgentMonType.WORKSTATION)
 | 
			
		||||
                .only("id")
 | 
			
		||||
                .values_list("id", flat=True)
 | 
			
		||||
            )
 | 
			
		||||
@@ -155,7 +157,7 @@ class Policy(BaseAuditModel):
 | 
			
		||||
        explicit_clients_qs = Client.objects.none()
 | 
			
		||||
        explicit_sites_qs = Site.objects.none()
 | 
			
		||||
 | 
			
		||||
        if not mon_type or mon_type == "workstation":
 | 
			
		||||
        if not mon_type or mon_type == AgentMonType.WORKSTATION:
 | 
			
		||||
            explicit_clients_qs |= self.workstation_clients.exclude(  # type: ignore
 | 
			
		||||
                id__in=excluded_clients_ids
 | 
			
		||||
            )
 | 
			
		||||
@@ -163,7 +165,7 @@ class Policy(BaseAuditModel):
 | 
			
		||||
                id__in=excluded_sites_ids
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        if not mon_type or mon_type == "server":
 | 
			
		||||
        if not mon_type or mon_type == AgentMonType.SERVER:
 | 
			
		||||
            explicit_clients_qs |= self.server_clients.exclude(  # type: ignore
 | 
			
		||||
                id__in=excluded_clients_ids
 | 
			
		||||
            )
 | 
			
		||||
@@ -211,16 +213,15 @@ class Policy(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def get_policy_tasks(agent: "Agent") -> "List[AutomatedTask]":
 | 
			
		||||
 | 
			
		||||
        # List of all tasks to be applied
 | 
			
		||||
        tasks = list()
 | 
			
		||||
        tasks = []
 | 
			
		||||
 | 
			
		||||
        # Get policies applied to agent and agent site and client
 | 
			
		||||
        policies = agent.get_agent_policies()
 | 
			
		||||
 | 
			
		||||
        processed_policies = list()
 | 
			
		||||
        processed_policies = []
 | 
			
		||||
 | 
			
		||||
        for _, policy in policies.items():
 | 
			
		||||
        for policy in policies.values():
 | 
			
		||||
            if policy and policy.active and policy.pk not in processed_policies:
 | 
			
		||||
                processed_policies.append(policy.pk)
 | 
			
		||||
                for task in policy.autotasks.all():
 | 
			
		||||
@@ -230,7 +231,6 @@ class Policy(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def get_policy_checks(agent: "Agent") -> "List[Check]":
 | 
			
		||||
 | 
			
		||||
        # Get checks added to agent directly
 | 
			
		||||
        agent_checks = list(agent.agentchecks.all())
 | 
			
		||||
 | 
			
		||||
@@ -239,12 +239,12 @@ class Policy(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
        # Used to hold the policies that will be applied and the order in which they are applied
 | 
			
		||||
        # Enforced policies are applied first
 | 
			
		||||
        enforced_checks = list()
 | 
			
		||||
        policy_checks = list()
 | 
			
		||||
        enforced_checks = []
 | 
			
		||||
        policy_checks = []
 | 
			
		||||
 | 
			
		||||
        processed_policies = list()
 | 
			
		||||
        processed_policies = []
 | 
			
		||||
 | 
			
		||||
        for _, policy in policies.items():
 | 
			
		||||
        for policy in policies.values():
 | 
			
		||||
            if policy and policy.active and policy.pk not in processed_policies:
 | 
			
		||||
                processed_policies.append(policy.pk)
 | 
			
		||||
                if policy.enforced:
 | 
			
		||||
@@ -258,28 +258,31 @@ class Policy(BaseAuditModel):
 | 
			
		||||
            return []
 | 
			
		||||
 | 
			
		||||
        # Sorted Checks already added
 | 
			
		||||
        added_diskspace_checks: List[str] = list()
 | 
			
		||||
        added_ping_checks: List[str] = list()
 | 
			
		||||
        added_winsvc_checks: List[str] = list()
 | 
			
		||||
        added_script_checks: List[int] = list()
 | 
			
		||||
        added_eventlog_checks: List[List[str]] = list()
 | 
			
		||||
        added_cpuload_checks: List[int] = list()
 | 
			
		||||
        added_memory_checks: List[int] = list()
 | 
			
		||||
        added_diskspace_checks: List[str] = []
 | 
			
		||||
        added_ping_checks: List[str] = []
 | 
			
		||||
        added_winsvc_checks: List[str] = []
 | 
			
		||||
        added_script_checks: List[int] = []
 | 
			
		||||
        added_eventlog_checks: List[List[str]] = []
 | 
			
		||||
        added_cpuload_checks: List[int] = []
 | 
			
		||||
        added_memory_checks: List[int] = []
 | 
			
		||||
 | 
			
		||||
        # Lists all agent and policy checks that will be returned
 | 
			
		||||
        diskspace_checks: "List[Check]" = list()
 | 
			
		||||
        ping_checks: "List[Check]" = list()
 | 
			
		||||
        winsvc_checks: "List[Check]" = list()
 | 
			
		||||
        script_checks: "List[Check]" = list()
 | 
			
		||||
        eventlog_checks: "List[Check]" = list()
 | 
			
		||||
        cpuload_checks: "List[Check]" = list()
 | 
			
		||||
        memory_checks: "List[Check]" = list()
 | 
			
		||||
        diskspace_checks: "List[Check]" = []
 | 
			
		||||
        ping_checks: "List[Check]" = []
 | 
			
		||||
        winsvc_checks: "List[Check]" = []
 | 
			
		||||
        script_checks: "List[Check]" = []
 | 
			
		||||
        eventlog_checks: "List[Check]" = []
 | 
			
		||||
        cpuload_checks: "List[Check]" = []
 | 
			
		||||
        memory_checks: "List[Check]" = []
 | 
			
		||||
 | 
			
		||||
        overridden_checks: List[int] = list()
 | 
			
		||||
        overridden_checks: List[int] = []
 | 
			
		||||
 | 
			
		||||
        # Loop over checks in with enforced policies first, then non-enforced policies
 | 
			
		||||
        for check in enforced_checks + agent_checks + policy_checks:
 | 
			
		||||
            if check.check_type == "diskspace" and agent.plat == "windows":
 | 
			
		||||
            if (
 | 
			
		||||
                check.check_type == CheckType.DISK_SPACE
 | 
			
		||||
                and agent.plat == AgentPlat.WINDOWS
 | 
			
		||||
            ):
 | 
			
		||||
                # Check if drive letter was already added
 | 
			
		||||
                if check.disk not in added_diskspace_checks:
 | 
			
		||||
                    added_diskspace_checks.append(check.disk)
 | 
			
		||||
@@ -289,7 +292,7 @@ class Policy(BaseAuditModel):
 | 
			
		||||
                elif check.agent:
 | 
			
		||||
                    overridden_checks.append(check.pk)
 | 
			
		||||
 | 
			
		||||
            elif check.check_type == "ping":
 | 
			
		||||
            elif check.check_type == CheckType.PING:
 | 
			
		||||
                # Check if IP/host was already added
 | 
			
		||||
                if check.ip not in added_ping_checks:
 | 
			
		||||
                    added_ping_checks.append(check.ip)
 | 
			
		||||
@@ -299,7 +302,10 @@ class Policy(BaseAuditModel):
 | 
			
		||||
                elif check.agent:
 | 
			
		||||
                    overridden_checks.append(check.pk)
 | 
			
		||||
 | 
			
		||||
            elif check.check_type == "cpuload" and agent.plat == "windows":
 | 
			
		||||
            elif (
 | 
			
		||||
                check.check_type == CheckType.CPU_LOAD
 | 
			
		||||
                and agent.plat == AgentPlat.WINDOWS
 | 
			
		||||
            ):
 | 
			
		||||
                # Check if cpuload list is empty
 | 
			
		||||
                if not added_cpuload_checks:
 | 
			
		||||
                    added_cpuload_checks.append(check.pk)
 | 
			
		||||
@@ -309,7 +315,9 @@ class Policy(BaseAuditModel):
 | 
			
		||||
                elif check.agent:
 | 
			
		||||
                    overridden_checks.append(check.pk)
 | 
			
		||||
 | 
			
		||||
            elif check.check_type == "memory" and agent.plat == "windows":
 | 
			
		||||
            elif (
 | 
			
		||||
                check.check_type == CheckType.MEMORY and agent.plat == AgentPlat.WINDOWS
 | 
			
		||||
            ):
 | 
			
		||||
                # Check if memory check list is empty
 | 
			
		||||
                if not added_memory_checks:
 | 
			
		||||
                    added_memory_checks.append(check.pk)
 | 
			
		||||
@@ -319,7 +327,9 @@ class Policy(BaseAuditModel):
 | 
			
		||||
                elif check.agent:
 | 
			
		||||
                    overridden_checks.append(check.pk)
 | 
			
		||||
 | 
			
		||||
            elif check.check_type == "winsvc" and agent.plat == "windows":
 | 
			
		||||
            elif (
 | 
			
		||||
                check.check_type == CheckType.WINSVC and agent.plat == AgentPlat.WINDOWS
 | 
			
		||||
            ):
 | 
			
		||||
                # Check if service name was already added
 | 
			
		||||
                if check.svc_name not in added_winsvc_checks:
 | 
			
		||||
                    added_winsvc_checks.append(check.svc_name)
 | 
			
		||||
@@ -329,7 +339,7 @@ class Policy(BaseAuditModel):
 | 
			
		||||
                elif check.agent:
 | 
			
		||||
                    overridden_checks.append(check.pk)
 | 
			
		||||
 | 
			
		||||
            elif check.check_type == "script" and agent.is_supported_script(
 | 
			
		||||
            elif check.check_type == CheckType.SCRIPT and agent.is_supported_script(
 | 
			
		||||
                check.script.supported_platforms
 | 
			
		||||
            ):
 | 
			
		||||
                # Check if script id was already added
 | 
			
		||||
@@ -341,7 +351,10 @@ class Policy(BaseAuditModel):
 | 
			
		||||
                elif check.agent:
 | 
			
		||||
                    overridden_checks.append(check.pk)
 | 
			
		||||
 | 
			
		||||
            elif check.check_type == "eventlog" and agent.plat == "windows":
 | 
			
		||||
            elif (
 | 
			
		||||
                check.check_type == CheckType.EVENT_LOG
 | 
			
		||||
                and agent.plat == AgentPlat.WINDOWS
 | 
			
		||||
            ):
 | 
			
		||||
                # Check if events were already added
 | 
			
		||||
                if [check.log_name, check.event_id] not in added_eventlog_checks:
 | 
			
		||||
                    added_eventlog_checks.append([check.log_name, check.event_id])
 | 
			
		||||
 
 | 
			
		||||
@@ -7,5 +7,5 @@ class AutomationPolicyPerms(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, r, view) -> bool:
 | 
			
		||||
        if r.method == "GET":
 | 
			
		||||
            return _has_perm(r, "can_list_automation_policies")
 | 
			
		||||
        else:
 | 
			
		||||
            return _has_perm(r, "can_manage_automation_policies")
 | 
			
		||||
 | 
			
		||||
        return _has_perm(r, "can_manage_automation_policies")
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user