Compare commits
	
		
			1796 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | bc99434574 | ||
|  | 9e86020ef7 | ||
|  | 6e9bb0c4f4 | ||
|  | d66a41a8a3 | ||
|  | 90914bff14 | ||
|  | 62414848f4 | ||
|  | d4ece6ecd7 | ||
|  | d1ec60bb63 | ||
|  | 4f672c736b | ||
|  | 2e5c351d8b | ||
|  | 3562553346 | ||
|  | 4750b292a5 | ||
|  | 3eb0561e90 | ||
|  | abb118c8ca | ||
|  | 2818a229b6 | ||
|  | a9b8af3677 | ||
|  | 0354da00da | ||
|  | b179587475 | ||
|  | 3021f90bc5 | ||
|  | a14b0278c8 | ||
|  | 80070b333e | ||
|  | 3aa8dcac11 | ||
|  | e920f05611 | ||
|  | 3594afd3aa | ||
|  | 9daaee8212 | ||
|  | d022707349 | ||
|  | 3948605ae6 | ||
|  | f2ded5fdd6 | ||
|  | 00b47be181 | ||
|  | a2fac5d946 | ||
|  | a00b5bb36b | ||
|  | d4fbc34085 | ||
|  | e9e3031992 | ||
|  | c2c7553f56 | ||
|  | 4e60cb89c9 | ||
|  | ec4523240f | ||
|  | 1655ddbcaa | ||
|  | 997c677f30 | ||
|  | d5fc8a2d7e | ||
|  | 3bcd0302a8 | ||
|  | de91b7e8af | ||
|  | 7efd1d7c9e | ||
|  | b5151a2178 | ||
|  | c8432020c6 | ||
|  | 2c9d413a1a | ||
|  | cdf842e7ad | ||
|  | c917007949 | ||
|  | 64278c6b3c | ||
|  | 10a01ed14a | ||
|  | ba3bd1407b | ||
|  | 73666c9a04 | ||
|  | eae24083c9 | ||
|  | a644510c27 | ||
|  | 57859d0da2 | ||
|  | 057f0ff648 | ||
|  | 05d1c867f2 | ||
|  | a2238fa435 | ||
|  | 12b7426a7c | ||
|  | 5148d613a7 | ||
|  | f455c15882 | ||
|  | 618fdabd0e | ||
|  | 3b69e2896c | ||
|  | 7306b63ab1 | ||
|  | 7e3133caa2 | ||
|  | 560901d714 | ||
|  | 166ce9ae78 | ||
|  | d3395a685e | ||
|  | 6d5e9a8566 | ||
|  | 69ec03feb4 | ||
|  | f92982cd5a | ||
|  | 5570f2b464 | ||
|  | ad19dc0240 | ||
|  | 9b1d4faff8 | ||
|  | 76756d20e9 | ||
|  | e564500480 | ||
|  | 19c15ce58d | ||
|  | a027785098 | ||
|  | 36a9f10aae | ||
|  | 99a11a4b53 | ||
|  | 55cac4465c | ||
|  | ff395fd074 | ||
|  | 972b6e09c7 | ||
|  | e793a33b15 | ||
|  | e70d4ff3f3 | ||
|  | cd0635d3a0 | ||
|  | 81702d8595 | ||
|  | aaa4a65b04 | ||
|  | 430797e626 | ||
|  | d454001f49 | ||
|  | bd90ee1f58 | ||
|  | 196aaa5427 | ||
|  | 6e42233b33 | ||
|  | 8e44df8525 | ||
|  | a8a1536941 | ||
|  | 99d1728c70 | ||
|  | 6bbb92cdb9 | ||
|  | b80e7c06bf | ||
|  | bf467b874c | ||
|  | 43c9f6be56 | ||
|  | 6811a4f4ae | ||
|  | 1f16dd9c43 | ||
|  | 63a43ce104 | ||
|  | bd7ce5417e | ||
|  | 941ee54a97 | ||
|  | a5d4a64f47 | ||
|  | d96fcd4a98 | ||
|  | de42e2f747 | ||
|  | 822a93aeb6 | ||
|  | c31b4aaeff | ||
|  | 8c9a386054 | ||
|  | 8c90933615 | ||
|  | 6f8c242333 | ||
|  | fe8b66873a | ||
|  | 00c5f1365a | ||
|  | f7d317328a | ||
|  | 3ccd705225 | ||
|  | 9e439fffaa | ||
|  | 859dc170e7 | ||
|  | 1932d8fad9 | ||
|  | 0c814ae436 | ||
|  | 89313d8a37 | ||
|  | 2b85722222 | ||
|  | 57e5b0188c | ||
|  | 2d7c830e70 | ||
|  | ccaa1790a9 | ||
|  | f6531d905e | ||
|  | 64a31879d3 | ||
|  | 0c6a4b1ed2 | ||
|  | 67801f39fe | ||
|  | 892a0d67bf | ||
|  | 9fc0b7d5cc | ||
|  | 22a614ef54 | ||
|  | cd257b8e4d | ||
|  | fa1ee2ca14 | ||
|  | 34ea1adde6 | ||
|  | 41cf8abb1f | ||
|  | c0ffec1a4c | ||
|  | 65779b8eaf | ||
|  | c47bdb2d56 | ||
|  | d47ae642e7 | ||
|  | 39c4609cc6 | ||
|  | 3ebba02a10 | ||
|  | 4dc7a96e79 | ||
|  | 5a49a29110 | ||
|  | 983a5c2034 | ||
|  | 15829f04a3 | ||
|  | 934618bc1c | ||
|  | 2c5ec75b88 | ||
|  | df11fd744f | ||
|  | 4dba0fb43d | ||
|  | 7a0d86b8dd | ||
|  | a94cd98e0f | ||
|  | 8e95e51edc | ||
|  | 6f1b00284a | ||
|  | 58549a6cac | ||
|  | acc9a6118f | ||
|  | c7811e861c | ||
|  | 55cf766ff0 | ||
|  | a1eaf38324 | ||
|  | c6788092d3 | ||
|  | f89f74ef3f | ||
|  | 3e40f02001 | ||
|  | c169967c1b | ||
|  | 2830e7c569 | ||
|  | 415f08ba3a | ||
|  | d726bcdc19 | ||
|  | f259c25a70 | ||
|  | 4db937cf1f | ||
|  | dad9d0660c | ||
|  | 0c450a5bb2 | ||
|  | ef59819c01 | ||
|  | c651e7c84b | ||
|  | 20b8debb1c | ||
|  | dd5743f0a1 | ||
|  | 7da2b51fae | ||
|  | 0236800392 | ||
|  | 4f822878f7 | ||
|  | c2810e5fe5 | ||
|  | b89ba4b801 | ||
|  | 07c680b839 | ||
|  | fd50db4eab | ||
|  | 0ee95b36a6 | ||
|  | b8cf07149e | ||
|  | 1b699f1a87 | ||
|  | d3bfd238d3 | ||
|  | 1f43abb3c8 | ||
|  | 287c753e4a | ||
|  | 8a5374d31a | ||
|  | e219eaa934 | ||
|  | fd314480ca | ||
|  | dd45396cf3 | ||
|  | 1e2a56c5e9 | ||
|  | 8011773af4 | ||
|  | ddc69c692e | ||
|  | df925c9744 | ||
|  | 1726341aad | ||
|  | 63b1ccc7a7 | ||
|  | ee5db31518 | ||
|  | e80397c857 | ||
|  | 81aa7ca1a4 | ||
|  | f0f7695890 | ||
|  | e7e8ce2f7a | ||
|  | ba37a3f18d | ||
|  | 60b11a7a5d | ||
|  | 29461c20a7 | ||
|  | 2ff1f34543 | ||
|  | b75d7f970f | ||
|  | 204681f097 | ||
|  | e239fe95a4 | ||
|  | 0a101f061a | ||
|  | f112a17afa | ||
|  | 54658a66d2 | ||
|  | 6b8f5a76e4 | ||
|  | 623a5d338d | ||
|  | 9c5565cfd5 | ||
|  | 722f2efaee | ||
|  | 4928264204 | ||
|  | 12d62ddc2a | ||
|  | da54e97217 | ||
|  | 9c0993dac8 | ||
|  | 175486b7c4 | ||
|  | 4760a287f6 | ||
|  | 0237b48c87 | ||
|  | 95c9f22e6c | ||
|  | 9b001219d5 | ||
|  | 6ff15efc7b | ||
|  | 6fe1dccc7e | ||
|  | 1c80f6f3fa | ||
|  | 54d3177fdd | ||
|  | a24ad245d2 | ||
|  | f38cfdcadf | ||
|  | 92e4ad8ccd | ||
|  | 3f3ab088d2 | ||
|  | 2c2cbaa175 | ||
|  | 911b6bf863 | ||
|  | 31462cab64 | ||
|  | 1ee35da62d | ||
|  | edf4815595 | ||
|  | 06ccee5d18 | ||
|  | d5ad85725f | ||
|  | 4d5bddb413 | ||
|  | 2f4da7c381 | ||
|  | 8b845fce03 | ||
|  | 9fd15c38a9 | ||
|  | ec1573d01f | ||
|  | 92ec1cc9e7 | ||
|  | 8b2f9665ce | ||
|  | cb388a5a78 | ||
|  | 7f4389ae08 | ||
|  | 76d71beaa2 | ||
|  | 31bb9c2197 | ||
|  | 6a2cd5c45a | ||
|  | 520632514b | ||
|  | f998b28d0b | ||
|  | 1a6587e9e6 | ||
|  | 9b4b729d19 | ||
|  | e80345295e | ||
|  | 026c259a2e | ||
|  | 63474c2269 | ||
|  | faa1a9312f | ||
|  | 23fa0726d5 | ||
|  | 22210eaf7d | ||
|  | dcd8bee676 | ||
|  | 06f0fa8f0e | ||
|  | 6d0f9e2cd5 | ||
|  | 732afdb65d | ||
|  | 1a9e8742f7 | ||
|  | b8eda37339 | ||
|  | 5107db6169 | ||
|  | 2c8f207454 | ||
|  | 489bc9c3b3 | ||
|  | 514713e883 | ||
|  | 17cc0cd09c | ||
|  | 4475df1295 | ||
|  | fdad267cfd | ||
|  | 3684fc80f0 | ||
|  | e97a5fef94 | ||
|  | de2972631f | ||
|  | e5b8fd67c8 | ||
|  | 5fade89e2d | ||
|  | 2eefedadb3 | ||
|  | e63d7a0b8a | ||
|  | 2a1b1849fa | ||
|  | 0461cb7f19 | ||
|  | 0932e0be03 | ||
|  | 4638ac9474 | ||
|  | d8d7255029 | ||
|  | fa05276c3f | ||
|  | e50a5d51d8 | ||
|  | c03ba78587 | ||
|  | ff07c69e7d | ||
|  | 735b84b26d | ||
|  | 8dd069ad67 | ||
|  | 1857e68003 | ||
|  | ff2508382a | ||
|  | 9cb952b116 | ||
|  | 105e8089bb | ||
|  | 730f37f247 | ||
|  | 284716751f | ||
|  | 8d0db699bf | ||
|  | 53cf1cae58 | ||
|  | 307e4719e0 | ||
|  | 5effae787a | ||
|  | 6532be0b52 | ||
|  | fb225a5347 | ||
|  | b83830a45e | ||
|  | ca28288c33 | ||
|  | b6f8d9cb25 | ||
|  | 9cad0f11e5 | ||
|  | 807be08566 | ||
|  | 67f6a985f8 | ||
|  | f87d54ae8d | ||
|  | d894bf7271 | ||
|  | 56e0e5cace | ||
|  | 685084e784 | ||
|  | cbeec5a973 | ||
|  | 3fff56bcd7 | ||
|  | c504c23eec | ||
|  | 16dae5a655 | ||
|  | e512c5ae7d | ||
|  | 094078b928 | ||
|  | 34fc3ff919 | ||
|  | 4391f48e78 | ||
|  | 775608a3c0 | ||
|  | b326228901 | ||
|  | b2e98173a8 | ||
|  | 65c9b7952c | ||
|  | b9dc9e7d62 | ||
|  | ce178d0354 | ||
|  | a3ff6efebc | ||
|  | 6a9bc56723 | ||
|  | c9ac158d25 | ||
|  | 4b937a0fe8 | ||
|  | 405bf26ac5 | ||
|  | 5dcda0e0a0 | ||
|  | 83e9b60308 | ||
|  | 10b40b4730 | ||
|  | 79d6d804ef | ||
|  | e9c7b6d8f8 | ||
|  | 4fcfbfb3f4 | ||
|  | 30cde14ed3 | ||
|  | cf76e6f538 | ||
|  | d0f600ec8d | ||
|  | 675f9e956f | ||
|  | 381605a6bb | ||
|  | 0fce66062b | ||
|  | 747cc9e5da | ||
|  | 25a1b464da | ||
|  | 3b6738b547 | ||
|  | fc93e3e97f | ||
|  | 0edbb13d48 | ||
|  | 673687341c | ||
|  | 3969208942 | ||
|  | 3fa89b58df | ||
|  | a43a9c8543 | ||
|  | 45deda4dea | ||
|  | 6ec46f02a9 | ||
|  | d643c17ff1 | ||
|  | e5de89c6b4 | ||
|  | c21e7c632d | ||
|  | 6ae771682a | ||
|  | bf2075b902 | ||
|  | 62ec8c8f76 | ||
|  | b84d4a99b8 | ||
|  | cce9dfe585 | ||
|  | 166be395b9 | ||
|  | fa3f5f8d68 | ||
|  | 2926b68c32 | ||
|  | a55f187958 | ||
|  | c76d263375 | ||
|  | 6740d97f8f | ||
|  | b079eebe79 | ||
|  | 363e48a1e8 | ||
|  | f60e4e3e4f | ||
|  | 1b02974efa | ||
|  | 496abdd230 | ||
|  | bc495d77d1 | ||
|  | fb54d4bb64 | ||
|  | 0786163dc3 | ||
|  | ed85611e75 | ||
|  | 86ebfce44a | ||
|  | dae51cff51 | ||
|  | 358a2e7220 | ||
|  | d45353e8c8 | ||
|  | 2f56e4e3a1 | ||
|  | 0e503f8273 | ||
|  | 876fe803f5 | ||
|  | 6adb9678b6 | ||
|  | 39bf7ba4a9 | ||
|  | 5da6e2ff99 | ||
|  | 44603c41a2 | ||
|  | 0feb982a73 | ||
|  | d93cb32f2e | ||
|  | 40c47eace2 | ||
|  | 509bdd879c | ||
|  | b98ebb6e9f | ||
|  | 924ddecff0 | ||
|  | ca64fd218d | ||
|  | 9b12b55acd | ||
|  | 450239564a | ||
|  | bb1cc62d2a | ||
|  | b4875c1e2d | ||
|  | a21440d663 | ||
|  | eb6836b63c | ||
|  | b39a2690c1 | ||
|  | 706902da1c | ||
|  | d5104b5d27 | ||
|  | a13ae5c4b1 | ||
|  | a92d1d9958 | ||
|  | 10852a9427 | ||
|  | b757ce1e38 | ||
|  | 91e75f3fa2 | ||
|  | 6c8e55eb2f | ||
|  | f821f700fa | ||
|  | d76d24408f | ||
|  | 7ad85dfe1c | ||
|  | 7d8be0a719 | ||
|  | bac15c18e4 | ||
|  | 2f266d39e6 | ||
|  | 5726d1fc52 | ||
|  | 69aee1823e | ||
|  | e6a0ae5f57 | ||
|  | e5df566c7a | ||
|  | 81e173b609 | ||
|  | d0ebcc6606 | ||
|  | 99c3fcf42a | ||
|  | 794666e7cc | ||
|  | 45abe4955d | ||
|  | 7eed421c70 | ||
|  | 69f7c397c2 | ||
|  | d2d136e922 | ||
|  | 396e435ae0 | ||
|  | 45d8e9102a | ||
|  | 12a51deffa | ||
|  | f2f69abec2 | ||
|  | 02b7f962e9 | ||
|  | eb813e6b22 | ||
|  | 5ddc604341 | ||
|  | 313e672e93 | ||
|  | ce77ad6de4 | ||
|  | bea22690b1 | ||
|  | c9a52bd7d0 | ||
|  | a244a341ec | ||
|  | 2b47870032 | ||
|  | de9e35ae6a | ||
|  | 1a6fec8ca9 | ||
|  | 094054cd99 | ||
|  | f85b8a81f1 | ||
|  | a44eaebf7c | ||
|  | f37b3c063e | ||
|  | 6e5d5a3b82 | ||
|  | bf0562d619 | ||
|  | ecaa81be3c | ||
|  | d98ae48935 | ||
|  | f52a76b16c | ||
|  | d421c27602 | ||
|  | 70e4cd4de1 | ||
|  | 29767e9265 | ||
|  | 46d4c7f96d | ||
|  | 161a6f3923 | ||
|  | 53e912341b | ||
|  | 19396ea11a | ||
|  | 1d9a5e742b | ||
|  | e8dfdd03f7 | ||
|  | 2f5b15dac7 | ||
|  | 525e1f5136 | ||
|  | 7d63d188af | ||
|  | 87889c12ea | ||
|  | 53d023f5ee | ||
|  | 1877ab8c67 | ||
|  | 72a5a8cab7 | ||
|  | 221e49a978 | ||
|  | 1a4c67d173 | ||
|  | 42fd23ece3 | ||
|  | 3035c0712a | ||
|  | 61315f8bfd | ||
|  | 52683124d8 | ||
|  | 1f77390366 | ||
|  | 322d492540 | ||
|  | f977d8cca9 | ||
|  | a9aedea2bd | ||
|  | 5560bbeecb | ||
|  | f226206703 | ||
|  | 170687226d | ||
|  | d56d3dc271 | ||
|  | 32a202aff4 | ||
|  | 6ee75e6e60 | ||
|  | 13d74cae3b | ||
|  | 88651916b0 | ||
|  | be12505d2f | ||
|  | 23fcf3b045 | ||
|  | 9e7459b204 | ||
|  | 4f0eb1d566 | ||
|  | ce00481f47 | ||
|  | f596af90ba | ||
|  | 5c74d1d021 | ||
|  | aff659b6b6 | ||
|  | 58724d95fa | ||
|  | 8d61fcd5c9 | ||
|  | 3e1be53c36 | ||
|  | f3754588bd | ||
|  | c4ffffeec8 | ||
|  | 5b69f6a358 | ||
|  | 1af89a7447 | ||
|  | 90abd81035 | ||
|  | 898824b13f | ||
|  | 9d093aa7f8 | ||
|  | 1770549f6c | ||
|  | d21be77fd2 | ||
|  | 41a1c19877 | ||
|  | 9b6571ce68 | ||
|  | 88e98e4e35 | ||
|  | 10c56ffbfa | ||
|  | cb2c8d6f3c | ||
|  | ca62b850ce | ||
|  | 5a75d4e140 | ||
|  | e0972b7c24 | ||
|  | 0db497916d | ||
|  | 23a0ad3c4e | ||
|  | 2b4e1c4b67 | ||
|  | 9b1b9244cf | ||
|  | ad570e9b16 | ||
|  | 812ba6de62 | ||
|  | 8f97124adb | ||
|  | 28289838f9 | ||
|  | cca8a010c3 | ||
|  | 91ab296692 | ||
|  | ee6c9c4272 | ||
|  | 21cd36fa92 | ||
|  | b1aafe3dbc | ||
|  | 5cd832de89 | ||
|  | 24dd9d0518 | ||
|  | aab6ab810a | ||
|  | d1d6d5e71e | ||
|  | e67dd68522 | ||
|  | e25eae846d | ||
|  | 995eeaa455 | ||
|  | 240c61b967 | ||
|  | 2d8b0753b4 | ||
|  | 44eab3de7f | ||
|  | 007be5bf95 | ||
|  | ee19c7c51f | ||
|  | ce56afbdf9 | ||
|  | 51012695a1 | ||
|  | 0eef2d2cc5 | ||
|  | 487f9f2815 | ||
|  | d065adcd8e | ||
|  | 0d9a1dc5eb | ||
|  | 8f9ad15108 | ||
|  | e538e9b843 | ||
|  | 4a702b6813 | ||
|  | 1e6fd2c57a | ||
|  | 600b959d89 | ||
|  | b96de9eb13 | ||
|  | 93be19b647 | ||
|  | 74f45f6f1d | ||
|  | 54ba3d2888 | ||
|  | 65d5149f60 | ||
|  | 917ebb3771 | ||
|  | 7e66b1f545 | ||
|  | 05837dca35 | ||
|  | 53be2ebe59 | ||
|  | 0341efcaea | ||
|  | ec75210fd3 | ||
|  | e6afe3e806 | ||
|  | 5aa46f068e | ||
|  | a11a5b28bc | ||
|  | 907aa566ca | ||
|  | 5c21f099a8 | ||
|  | b91201ae3e | ||
|  | 56d7e19968 | ||
|  | cf91c6c90e | ||
|  | 9011148adf | ||
|  | 897d0590d2 | ||
|  | 33b33e8458 | ||
|  | 7758f5c187 | ||
|  | 83d7a03ba4 | ||
|  | a9a0df9699 | ||
|  | df44f8f5f8 | ||
|  | 216a9ed035 | ||
|  | 35d61b6a6c | ||
|  | 5fb72cea53 | ||
|  | d54d021e9f | ||
|  | 06e78311df | ||
|  | df720f95ca | ||
|  | 00faff34d3 | ||
|  | 2b5b3ea4f3 | ||
|  | 95e608d0b4 | ||
|  | 1d55bf87dd | ||
|  | 1220ce53eb | ||
|  | 2006218f87 | ||
|  | 40f427a387 | ||
|  | 445e95baed | ||
|  | 67fbc9ad33 | ||
|  | 1253e9e465 | ||
|  | 21069432e8 | ||
|  | 6facf6a324 | ||
|  | 7556197485 | ||
|  | 8dddd2d896 | ||
|  | f319c95c2b | ||
|  | 8e972b0907 | ||
|  | 395e400215 | ||
|  | 3685e3111f | ||
|  | 7bb1c75dc6 | ||
|  | b20834929c | ||
|  | 181891757e | ||
|  | b16feeae44 | ||
|  | 684e049f27 | ||
|  | 8cebd901b2 | ||
|  | 3c96beb8fb | ||
|  | 8a46459cf9 | ||
|  | be5c3e9daa | ||
|  | e44453877c | ||
|  | f772a4ec56 | ||
|  | 44182ec683 | ||
|  | b9ab13fa53 | ||
|  | 2ad6721c95 | ||
|  | b7d0604e62 | ||
|  | a7518b4b26 | ||
|  | 50613f5d3e | ||
|  | f814767703 | ||
|  | 4af86d6456 | ||
|  | f0a4f00c2d | ||
|  | 4321affddb | ||
|  | 926ed55b9b | ||
|  | 2ebf308565 | ||
|  | 1c5e736dce | ||
|  | b591f9f5b7 | ||
|  | 9724882578 | ||
|  | ddef2df101 | ||
|  | 8af69c4284 | ||
|  | 6ebe1ab467 | ||
|  | 24e4d9cf6d | ||
|  | f35fa0aa58 | ||
|  | 4942f262f1 | ||
|  | a20b1a973e | ||
|  | eae5e00706 | ||
|  | 403762d862 | ||
|  | 5c92d4b454 | ||
|  | 38179b9d38 | ||
|  | 8f510dde5a | ||
|  | be42d56e37 | ||
|  | 6294530fa3 | ||
|  | c5c8f5fab1 | ||
|  | 3d41d79078 | ||
|  | 3005061a11 | ||
|  | 65ea46f457 | ||
|  | eca8f32570 | ||
|  | 8d1ef19c61 | ||
|  | 71d87d866b | ||
|  | c4f88bdce7 | ||
|  | f722a115b1 | ||
|  | 1583beea7b | ||
|  | 5b388c587b | ||
|  | e254923167 | ||
|  | b0dbdd7803 | ||
|  | aa6ebe0122 | ||
|  | c5f179bab8 | ||
|  | e65cb86638 | ||
|  | a349998640 | ||
|  | 43f60610b8 | ||
|  | 46d042087a | ||
|  | ee214727f6 | ||
|  | b4c1ec55ec | ||
|  | 0fdd54f710 | ||
|  | 4f0cdeaec0 | ||
|  | e5cc38857c | ||
|  | fe4b9d71c0 | ||
|  | 5c1181e40e | ||
|  | 8b71832bc2 | ||
|  | 8412ed6065 | ||
|  | 207f6cdc7c | ||
|  | b0b51f5730 | ||
|  | def6833ef0 | ||
|  | c528dd3de1 | ||
|  | 544270e35d | ||
|  | 657e029fee | ||
|  | 49469d7689 | ||
|  | 4f0dd452c8 | ||
|  | 3f741eab11 | ||
|  | 190368788f | ||
|  | 8306a3f566 | ||
|  | 988c134c09 | ||
|  | af0a4d578b | ||
|  | 9bc0abc831 | ||
|  | 41410e99e7 | ||
|  | deae04d5ff | ||
|  | 7d6eeffd66 | ||
|  | 629858e095 | ||
|  | dfdb628347 | ||
|  | 6e48b28fc9 | ||
|  | 3ba450e837 | ||
|  | 688ed93500 | ||
|  | 7268ba20a2 | ||
|  | 63d9e73098 | ||
|  | 564c048f90 | ||
|  | 5f801c74d5 | ||
|  | b405fbc09a | ||
|  | 7a64c2eb49 | ||
|  | c93cbac3b1 | ||
|  | 8b0f67b8a6 | ||
|  | 0d96129f2d | ||
|  | 54ee12d2b3 | ||
|  | 92fc042103 | ||
|  | 9bb7016fa7 | ||
|  | 3ad56feafb | ||
|  | 14d59c3dec | ||
|  | 443f419770 | ||
|  | ddbb58755e | ||
|  | 524283b9ff | ||
|  | fb178d2944 | ||
|  | 52f4ad9403 | ||
|  | ba0c08ef1f | ||
|  | 9e19b1e04c | ||
|  | b2118201b1 | ||
|  | b4346aa056 | ||
|  | b599f05aab | ||
|  | 93d78a0200 | ||
|  | 449957b2eb | ||
|  | 0a6d44bad3 | ||
|  | 17ceaaa503 | ||
|  | d70803b416 | ||
|  | aa414d4702 | ||
|  | f24e1b91ea | ||
|  | 1df8163090 | ||
|  | 659ddf6a45 | ||
|  | e110068da4 | ||
|  | c943f6f936 | ||
|  | cb1fe7fe54 | ||
|  | 593f1f63cc | ||
|  | 66aa70cf75 | ||
|  | 304be99067 | ||
|  | 9a01ec35f4 | ||
|  | bfa5b4fba5 | ||
|  | d2f63ef353 | ||
|  | 50f334425e | ||
|  | f78212073c | ||
|  | 5c655f5a82 | ||
|  | 6a6446bfcb | ||
|  | b60a3a5e50 | ||
|  | 02ccbab8e5 | ||
|  | 023ff3f964 | ||
|  | 7c5e8df3b8 | ||
|  | 56fdab260b | ||
|  | 7cce49dc1a | ||
|  | 2dfaafb20b | ||
|  | 6138a5bf54 | ||
|  | 828c67cc00 | ||
|  | e70cd44e18 | ||
|  | efa5ac5edd | ||
|  | 788b11e759 | ||
|  | d049d7a61f | ||
|  | 075c833b58 | ||
|  | e9309c2a96 | ||
|  | a592d2b397 | ||
|  | 3ad1805ac0 | ||
|  | dbc2bab698 | ||
|  | 79eec5c299 | ||
|  | 7754b0c575 | ||
|  | be4289ce76 | ||
|  | 67f5226270 | ||
|  | b6d77c581b | ||
|  | d84bf47d04 | ||
|  | aba3a7bb9e | ||
|  | 6281736d89 | ||
|  | 94d96f89d3 | ||
|  | 4b55f9dead | ||
|  | 5c6dce94df | ||
|  | f7d8f9c7f5 | ||
|  | 053df24f9c | ||
|  | 1dc470e434 | ||
|  | cfd8773267 | ||
|  | 67045cf6c1 | ||
|  | ddfb9e7239 | ||
|  | 9f6eed5472 | ||
|  | 15a1e2ebcb | ||
|  | fcfe450b07 | ||
|  | a69bbb3bc9 | ||
|  | 6d2559cfc1 | ||
|  | b3a62615f3 | ||
|  | 57f5cca1cb | ||
|  | 6b9851f540 | ||
|  | 36fd203a88 | ||
|  | 3f5cb5d61c | ||
|  | 862fc6a946 | ||
|  | 92c386ac0e | ||
|  | 98a11a3645 | ||
|  | 62be0ed936 | ||
|  | b7de73fd8a | ||
|  | e2413f1af2 | ||
|  | 0e77d575c4 | ||
|  | ba42c5e367 | ||
|  | 6a06734192 | ||
|  | 5e26a406b7 | ||
|  | b6dd03138d | ||
|  | cf03ee03ee | ||
|  | 0e665b6bf0 | ||
|  | e3d0de7313 | ||
|  | bcf3a543a1 | ||
|  | b27f17c74a | ||
|  | 75d864771e | ||
|  | 6420060f2a | ||
|  | c149ae71b9 | ||
|  | 3a49dd034c | ||
|  | b26d7e82e3 | ||
|  | 415abdf0ce | ||
|  | f7f6f6ecb2 | ||
|  | 43d54f134a | ||
|  | 0d2606a13b | ||
|  | 1deb10dc88 | ||
|  | 1236d55544 | ||
|  | ecccf39455 | ||
|  | 8e0316825a | ||
|  | aa45fa87af | ||
|  | 71e78bd0c5 | ||
|  | 4766477c58 | ||
|  | d97e49ff2b | ||
|  | 6b9d775cb9 | ||
|  | e521f580d7 | ||
|  | 25e7cf7db0 | ||
|  | 0cab33787d | ||
|  | bc6faf817f | ||
|  | d46ae55863 | ||
|  | bbd900ab25 | ||
|  | 129ae93e2b | ||
|  | 44dd59fa3f | ||
|  | ec4e7559b0 | ||
|  | dce40611cf | ||
|  | e71b8546f9 | ||
|  | f827348467 | ||
|  | f3978343db | ||
|  | 2654a7ea70 | ||
|  | 1068bf4ef7 | ||
|  | e7fccc97cc | ||
|  | 733e289852 | ||
|  | 29d71a104c | ||
|  | 05200420ad | ||
|  | eb762d4bfd | ||
|  | 58ace9eda1 | ||
|  | eeb2623be0 | ||
|  | cfa242c2fe | ||
|  | ec0441ccc2 | ||
|  | ae2782a8fe | ||
|  | 58ff570251 | ||
|  | 7b554b12c7 | ||
|  | 58f7603d4f | ||
|  | 8895994c54 | ||
|  | de8f7e36d5 | ||
|  | 88d7a50265 | ||
|  | 21e19fc7e5 | ||
|  | faf4935a69 | ||
|  | 71a1f9d74a | ||
|  | bd8d523e10 | ||
|  | 60cae0e3ac | ||
|  | 5a342ac012 | ||
|  | bb8767dfc3 | ||
|  | fcb2779c15 | ||
|  | 77dd6c1f61 | ||
|  | 8118eef300 | ||
|  | 802d1489fe | ||
|  | 443a029185 | ||
|  | 4ee508fdd0 | ||
|  | aa5608f7e8 | ||
|  | cc472b4613 | ||
|  | 764b945ddc | ||
|  | fd2206ce4c | ||
|  | 48c0ac9f00 | ||
|  | 84eb4fe9ed | ||
|  | 4a5428812c | ||
|  | 023f98a89d | ||
|  | 66893dd0c1 | ||
|  | 25a6666e35 | ||
|  | 19d75309b5 | ||
|  | 11110d65c1 | ||
|  | a348f58fe2 | ||
|  | 13851dd976 | ||
|  | 2ec37c5da9 | ||
|  | 8c127160de | ||
|  | 2af820de9a | ||
|  | 55fb0bb3a0 | ||
|  | 9f9ecc521f | ||
|  | dfd01df5ba | ||
|  | 474090698c | ||
|  | 6b71cdeea4 | ||
|  | 581e974236 | ||
|  | ba3c3a42ce | ||
|  | c8bc5671c5 | ||
|  | ff9401a040 | ||
|  | 5e1bc1989f | ||
|  | a1dc91cd7d | ||
|  | 99f2772bb3 | ||
|  | e5d0e42655 | ||
|  | 2c914cc374 | ||
|  | 9bceb62381 | ||
|  | de7518a800 | ||
|  | 304fb63453 | ||
|  | 0f7ef60ca0 | ||
|  | 07c74e4641 | ||
|  | de7f325cfb | ||
|  | 42cdf70cb4 | ||
|  | 6beb6be131 | ||
|  | fa4fc2a708 | ||
|  | 2db9758260 | ||
|  | 715982e40a | ||
|  | d00cd4453a | ||
|  | 429c08c24a | ||
|  | 6a71490e20 | ||
|  | 9bceda0646 | ||
|  | a1027a6773 | ||
|  | 302d4b75f9 | ||
|  | 5f6ee0e883 | ||
|  | 27f9720de1 | ||
|  | 22aa3fdbbc | ||
|  | 069ecdd33f | ||
|  | dd545ae933 | ||
|  | 6650b705c4 | ||
|  | 59b0350289 | ||
|  | 1ad159f820 | ||
|  | 0bf42190e9 | ||
|  | d2fa836232 | ||
|  | c387774093 | ||
|  | e99736ba3c | ||
|  | 16cb54fcc9 | ||
|  | 5aa15c51ec | ||
|  | a8aedd9cf3 | ||
|  | b851b632bc | ||
|  | 541e07fb65 | ||
|  | 6ad16a897d | ||
|  | 72f1053a93 | ||
|  | fb15a2762c | ||
|  | 9165248b91 | ||
|  | add18b29db | ||
|  | 1971653548 | ||
|  | 392cd64d7b | ||
|  | b5affbb7c8 | ||
|  | 71d1206277 | ||
|  | 26e6a8c409 | ||
|  | eb54fae11a | ||
|  | ee773e5966 | ||
|  | 7218ccdba8 | ||
|  | 332400e48a | ||
|  | ad1a5d3702 | ||
|  | 3006b4184d | ||
|  | 84eb84a080 | ||
|  | 60beea548b | ||
|  | 5f9c149e59 | ||
|  | 53367c6f04 | ||
|  | d7f817ee44 | ||
|  | d33a87da54 | ||
|  | 3aebfb12b7 | ||
|  | 1d6c55ffa6 | ||
|  | 5e7080aac3 | ||
|  | fad739bc01 | ||
|  | c6b7f23884 | ||
|  | a6f7e446de | ||
|  | 89d95d3ae1 | ||
|  | 764208698f | ||
|  | 57129cf934 | ||
|  | aae1a842d5 | ||
|  | 623f35aec7 | ||
|  | 870bf842cf | ||
|  | 07f2d7dd5c | ||
|  | f223f2edc5 | ||
|  | e848a9a577 | ||
|  | 7569d98e07 | ||
|  | 596dee2f24 | ||
|  | 9970403964 | ||
|  | 07a88ae00d | ||
|  | 5475b4d287 | ||
|  | 6631dcfd3e | ||
|  | 0dd3f337f3 | ||
|  | 8eb27b5875 | ||
|  | 2d1863031c | ||
|  | 9feb76ca81 | ||
|  | 993e8f4ab3 | ||
|  | e08ae95d4f | ||
|  | 15359e8846 | ||
|  | d1457b312b | ||
|  | c9dd2af196 | ||
|  | 564ef4e688 | ||
|  | a33e6e8bb5 | ||
|  | cf34f33f04 | ||
|  | 827cfe4e8f | ||
|  | 2ce1c2383c | ||
|  | 6fc0a665ae | ||
|  | 4f16d01263 | ||
|  | 67cc37354a | ||
|  | e388243ef4 | ||
|  | 3dc92763c7 | ||
|  | dfe97dd466 | ||
|  | 2803cee29b | ||
|  | 3a03020e54 | ||
|  | 64443cc703 | ||
|  | 4d1aa6ed18 | ||
|  | 84837e88d2 | ||
|  | ff49c936ea | ||
|  | e6e0901329 | ||
|  | 23b6284b51 | ||
|  | 33dfbcbe32 | ||
|  | 700c23d537 | ||
|  | 369fac9e38 | ||
|  | 2229eb1167 | ||
|  | a3dec841b6 | ||
|  | b17620bdb6 | ||
|  | f39cd5ae2f | ||
|  | 83a19e005b | ||
|  | a9dd01b0c8 | ||
|  | eb59afa1d1 | ||
|  | 2adcfce9d0 | ||
|  | 314ab9b304 | ||
|  | 8576fb82c7 | ||
|  | 0f95a6bb2f | ||
|  | ad5104567d | ||
|  | ece68ba1d5 | ||
|  | acccd3a586 | ||
|  | 8ebef1c1ca | ||
|  | 28abc0d5ed | ||
|  | 1efe25d3ec | ||
|  | c40e4f8e4b | ||
|  | baca84092d | ||
|  | 346d4da059 | ||
|  | ade64d6c0a | ||
|  | 8204bdfc5f | ||
|  | 1a9bb3e986 | ||
|  | 49356479e5 | ||
|  | c44e9a7292 | ||
|  | 21771a593f | ||
|  | 84458dfc4c | ||
|  | 5835632dab | ||
|  | 67aa7229ef | ||
|  | b72dc3ed3a | ||
|  | 0f93d4a5bd | ||
|  | 106320b035 | ||
|  | 63951705cd | ||
|  | a8d56921d5 | ||
|  | 10bc133cf1 | ||
|  | adeb5b35c9 | ||
|  | 589ff46ea5 | ||
|  | 656fcb9fe7 | ||
|  | 1cb9353006 | ||
|  | 57bf16ba07 | ||
|  | 659846ed88 | ||
|  | 25894044e0 | ||
|  | e7a0826beb | ||
|  | 1f7ddee23b | ||
|  | 7e186730db | ||
|  | 6713a50208 | ||
|  | 7c9d8fcfec | ||
|  | 33bfc8cfe8 | ||
|  | ca735bc14a | ||
|  | 4ba748a18b | ||
|  | f1845106f8 | ||
|  | 67e7156c4b | ||
|  | 4a476adebf | ||
|  | 918798f8cc | ||
|  | 5a3f868866 | ||
|  | feea2c6396 | ||
|  | 707b4c46d9 | ||
|  | 89ca39fc2b | ||
|  | 204281b12d | ||
|  | a8538a7e95 | ||
|  | dee1b471e9 | ||
|  | aa04e9b01f | ||
|  | 350f0dc604 | ||
|  | 6021f2efd6 | ||
|  | 51838ec25a | ||
|  | 54768a121e | ||
|  | 8ff72cdca3 | ||
|  | 2cb53ad06b | ||
|  | b8349de31d | ||
|  | d7e11af7f8 | ||
|  | dd8d39e698 | ||
|  | afb1316daa | ||
|  | 04d7017536 | ||
|  | 6a1c75b060 | ||
|  | 5c94611f3b | ||
|  | 4e5676e80f | ||
|  | c96d688a9c | ||
|  | 804242e9a5 | ||
|  | 0ec9760b17 | ||
|  | d481ae3da4 | ||
|  | 4742c14fc1 | ||
|  | 509b0d501b | ||
|  | d4c9b04d4e | ||
|  | 16fb4d331b | ||
|  | e9e5bf31a7 | ||
|  | 221418120e | ||
|  | 46f852e26e | ||
|  | 4234cf0a31 | ||
|  | 7f3daea648 | ||
|  | 2eb16c82f4 | ||
|  | e00b2ce591 | ||
|  | d71e1311ca | ||
|  | 2cf16963e3 | ||
|  | 10bf7b7fb4 | ||
|  | 182c85a228 | ||
|  | 94b1988b90 | ||
|  | 6f7e62e9a0 | ||
|  | aa7076af04 | ||
|  | c928e8f0d4 | ||
|  | 5c6b106f68 | ||
|  | d45bcea1ff | ||
|  | 6ff2dc79f8 | ||
|  | b752329987 | ||
|  | f21465335a | ||
|  | 0801adfc4b | ||
|  | 5bee8052d5 | ||
|  | 68dca5dfef | ||
|  | 3f51dd1d2f | ||
|  | 7f80889d77 | ||
|  | efc61c0222 | ||
|  | 6fc0a05d34 | ||
|  | a9be872d7a | ||
|  | 6ca85f099e | ||
|  | 86ff677b8a | ||
|  | 35e295df86 | ||
|  | cd4d301790 | ||
|  | 93bb329c3d | ||
|  | 7c1e0f2c30 | ||
|  | b57f471f44 | ||
|  | 252a9a2ed6 | ||
|  | 7258d4d787 | ||
|  | 75522fa295 | ||
|  | 4ba8f41d95 | ||
|  | f326f8e4de | ||
|  | f863dc058e | ||
|  | 20891db251 | ||
|  | f1d05f1342 | ||
|  | 8dd636b0eb | ||
|  | 6b5bda8ee1 | ||
|  | ddc5597157 | ||
|  | ae112c7257 | ||
|  | c22f10f96a | ||
|  | 18d10c9bec | ||
|  | 890e430cb7 | ||
|  | dadc3d4cd7 | ||
|  | d98b4d7320 | ||
|  | 340f532238 | ||
|  | 7669f68e7c | ||
|  | 3557e5514f | ||
|  | a9f09b7614 | ||
|  | 845b9e4568 | ||
|  | 24a6092dcf | ||
|  | 195ae7d8b1 | ||
|  | a5c6ea7ffc | ||
|  | eb7a4ac29f | ||
|  | 508ef73fde | ||
|  | 838d6d8076 | ||
|  | 762c3159b8 | ||
|  | 7a88a06bcf | ||
|  | 0b1e3d7de5 | ||
|  | 9a83c73f21 | ||
|  | aa50c7b268 | ||
|  | 179a5a80f4 | ||
|  | 0ddae527ef | ||
|  | ee7a46de26 | ||
|  | 95522fda74 | ||
|  | e58881c2bd | ||
|  | 36a902a44e | ||
|  | 16b74549a2 | ||
|  | da7ededfb1 | ||
|  | 790bb08718 | ||
|  | e6765f421f | ||
|  | 7e8f1fe904 | ||
|  | eacce4578a | ||
|  | 07b2543972 | ||
|  | d1c3fc8493 | ||
|  | f453b16010 | ||
|  | 05151d8978 | ||
|  | 8218e1acc3 | ||
|  | 30212fc89a | ||
|  | b31c13fcae | ||
|  | 6b95fc6f1d | ||
|  | 369cf17eb2 | ||
|  | 4dd8f512cc | ||
|  | 26cfec7d80 | ||
|  | 67a87ccf00 | ||
|  | 667cebcf94 | ||
|  | bc1747ca1c | ||
|  | 945d8647bf | ||
|  | dfe2e94627 | ||
|  | 09a5591eec | ||
|  | f2bf06a0ba | ||
|  | eedad4ab1c | ||
|  | 336a62ab29 | ||
|  | b5603a5233 | ||
|  | 73890f553c | ||
|  | f6243b8968 | ||
|  | 3770dc74d4 | ||
|  | 45f4e947c5 | ||
|  | 9928d7c6e1 | ||
|  | bf776eeb2b | ||
|  | ae7c0e9195 | ||
|  | e90b640602 | ||
|  | ba7529d3f5 | ||
|  | 34667f252e | ||
|  | d18bddcb7b | ||
|  | 96dff49d33 | ||
|  | b389728338 | ||
|  | cdc7da86f3 | ||
|  | 4745cc0378 | ||
|  | 434f132479 | ||
|  | fb0f31ffc7 | ||
|  | bb1d73c0ae | ||
|  | 0e823d1191 | ||
|  | 48f4199ff3 | ||
|  | eaf379587b | ||
|  | 672446b7d1 | ||
|  | dfe52c1b07 | ||
|  | d63df03ad8 | ||
|  | aba4f9f2ce | ||
|  | ac5c1e7803 | ||
|  | d521dbf50e | ||
|  | f210ed3e6a | ||
|  | df3cac4ea6 | ||
|  | f778c5175b | ||
|  | 6c66ff28dd | ||
|  | d5b6ec702b | ||
|  | c62a5fcef2 | ||
|  | 59c47e9200 | ||
|  | 4ba44d8932 | ||
|  | 27dae05e1b | ||
|  | a251ae9b90 | ||
|  | 7e960b2bde | ||
|  | 5df4825158 | ||
|  | 8984d06d93 | ||
|  | eed7aac047 | ||
|  | 54b068de4a | ||
|  | f0f33b00b6 | ||
|  | 1043405088 | ||
|  | 0131b10805 | ||
|  | a19b441f62 | ||
|  | 28edc31d43 | ||
|  | 0f9872a818 | ||
|  | 76ce4296f3 | ||
|  | 3dd2671380 | ||
|  | 298ca31332 | ||
|  | 8f911aa6b9 | ||
|  | 82a5c7d9b1 | ||
|  | 7f013dcdba | ||
|  | 68e2e16076 | ||
|  | ea23c763c9 | ||
|  | 5dcecb3206 | ||
|  | 5bd48e2d0e | ||
|  | afd0a02589 | ||
|  | 2379192d53 | ||
|  | a6489290c8 | ||
|  | 5f74c43415 | ||
|  | aa8b84a302 | ||
|  | b987d041b0 | ||
|  | b62e37307e | ||
|  | 61a59aa6ac | ||
|  | f79ec27f1d | ||
|  | b993fe380f | ||
|  | d974b5f55f | ||
|  | f21ae93197 | ||
|  | 342ff18be8 | ||
|  | a8236f69bf | ||
|  | ab15a2448d | ||
|  | 6ff4d8f558 | ||
|  | bb04ba528c | ||
|  | b94a795189 | ||
|  | 9968184733 | ||
|  | 1be6f8f87a | ||
|  | 426821cceb | ||
|  | 4fec0deaf7 | ||
|  | 144ac5b6ce | ||
|  | 97c73786fa | ||
|  | 82e59d7da0 | ||
|  | b2c10de6af | ||
|  | d72029c2c6 | ||
|  | 17b9987063 | ||
|  | fde07da2b7 | ||
|  | c23bc29511 | ||
|  | 714cad2a52 | ||
|  | 357d5d2fde | ||
|  | d477cce901 | ||
|  | eb6af52ad1 | ||
|  | aae75023a7 | ||
|  | 41dcd4f458 | ||
|  | 4651ae4495 | ||
|  | ed61e0b0fc | ||
|  | 1eefc6fbf4 | ||
|  | 09ebf2cea2 | ||
|  | b3b0c4cd65 | ||
|  | f4b7924e8f | ||
|  | ea68d38b82 | ||
|  | dfbaa71132 | ||
|  | 6c328deb08 | ||
|  | add564d5bf | ||
|  | fa94acb426 | ||
|  | 6827468f13 | ||
|  | 53fd43868f | ||
|  | 9ced7561c5 | ||
|  | 31d55d3425 | ||
|  | 171d2a5bb9 | ||
|  | c5d05c1205 | ||
|  | 2973e0559a | ||
|  | ec27288dcf | ||
|  | f92e5c7093 | ||
|  | 7c67155c49 | ||
|  | b102cd4652 | ||
|  | 67f9a48c37 | ||
|  | a0c8a1ee65 | ||
|  | 7e7d272b06 | ||
|  | 3c642240ae | ||
|  | b5157fcaf1 | ||
|  | d1cb42f1bc | ||
|  | 84cde1a16a | ||
|  | 877f5db1ce | ||
|  | 787164e245 | ||
|  | d77fc5e7c5 | ||
|  | cca39a67d6 | ||
|  | a6c9a0431a | ||
|  | 729a80a639 | ||
|  | 31cb3001f6 | ||
|  | 5d0f54a329 | ||
|  | c8c3f5b5b7 | ||
|  | ba473ed75a | ||
|  | 7236fd59f8 | ||
|  | 9471e8f1fd | ||
|  | a2d39b51bb | ||
|  | 2920934b55 | ||
|  | 3f709d448e | ||
|  | b79f66183f | ||
|  | 8672f57e55 | ||
|  | 1e99c82351 | ||
|  | 1a2ff851f3 | ||
|  | f1c27c3959 | ||
|  | b30dac0f15 | ||
|  | cc79e5cdaf | ||
|  | d9a3b2f2cb | ||
|  | 479b528d09 | ||
|  | 461fb84fb9 | ||
|  | bd7685e3fa | ||
|  | cd98cb64b3 | ||
|  | 0f32a3ec24 | ||
|  | ca446cac87 | ||
|  | 6ea907ffda | ||
|  | 5287baa70d | ||
|  | 25935fec84 | ||
|  | e855a063ff | ||
|  | c726b8c9f0 | ||
|  | 13cb99290e | ||
|  | cea9413fd1 | ||
|  | 1432853b39 | ||
|  | 6d6c2b86e8 | ||
|  | 77b1d964b5 | ||
|  | 549936fc09 | ||
|  | c9c32f09c5 | ||
|  | 77f7778d4a | ||
|  | 84b6be9364 | ||
|  | 1e43b55804 | ||
|  | ba9bdaae0a | ||
|  | 7dfd7bde8e | ||
|  | 5e6c4161d0 | ||
|  | d75d56dfc9 | ||
|  | 1d9d350091 | ||
|  | 5744053c6f | ||
|  | 65589b6ca2 | ||
|  | e03a9d1137 | ||
|  | 29f80f2276 | ||
|  | a9b74aa69b | ||
|  | 63ebfd3210 | ||
|  | 87fa5ff7a6 | ||
|  | b686b53a9c | ||
|  | 258261dc64 | ||
|  | 9af5c9ead9 | ||
|  | 382654188c | ||
|  | fa1df082b7 | ||
|  | 5c227d8f80 | ||
|  | 81dabdbfb7 | ||
|  | 91f89f5a33 | ||
|  | 9f92746aa0 | ||
|  | 5d6e6f9441 | ||
|  | 01395a2726 | ||
|  | 465d75c65d | ||
|  | 4634f8927e | ||
|  | 74a287f9fe | ||
|  | 7ff6c79835 | ||
|  | 3629982237 | ||
|  | ddb610f1bc | ||
|  | f899905d27 | ||
|  | 3e4531b5c5 | ||
|  | a9e189e51d | ||
|  | 58ba08a8f3 | ||
|  | 9078ff27d8 | ||
|  | 6f43e61c24 | ||
|  | 4be0d3f212 | ||
|  | 00e47e5a27 | ||
|  | 152e145b32 | ||
|  | 54e55e8f57 | ||
|  | 05b8707f9e | ||
|  | 543e952023 | ||
|  | 6e5f40ea06 | ||
|  | bbafb0be87 | ||
|  | 1c9c5232fe | ||
|  | 598d79a502 | ||
|  | 37d8360b77 | ||
|  | 82d9ca3317 | ||
|  | 4e4238d486 | ||
|  | c77dbe44dc | ||
|  | e03737f15f | ||
|  | a02629bcd7 | ||
|  | 6c3fc23d78 | ||
|  | 0fe40f9ccb | ||
|  | 9bd7c8edd1 | ||
|  | 83ba480863 | ||
|  | f158ea25e9 | ||
|  | 0227519eab | ||
|  | 616a9685fa | ||
|  | fe61b01320 | ||
|  | 7b25144311 | ||
|  | 9d42fbbdd7 | ||
|  | 39ac5b088b | ||
|  | c14ffd08a0 | ||
|  | 6e1239340b | ||
|  | a297dc8b3b | ||
|  | 8d4ecc0898 | ||
|  | eae9c04429 | ||
|  | a41c48a9c5 | ||
|  | ff2a94bd9b | ||
|  | 4a1f5558b8 | ||
|  | 608db9889f | ||
|  | 012b697337 | ||
|  | 0580506cf3 | ||
|  | ff4ab9b661 | ||
|  | b7ce5fdd3e | ||
|  | a11e617322 | ||
|  | d0beac7e2b | ||
|  | 9db497092f | ||
|  | 8eb91c08aa | ||
|  | ded5437522 | ||
|  | 9348657951 | ||
|  | bca85933f7 | ||
|  | c32bb35f1c | ||
|  | 4b84062d62 | ||
|  | d6d0f8fa17 | ||
|  | dd72c875d3 | ||
|  | 1a1df50300 | ||
|  | 53cbb527b4 | ||
|  | 8b87b2717e | ||
|  | 1007d6dac7 | ||
|  | 6799fac120 | ||
|  | 558e6288ca | ||
|  | d9cb73291b | ||
|  | d0f7be3ac3 | ||
|  | 331e16d3ca | ||
|  | 0db246c311 | ||
|  | 94dc62ff58 | ||
|  | e68ecf6844 | ||
|  | 5167b0a8c6 | ||
|  | 77e3d3786d | ||
|  | 708d4d39bc | ||
|  | 2a8cda2a1e | ||
|  | 8d783840ad | ||
|  | abe39d5790 | ||
|  | d7868e9e5a | ||
|  | 7b84e36e15 | ||
|  | 6cab6d69d8 | ||
|  | 87846d7aef | ||
|  | 2557769c6a | ||
|  | 48375f3878 | ||
|  | 176c85d8c1 | ||
|  | 17cad71ede | ||
|  | e8bf9d4e6f | ||
|  | 7bdd2038ef | ||
|  | e9f6e7943a | ||
|  | e74ba387ab | ||
|  | 27c79e5b99 | ||
|  | 8170d5ea73 | ||
|  | 196f73705d | ||
|  | ad0bbf5248 | ||
|  | 4cae9cd90d | ||
|  | be7bc55a76 | ||
|  | 684b545e8f | ||
|  | 7835cc3b10 | ||
|  | f8706b51e8 | ||
|  | d97f8fd5da | ||
|  | f8fa87441e | ||
|  | d42537814a | ||
|  | 792421b0e2 | ||
|  | 72d55a010b | ||
|  | 880d8258ce | ||
|  | b79bf82efb | ||
|  | b3118b6253 | ||
|  | ba172e2e25 | ||
|  | 892d53abeb | ||
|  | 5cbaa1ce98 | ||
|  | 7b35d9ad2e | ||
|  | 8462de7911 | ||
|  | 8721f44298 | ||
|  | c7a2d69afa | ||
|  | 0453d81e7a | ||
|  | 501c04ac2b | ||
|  | 0ef4e9a5c3 | ||
|  | 129c50e598 | ||
|  | 3e276fc2ac | ||
|  | 658d5e05ae | ||
|  | 4e7d5d476e | ||
|  | 6a55ca20f3 | ||
|  | c56c537f7f | ||
|  | fd7d776121 | ||
|  | 1af28190d8 | ||
|  | 6b305be567 | ||
|  | 3bf70513b7 | ||
|  | 7e64404654 | ||
|  | e1b5226f34 | ||
|  | 0d7128ad31 | ||
|  | 5778626087 | ||
|  | 3ff48756ed | ||
|  | 0ce9a6eeba | ||
|  | ad527b4aed | ||
|  | 6633bb452e | ||
|  | efeb0b4feb | ||
|  | 8cc11fc102 | ||
|  | ee6a167220 | ||
|  | 8d4ad3c405 | ||
|  | 072fbf4d60 | ||
|  | 727c41c283 | ||
|  | e2266838b6 | ||
|  | 775762d615 | ||
|  | 900c3008cb | ||
|  | 09379213a6 | ||
|  | ceb97048e3 | ||
|  | 4561515517 | ||
|  | a7b285759f | ||
|  | b4531b2a12 | ||
|  | 9e1d261c76 | ||
|  | e35fa15cd2 | ||
|  | dbd1f0d4f9 | ||
|  | 9ade78b703 | ||
|  | f20e244b5f | ||
|  | 0989308b7e | ||
|  | 12c7140536 | ||
|  | 2a0b605e92 | ||
|  | 6978890e6a | ||
|  | 561abd6cb9 | ||
|  | 4dd6227f0b | ||
|  | 1ec314c31c | ||
|  | a2be5a00be | ||
|  | 4e2241c115 | ||
|  | 8459bca64a | ||
|  | 24cb0565b9 | ||
|  | 9442acb028 | ||
|  | 4f7f181a42 | ||
|  | b7dd8737a7 | ||
|  | 2207eeb727 | ||
|  | 89dad7dfe7 | ||
|  | e5803d0cf3 | ||
|  | c1fffe9ae6 | ||
|  | 9e6cbd3d32 | ||
|  | 2ea8742510 | ||
|  | 5cfa0254f9 | ||
|  | 8cd2544f78 | ||
|  | c03b768364 | ||
|  | d60481ead4 | ||
|  | 126be3827d | ||
|  | 121274dca2 | ||
|  | 0ecf8da27e | ||
|  | 4a6bcb525d | ||
|  | 83f9ee50dd | ||
|  | 2bff297f79 | ||
|  | dee68f6933 | ||
|  | afa1e19c83 | ||
|  | 6052088eb4 | ||
|  | c7fa5167c4 | ||
|  | 1034b0b146 | ||
|  | 8bcc4e5945 | ||
|  | c3c24aa1db | ||
|  | 281c75d2d2 | ||
|  | 52307420f3 | ||
|  | 6185347cd8 | ||
|  | b6cd29f77e | ||
|  | b8ea8b1567 | ||
|  | 2f7dc98830 | ||
|  | e248a99f79 | ||
|  | 4fb6d9aa5d | ||
|  | f092ea8d67 | ||
|  | c32cbbdda6 | ||
|  | 2497675259 | ||
|  | 8d084ab90a | ||
|  | 2398773ef0 | ||
|  | a05998a30e | ||
|  | f863c29194 | ||
|  | d16a98c788 | ||
|  | 9421b02e96 | ||
|  | 10256864e4 | ||
|  | 85d010615d | ||
|  | cd1cb186be | ||
|  | 4458354d70 | ||
|  | 0f27da8808 | ||
|  | dd76bfa3c2 | ||
|  | 5780a66f7d | ||
|  | d4342c034c | ||
|  | 1ec43f2530 | ||
|  | 3c300d8fdf | ||
|  | 23119b55d1 | ||
|  | c8fb0e8f8a | ||
|  | 0ec32a77ef | ||
|  | 52921bfce8 | ||
|  | 960b929097 | ||
|  | d4ce23eced | ||
|  | 6925510f44 | ||
|  | 9827ad4c22 | ||
|  | ef8aaee028 | ||
|  | 3d7d39f248 | ||
|  | 3eac620560 | ||
|  | ab17006956 | ||
|  | bfc6889ee9 | ||
|  | 0ec0b4a044 | ||
|  | f1a523f327 | ||
|  | 4181449aea | ||
|  | e192f8db52 | ||
|  | 8097c681ac | ||
|  | f45938bdd5 | ||
|  | 6ea4e97eca | ||
|  | f274c8e837 | ||
|  | 335e571485 | ||
|  | a11616aace | ||
|  | 883acadbc4 | ||
|  | f51e6a3fcf | ||
|  | 371e081c0d | ||
|  | 6f41b3bf1c | ||
|  | c1d74a6c9e | ||
|  | 24eaa6796e | ||
|  | 1521e3b620 | ||
|  | b6ff38dd62 | ||
|  | 44ea9ac03c | ||
|  | 4c2701505b | ||
|  | 9022fe18da | ||
|  | 63be349f8b | ||
|  | c40256a290 | ||
|  | 33ecb8ec52 | ||
|  | 82d62a0015 | ||
|  | 6278240526 | ||
|  | 8c2dc5f57d | ||
|  | 2e5868778a | ||
|  | a10b8dab9b | ||
|  | 92f4f7ef59 | ||
|  | 31257bd5cb | ||
|  | bb6510862f | ||
|  | 797ecf0780 | ||
|  | f9536dc67f | ||
|  | e8b95362af | ||
|  | bdc39ad4ec | ||
|  | 4a202c5585 | ||
|  | 3c6b321f73 | ||
|  | cb29b52799 | ||
|  | 7e48015a54 | ||
|  | 9ed3abf932 | ||
|  | 61762828a3 | ||
|  | 59beabe5ac | ||
|  | 0b30faa28c | ||
|  | d12d49b93f | ||
|  | f1d64d275a | ||
|  | d094eeeb03 | ||
|  | be25af658e | ||
|  | 794f52c229 | ||
|  | 5d4dc4ed4c | ||
|  | e49d97b898 | ||
|  | b6b4f1ba62 | ||
|  | 653d476716 | ||
|  | 48b855258c | ||
|  | c7efdaf5f9 | ||
|  | 22523ed3d3 | ||
|  | 33c602dd61 | ||
|  | e2a5509b76 | ||
|  | 61a0fa1a89 | ||
|  | a35bd8292b | ||
|  | 06c8ae60e3 | ||
|  | deeab1f845 | ||
|  | da81c4c987 | ||
|  | d180f1b2d5 | ||
|  | 526135629c | ||
|  | 6b9493e057 | ||
|  | 9bb33d2afc | ||
|  | 7421138533 | ||
|  | d0800c52bb | ||
|  | 913fcd4df2 | ||
|  | 83322cc725 | ||
|  | 5944501feb | ||
|  | 17e3603d3d | ||
|  | 95be43ae47 | ||
|  | feb91cbbaa | ||
|  | 79409af168 | ||
|  | 5dbfb64822 | ||
|  | 5e7ebf5e69 | ||
|  | e73215ca74 | ||
|  | a5f123b9ce | ||
|  | ac058e9675 | ||
|  | 371b764d1d | ||
|  | 66d7172e09 | ||
|  | 99d3a8a749 | ||
|  | db5ff372a4 | ||
|  | 3fe83f81be | ||
|  | 669e638fd6 | ||
|  | f1f999f3b6 | ||
|  | 6f3b6fa9ce | ||
|  | 938f945301 | ||
|  | e3efb2aad6 | ||
|  | 1e678c0d78 | ||
|  | a59c111140 | ||
|  | a8b2a31bed | ||
|  | 37402f9ee8 | ||
|  | e7b5ecb40f | ||
|  | c817ef04b9 | ||
|  | f52b18439c | ||
|  | 1e03c628d5 | ||
|  | 71fb39db1f | ||
|  | bcfb3726b0 | ||
|  | c6e9e29671 | ||
|  | 1bfefcce39 | ||
|  | 22488e93e1 | ||
|  | 244b89f035 | ||
|  | 1f9a241b94 | ||
|  | 03641aae42 | ||
|  | a2bdd113cc | ||
|  | a92e2f3c7b | ||
|  | 97766b3a57 | ||
|  | 9ef4c3bb06 | ||
|  | d82f0cd757 | ||
|  | 5f529e2af4 | ||
|  | beadd9e02b | ||
|  | 72543789cb | ||
|  | 5789439fa9 | ||
|  | f549126bcf | ||
|  | 7197548bad | ||
|  | 241fde783c | ||
|  | 2b872cd1f4 | ||
|  | a606fb4d1d | ||
|  | 9f9c6be38e | ||
|  | 01ee524049 | ||
|  | af9cb65338 | ||
|  | 8aa11c580b | ||
|  | ada627f444 | ||
|  | a7b6d338c3 | ||
|  | 9f00538b97 | ||
|  | a085015282 | ||
|  | 0b9c220fbb | ||
|  | 0e3d04873d | ||
|  | b7578d939f | ||
|  | b5c28de03f | ||
|  | e17d25c156 | ||
|  | c25dc1b99c | ||
|  | a493a574bd | ||
|  | 4284493dce | ||
|  | 25059de8e1 | ||
|  | 1731b05ad0 | ||
|  | e80dc663ac | ||
|  | 39988a4c2f | ||
|  | 415bff303a | ||
|  | a65eb62a54 | ||
|  | 03b2982128 | ||
|  | bff0527857 | ||
|  | f3b7634254 | ||
|  | 6a9593c0b9 | ||
|  | edb785b8e5 | ||
|  | 26d757b50a | ||
|  | 535079ee87 | ||
|  | ac380c29c1 | ||
|  | 3fd212f26c | ||
|  | 04a3abc651 | ||
|  | 6caf85ddd1 | ||
|  | 16e4071508 | ||
|  | 69e7c4324b | ||
|  | a1c4a8cbe5 | ||
|  | e37f6cfda7 | ||
|  | 989c804409 | ||
|  | 7345bc3c82 | ||
|  | 69bee35700 | ||
|  | 598e24df7c | ||
|  | 0ae669201e | ||
|  | f52a8a4642 | ||
|  | 9c40b61ef2 | ||
|  | 72dabcda83 | ||
|  | 161a06dbcc | ||
|  | 8ed3d4e70c | ||
|  | a4223ccc8a | ||
|  | ca85923855 | ||
|  | 52bfe7c493 | ||
|  | 4786bd0cbe | ||
|  | cadab160ff | ||
|  | 6a7f17b2b0 | ||
|  | 4986a4d775 | ||
|  | 903af0c2cf | ||
|  | 3282fa803c | ||
|  | 67cc47608d | ||
|  | 0411704b8b | ||
|  | 1de85b2c69 | ||
|  | 33b012f29d | ||
|  | 1357584df3 | ||
|  | e15809e271 | ||
|  | aba1662631 | ||
|  | 61eeb60c19 | ||
|  | 2655964113 | 
| @@ -23,6 +23,9 @@ POSTGRES_USER=postgres | ||||
| POSTGRES_PASS=postgrespass | ||||
|  | ||||
| # DEV SETTINGS | ||||
| APP_PORT=8000 | ||||
| API_PORT=8080 | ||||
| APP_PORT=80 | ||||
| API_PORT=80 | ||||
| HTTP_PROTOCOL=https | ||||
| DOCKER_NETWORK=172.21.0.0/24 | ||||
| DOCKER_NGINX_IP=172.21.0.20 | ||||
| NATS_PORTS=4222:4222 | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| FROM python:3.8-slim | ||||
| FROM python:3.9.9-slim | ||||
|  | ||||
| ENV TACTICAL_DIR /opt/tactical | ||||
| ENV TACTICAL_GO_DIR /usr/local/rmmgo | ||||
| ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready | ||||
| ENV WORKSPACE_DIR /workspace | ||||
| ENV TACTICAL_USER tactical | ||||
| @@ -9,20 +8,18 @@ ENV VIRTUAL_ENV ${WORKSPACE_DIR}/api/tacticalrmm/env | ||||
| ENV PYTHONDONTWRITEBYTECODE=1 | ||||
| ENV PYTHONUNBUFFERED=1 | ||||
|  | ||||
| EXPOSE 8000 | ||||
| EXPOSE 8000 8383 8005 | ||||
|  | ||||
| RUN groupadd -g 1000 tactical && \ | ||||
|     useradd -u 1000 -g 1000 tactical | ||||
|  | ||||
| # Copy Go Files | ||||
| COPY --from=golang:1.15 /usr/local/go ${TACTICAL_GO_DIR}/go | ||||
| # Copy dev python reqs | ||||
| COPY .devcontainer/requirements.txt  / | ||||
|  | ||||
| # Copy Dev python reqs | ||||
| COPY ./requirements.txt / | ||||
|  | ||||
| # Copy Docker Entrypoint | ||||
| COPY ./entrypoint.sh / | ||||
| # Copy docker entrypoint.sh | ||||
| COPY .devcontainer/entrypoint.sh / | ||||
| RUN chmod +x /entrypoint.sh | ||||
|  | ||||
| ENTRYPOINT ["/entrypoint.sh"] | ||||
|  | ||||
| WORKDIR ${WORKSPACE_DIR}/api/tacticalrmm | ||||
|   | ||||
| @@ -2,11 +2,13 @@ version: '3.4' | ||||
|  | ||||
| services: | ||||
|   api-dev: | ||||
|     container_name: trmm-api-dev | ||||
|     image: api-dev | ||||
|     restart: always | ||||
|     build: | ||||
|       context: . | ||||
|       dockerfile: ./api.dockerfile | ||||
|     command: ["tactical-api"] | ||||
|       context: .. | ||||
|       dockerfile: .devcontainer/api.dockerfile | ||||
|     command: [ "tactical-api" ] | ||||
|     environment: | ||||
|       API_PORT: ${API_PORT} | ||||
|     ports: | ||||
| @@ -16,12 +18,15 @@ services: | ||||
|       - ..:/workspace:cached | ||||
|     networks: | ||||
|       dev: | ||||
|         aliases:  | ||||
|         aliases: | ||||
|           - tactical-backend | ||||
|  | ||||
|   app-dev: | ||||
|     image: node:12-alpine | ||||
|     command: /bin/sh -c "npm install && npm run serve -- --host 0.0.0.0 --port ${APP_PORT}" | ||||
|     container_name: trmm-app-dev | ||||
|     image: node:14-alpine | ||||
|     restart: always | ||||
|     command: /bin/sh -c "npm install npm@latest -g && npm install && npm run serve | ||||
|       -- --host 0.0.0.0 --port ${APP_PORT}" | ||||
|     working_dir: /workspace/web | ||||
|     volumes: | ||||
|       - ..:/workspace:cached | ||||
| @@ -29,26 +34,12 @@ services: | ||||
|       - "8080:${APP_PORT}" | ||||
|     networks: | ||||
|       dev: | ||||
|         aliases:  | ||||
|         aliases: | ||||
|           - tactical-frontend | ||||
|  | ||||
|   # salt master and api | ||||
|   salt-dev: | ||||
|     image: ${IMAGE_REPO}tactical-salt:${VERSION} | ||||
|     restart: always | ||||
|     volumes: | ||||
|       - tactical-data-dev:/opt/tactical | ||||
|       - salt-data-dev:/etc/salt | ||||
|     ports: | ||||
|       - "4505:4505" | ||||
|       - "4506:4506" | ||||
|     networks: | ||||
|       dev: | ||||
|         aliases: | ||||
|           - tactical-salt | ||||
|    | ||||
|   # nats | ||||
|   nats-dev: | ||||
|     container_name: trmm-nats-dev | ||||
|     image: ${IMAGE_REPO}tactical-nats:${VERSION} | ||||
|     restart: always | ||||
|     environment: | ||||
| @@ -56,7 +47,7 @@ services: | ||||
|       API_PORT: ${API_PORT} | ||||
|       DEV: 1 | ||||
|     ports: | ||||
|       - "4222:4222" | ||||
|       - "${NATS_PORTS}" | ||||
|     volumes: | ||||
|       - tactical-data-dev:/opt/tactical | ||||
|       - ..:/workspace:cached | ||||
| @@ -68,15 +59,16 @@ services: | ||||
|  | ||||
|   # meshcentral container | ||||
|   meshcentral-dev: | ||||
|     container_name: trmm-meshcentral-dev | ||||
|     image: ${IMAGE_REPO}tactical-meshcentral:${VERSION} | ||||
|     restart: always | ||||
|     environment:  | ||||
|     environment: | ||||
|       MESH_HOST: ${MESH_HOST} | ||||
|       MESH_USER: ${MESH_USER} | ||||
|       MESH_PASS: ${MESH_PASS} | ||||
|       MONGODB_USER: ${MONGODB_USER} | ||||
|       MONGODB_PASSWORD: ${MONGODB_PASSWORD} | ||||
|       NGINX_HOST_IP: 172.21.0.20 | ||||
|       NGINX_HOST_IP: ${DOCKER_NGINX_IP} | ||||
|     networks: | ||||
|       dev: | ||||
|         aliases: | ||||
| @@ -90,6 +82,7 @@ services: | ||||
|  | ||||
|   # mongodb container for meshcentral | ||||
|   mongodb-dev: | ||||
|     container_name: trmm-mongodb-dev | ||||
|     image: mongo:4.4 | ||||
|     restart: always | ||||
|     environment: | ||||
| @@ -105,6 +98,7 @@ services: | ||||
|  | ||||
|   # postgres database for api service | ||||
|   postgres-dev: | ||||
|     container_name: trmm-postgres-dev | ||||
|     image: postgres:13-alpine | ||||
|     restart: always | ||||
|     environment: | ||||
| @@ -120,20 +114,22 @@ services: | ||||
|  | ||||
|   # redis container for celery tasks | ||||
|   redis-dev: | ||||
|     container_name: trmm-redis-dev | ||||
|     restart: always | ||||
|     command: redis-server --appendonly yes | ||||
|     image: redis:6.0-alpine | ||||
|     volumes: | ||||
|       - redis-data-dev:/data | ||||
|     networks: | ||||
|       dev: | ||||
|         aliases: | ||||
|           - tactical-redis | ||||
|  | ||||
|   init-dev: | ||||
|     container_name: trmm-init-dev | ||||
|     image: api-dev | ||||
|     build: | ||||
|       context: . | ||||
|       dockerfile: ./api.dockerfile | ||||
|     restart: on-failure | ||||
|     command: ["tactical-init-dev"] | ||||
|     command: [ "tactical-init-dev" ] | ||||
|     environment: | ||||
|       POSTGRES_USER: ${POSTGRES_USER} | ||||
|       POSTGRES_PASS: ${POSTGRES_PASS} | ||||
| @@ -156,11 +152,9 @@ services: | ||||
|  | ||||
|   # container for celery worker service | ||||
|   celery-dev: | ||||
|     container_name: trmm-celery-dev | ||||
|     image: api-dev | ||||
|     build: | ||||
|       context: . | ||||
|       dockerfile: ./api.dockerfile | ||||
|     command: ["tactical-celery-dev"] | ||||
|     command: [ "tactical-celery-dev" ] | ||||
|     restart: always | ||||
|     networks: | ||||
|       - dev | ||||
| @@ -173,11 +167,9 @@ services: | ||||
|  | ||||
|   # container for celery beat service | ||||
|   celerybeat-dev: | ||||
|     container_name: trmm-celerybeat-dev | ||||
|     image: api-dev | ||||
|     build: | ||||
|       context: . | ||||
|       dockerfile: ./api.dockerfile | ||||
|     command: ["tactical-celerybeat-dev"] | ||||
|     command: [ "tactical-celerybeat-dev" ] | ||||
|     restart: always | ||||
|     networks: | ||||
|       - dev | ||||
| @@ -188,16 +180,16 @@ services: | ||||
|       - postgres-dev | ||||
|       - redis-dev | ||||
|  | ||||
|   # container for celery winupdate tasks | ||||
|   celerywinupdate-dev: | ||||
|   # container for websockets communication | ||||
|   websockets-dev: | ||||
|     container_name: trmm-websockets-dev | ||||
|     image: api-dev | ||||
|     build: | ||||
|       context: . | ||||
|       dockerfile: ./api.dockerfile | ||||
|     command: ["tactical-celerywinupdate-dev"] | ||||
|     command: [ "tactical-websockets-dev" ] | ||||
|     restart: always | ||||
|     networks: | ||||
|       - dev | ||||
|       dev: | ||||
|         aliases: | ||||
|           - tactical-websockets | ||||
|     volumes: | ||||
|       - tactical-data-dev:/opt/tactical | ||||
|       - ..:/workspace:cached | ||||
| @@ -205,8 +197,9 @@ services: | ||||
|       - postgres-dev | ||||
|       - redis-dev | ||||
|  | ||||
|   nginx-dev: | ||||
|   # container for tactical reverse proxy | ||||
|   nginx-dev: | ||||
|     container_name: trmm-nginx-dev | ||||
|     image: ${IMAGE_REPO}tactical-nginx:${VERSION} | ||||
|     restart: always | ||||
|     environment: | ||||
| @@ -217,21 +210,34 @@ services: | ||||
|       CERT_PRIV_KEY: ${CERT_PRIV_KEY} | ||||
|       APP_PORT: ${APP_PORT} | ||||
|       API_PORT: ${API_PORT} | ||||
|       DEV: 1 | ||||
|     networks: | ||||
|       dev: | ||||
|         ipv4_address: 172.21.0.20 | ||||
|         ipv4_address: ${DOCKER_NGINX_IP} | ||||
|     ports: | ||||
|       - "80:80" | ||||
|       - "443:443" | ||||
|     volumes: | ||||
|       - tactical-data-dev:/opt/tactical | ||||
|  | ||||
|   mkdocs-dev: | ||||
|     container_name: trmm-mkdocs-dev | ||||
|     image: api-dev | ||||
|     restart: always | ||||
|     command: [ "tactical-mkdocs-dev" ] | ||||
|     ports: | ||||
|       - "8005:8005" | ||||
|     volumes: | ||||
|       - ..:/workspace:cached | ||||
|     networks: | ||||
|       - dev | ||||
|  | ||||
| volumes: | ||||
|   tactical-data-dev: | ||||
|   postgres-data-dev: | ||||
|   mongo-dev-data: | ||||
|   mesh-data-dev: | ||||
|   salt-data-dev: | ||||
|   tactical-data-dev: null | ||||
|   postgres-data-dev: null | ||||
|   mongo-dev-data: null | ||||
|   mesh-data-dev: null | ||||
|   redis-data-dev: null | ||||
|  | ||||
| networks: | ||||
|   dev: | ||||
| @@ -239,4 +245,4 @@ networks: | ||||
|     ipam: | ||||
|       driver: default | ||||
|       config: | ||||
|         - subnet: 172.21.0.0/24   | ||||
|         - subnet: ${DOCKER_NETWORK} | ||||
|   | ||||
| @@ -9,9 +9,8 @@ set -e | ||||
| : "${POSTGRES_USER:=tactical}" | ||||
| : "${POSTGRES_PASS:=tactical}" | ||||
| : "${POSTGRES_DB:=tacticalrmm}" | ||||
| : "${SALT_HOST:=tactical-salt}" | ||||
| : "${SALT_USER:=saltapi}" | ||||
| : "${MESH_CONTAINER:=tactical-meshcentral}" | ||||
| : "${MESH_SERVICE:=tactical-meshcentral}" | ||||
| : "${MESH_WS_URL:=ws://${MESH_SERVICE}:443}" | ||||
| : "${MESH_USER:=meshcentral}" | ||||
| : "${MESH_PASS:=meshcentralpass}" | ||||
| : "${MESH_HOST:=tactical-meshcentral}" | ||||
| @@ -22,6 +21,9 @@ set -e | ||||
| : "${APP_PORT:=8080}" | ||||
| : "${API_PORT:=8000}" | ||||
|  | ||||
| : "${CERT_PRIV_PATH:=${TACTICAL_DIR}/certs/privkey.pem}" | ||||
| : "${CERT_PUB_PATH:=${TACTICAL_DIR}/certs/fullchain.pem}" | ||||
|  | ||||
| # Add python venv to path | ||||
| export PATH="${VIRTUAL_ENV}/bin:$PATH" | ||||
|  | ||||
| @@ -39,7 +41,7 @@ function django_setup { | ||||
|     sleep 5 | ||||
|   done | ||||
|  | ||||
|   until (echo > /dev/tcp/"${MESH_CONTAINER}"/443) &> /dev/null; do | ||||
|   until (echo > /dev/tcp/"${MESH_SERVICE}"/443) &> /dev/null; do | ||||
|     echo "waiting for meshcentral container to be ready..." | ||||
|     sleep 5 | ||||
|   done | ||||
| @@ -47,17 +49,9 @@ function django_setup { | ||||
|   echo "setting up django environment" | ||||
|  | ||||
|   # configure django settings | ||||
|   MESH_TOKEN=$(cat ${TACTICAL_DIR}/tmp/mesh_token) | ||||
|   MESH_TOKEN="$(cat ${TACTICAL_DIR}/tmp/mesh_token)" | ||||
|  | ||||
|   DJANGO_SEKRET=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 80 | head -n 1) | ||||
|  | ||||
|   # write salt pass to tmp dir | ||||
|   if [ ! -f "${TACTICAL__DIR}/tmp/salt_pass" ]; then | ||||
|     SALT_PASS=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 20 | head -n 1)  | ||||
|     echo "${SALT_PASS}" > ${TACTICAL_DIR}/tmp/salt_pass | ||||
|   else | ||||
|     SALT_PASS=$(cat ${TACTICAL_DIR}/tmp/salt_pass) | ||||
|   fi | ||||
|    | ||||
|   localvars="$(cat << EOF | ||||
| SECRET_KEY = '${DJANGO_SEKRET}' | ||||
| @@ -66,8 +60,8 @@ DEBUG = True | ||||
|  | ||||
| DOCKER_BUILD = True | ||||
|  | ||||
| CERT_FILE = '/opt/tactical/certs/fullchain.pem' | ||||
| KEY_FILE = '/opt/tactical/certs/privkey.pem' | ||||
| CERT_FILE = '${CERT_PUB_PATH}' | ||||
| KEY_FILE = '${CERT_PRIV_PATH}' | ||||
|  | ||||
| SCRIPTS_DIR = '${WORKSPACE_DIR}/scripts' | ||||
|  | ||||
| @@ -88,60 +82,44 @@ DATABASES = { | ||||
|     } | ||||
| } | ||||
|  | ||||
| REST_FRAMEWORK = { | ||||
|     'DATETIME_FORMAT': '%b-%d-%Y - %H:%M', | ||||
|  | ||||
|     'DEFAULT_PERMISSION_CLASSES': ( | ||||
|         'rest_framework.permissions.IsAuthenticated', | ||||
|     ), | ||||
|     'DEFAULT_AUTHENTICATION_CLASSES': ( | ||||
|         'knox.auth.TokenAuthentication', | ||||
|     ), | ||||
| } | ||||
|  | ||||
| if not DEBUG: | ||||
|     REST_FRAMEWORK.update({ | ||||
|         'DEFAULT_RENDERER_CLASSES': ( | ||||
|             'rest_framework.renderers.JSONRenderer', | ||||
|         ) | ||||
|     }) | ||||
|  | ||||
| SALT_USERNAME = '${SALT_USER}' | ||||
| SALT_PASSWORD = '${SALT_PASS}' | ||||
| SALT_HOST     = '${SALT_HOST}' | ||||
| MESH_USERNAME = '${MESH_USER}' | ||||
| MESH_SITE = 'https://${MESH_HOST}' | ||||
| MESH_TOKEN_KEY = '${MESH_TOKEN}' | ||||
| REDIS_HOST    = '${REDIS_HOST}' | ||||
| MESH_WS_URL = '${MESH_WS_URL}' | ||||
| ADMIN_ENABLED = True | ||||
| EOF | ||||
| )" | ||||
|  | ||||
|   echo "${localvars}" > ${WORKSPACE_DIR}/api/tacticalrmm/tacticalrmm/local_settings.py | ||||
|  | ||||
|   # run migrations and init scripts | ||||
|   python manage.py migrate --no-input | ||||
|   python manage.py collectstatic --no-input | ||||
|   python manage.py initial_db_setup | ||||
|   python manage.py initial_mesh_setup | ||||
|   python manage.py load_chocos | ||||
|   python manage.py load_community_scripts | ||||
|   python manage.py reload_nats | ||||
|   "${VIRTUAL_ENV}"/bin/python manage.py migrate --no-input | ||||
|   "${VIRTUAL_ENV}"/bin/python manage.py collectstatic --no-input | ||||
|   "${VIRTUAL_ENV}"/bin/python manage.py initial_db_setup | ||||
|   "${VIRTUAL_ENV}"/bin/python manage.py initial_mesh_setup | ||||
|   "${VIRTUAL_ENV}"/bin/python manage.py load_chocos | ||||
|   "${VIRTUAL_ENV}"/bin/python manage.py load_community_scripts | ||||
|   "${VIRTUAL_ENV}"/bin/python manage.py reload_nats | ||||
|   "${VIRTUAL_ENV}"/bin/python manage.py create_natsapi_conf | ||||
|   "${VIRTUAL_ENV}"/bin/python manage.py create_installer_user | ||||
|     "${VIRTUAL_ENV}"/bin/python manage.py post_update_tasks | ||||
|    | ||||
|  | ||||
|   # create super user  | ||||
|   echo "from accounts.models import User; User.objects.create_superuser('${TRMM_USER}', 'admin@example.com', '${TRMM_PASS}') if not User.objects.filter(username='${TRMM_USER}').exists() else 0;" | python manage.py shell | ||||
|  | ||||
| } | ||||
|  | ||||
| if [ "$1" = 'tactical-init-dev' ]; then | ||||
|  | ||||
|   # make directories if they don't exist | ||||
|   mkdir -p ${TACTICAL_DIR}/tmp | ||||
|   mkdir -p "${TACTICAL_DIR}/tmp" | ||||
|  | ||||
|   test -f "${TACTICAL_READY_FILE}" && rm "${TACTICAL_READY_FILE}" | ||||
|  | ||||
|   # setup Python virtual env and install dependencies | ||||
|   test -f ${VIRTUAL_ENV} && python -m venv --copies ${VIRTUAL_ENV} | ||||
|   pip install --no-cache-dir -r /requirements.txt | ||||
|   ! test -e "${VIRTUAL_ENV}" && python -m venv ${VIRTUAL_ENV} | ||||
|   "${VIRTUAL_ENV}"/bin/pip install --no-cache-dir -r /requirements.txt | ||||
|  | ||||
|   django_setup | ||||
|  | ||||
| @@ -149,10 +127,11 @@ if [ "$1" = 'tactical-init-dev' ]; then | ||||
|   webenv="$(cat << EOF | ||||
| PROD_URL = "${HTTP_PROTOCOL}://${API_HOST}" | ||||
| DEV_URL = "${HTTP_PROTOCOL}://${API_HOST}" | ||||
| APP_URL = https://${APP_HOST} | ||||
| APP_URL = "https://${APP_HOST}" | ||||
| DOCKER_BUILD = 1 | ||||
| EOF | ||||
| )" | ||||
|   echo "${webenv}" | tee ${WORKSPACE_DIR}/web/.env > /dev/null | ||||
|   echo "${webenv}" | tee "${WORKSPACE_DIR}"/web/.env > /dev/null | ||||
|  | ||||
|   # chown everything to tactical user | ||||
|   chown -R "${TACTICAL_USER}":"${TACTICAL_USER}" "${WORKSPACE_DIR}" | ||||
| @@ -163,25 +142,27 @@ EOF | ||||
| fi | ||||
|  | ||||
| if [ "$1" = 'tactical-api' ]; then | ||||
|   cp ${WORKSPACE_DIR}/api/tacticalrmm/core/goinstaller/bin/goversioninfo /usr/local/bin/goversioninfo | ||||
|   chmod +x /usr/local/bin/goversioninfo | ||||
|    | ||||
|   check_tactical_ready | ||||
|   python manage.py runserver 0.0.0.0:${API_PORT} | ||||
|   "${VIRTUAL_ENV}"/bin/python manage.py runserver 0.0.0.0:"${API_PORT}" | ||||
| fi | ||||
|  | ||||
| if [ "$1" = 'tactical-celery-dev' ]; then | ||||
|   check_tactical_ready | ||||
|   env/bin/celery -A tacticalrmm worker -l debug | ||||
|   "${VIRTUAL_ENV}"/bin/celery -A tacticalrmm worker -l debug | ||||
| fi | ||||
|  | ||||
| if [ "$1" = 'tactical-celerybeat-dev' ]; then | ||||
|   check_tactical_ready | ||||
|   test -f "${WORKSPACE_DIR}/api/tacticalrmm/celerybeat.pid" && rm "${WORKSPACE_DIR}/api/tacticalrmm/celerybeat.pid" | ||||
|   env/bin/celery -A tacticalrmm beat -l debug | ||||
|   "${VIRTUAL_ENV}"/bin/celery -A tacticalrmm beat -l debug | ||||
| fi | ||||
|  | ||||
| if [ "$1" = 'tactical-celerywinupdate-dev' ]; then | ||||
| if [ "$1" = 'tactical-websockets-dev' ]; then | ||||
|   check_tactical_ready | ||||
|   env/bin/celery -A tacticalrmm worker -Q wupdate -l debug | ||||
|   "${VIRTUAL_ENV}"/bin/daphne tacticalrmm.asgi:application --port 8383 -b 0.0.0.0 | ||||
| fi | ||||
|  | ||||
| if [ "$1" = 'tactical-mkdocs-dev' ]; then | ||||
|   cd "${WORKSPACE_DIR}/docs" | ||||
|   "${VIRTUAL_ENV}"/bin/mkdocs serve | ||||
| fi | ||||
|   | ||||
| @@ -1,44 +1,39 @@ | ||||
| # 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 | ||||
| amqp==2.6.1 | ||||
| asgiref==3.3.1 | ||||
| asyncio-nats-client==0.11.4 | ||||
| billiard==3.6.3.0 | ||||
| celery==4.4.6 | ||||
| certifi==2020.12.5 | ||||
| cffi==1.14.3 | ||||
| chardet==3.0.4 | ||||
| cryptography==3.2.1 | ||||
| decorator==4.4.2 | ||||
| Django==3.1.4 | ||||
| django-cors-headers==3.5.0 | ||||
| django-rest-knox==4.1.0 | ||||
| djangorestframework==3.12.2 | ||||
| future==0.18.2 | ||||
| idna==2.10 | ||||
| kombu==4.6.11 | ||||
| loguru==0.5.3 | ||||
| msgpack==1.0.0 | ||||
| packaging==20.4 | ||||
| psycopg2-binary==2.8.6 | ||||
| pycparser==2.20 | ||||
| pycryptodome==3.9.9 | ||||
| pyotp==2.4.1 | ||||
| pyparsing==2.4.7 | ||||
| pytz==2020.4 | ||||
| qrcode==6.1 | ||||
| redis==3.5.3 | ||||
| requests==2.25.0 | ||||
| six==1.15.0 | ||||
| sqlparse==0.4.1 | ||||
| twilio==6.49.0 | ||||
| urllib3==1.26.2 | ||||
| validators==0.18.1 | ||||
| vine==1.3.0 | ||||
| websockets==8.1 | ||||
| zipp==3.4.0 | ||||
| asyncio-nats-client | ||||
| celery | ||||
| channels | ||||
| channels_redis | ||||
| django-ipware | ||||
| Django==3.2.10 | ||||
| django-cors-headers | ||||
| django-rest-knox | ||||
| djangorestframework | ||||
| loguru | ||||
| msgpack | ||||
| psycopg2-binary | ||||
| pycparser | ||||
| pycryptodome | ||||
| pyotp | ||||
| pyparsing | ||||
| pytz | ||||
| qrcode | ||||
| redis | ||||
| twilio | ||||
| packaging | ||||
| validators | ||||
| websockets | ||||
| black | ||||
| Werkzeug | ||||
| django-extensions | ||||
| coverage | ||||
| coveralls | ||||
| model_bakery | ||||
| mkdocs | ||||
| mkdocs-material | ||||
| pymdown-extensions | ||||
| Pygments | ||||
| mypy | ||||
| pysnooper | ||||
| isort | ||||
| drf_spectacular | ||||
| pandas | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/FUNDING.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/FUNDING.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,7 +3,7 @@ | ||||
| github: wh1te909 | ||||
| patreon: # Replace with a single Patreon username | ||||
| open_collective: # Replace with a single Open Collective username | ||||
| ko_fi: # Replace with a single Ko-fi username | ||||
| ko_fi: tacticalrmm | ||||
| tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel | ||||
| community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry | ||||
| liberapay: # Replace with a single Liberapay username | ||||
|   | ||||
							
								
								
									
										40
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| --- | ||||
| name: Bug report | ||||
| about: Create a bug report | ||||
| title: '' | ||||
| labels: '' | ||||
| assignees: '' | ||||
|  | ||||
| --- | ||||
|  | ||||
| **Server Info (please complete the following information):** | ||||
|  - OS: [e.g. Ubuntu 20.04, Debian 10] | ||||
|  - Browser: [e.g. chrome, safari] | ||||
|  - RMM Version (as shown in top left of web UI): | ||||
|  | ||||
| **Installation Method:** | ||||
|   - [ ] Standard | ||||
|   - [ ] Docker | ||||
|  | ||||
| **Agent Info (please complete the following information):** | ||||
| - Agent version (as shown in the 'Summary' tab of the agent from web UI): | ||||
| - Agent OS: [e.g. Win 10 v2004, Server 2012 R2] | ||||
|  | ||||
| **Describe the bug** | ||||
| A clear and concise description of what the bug is. | ||||
|  | ||||
| **To Reproduce** | ||||
| Steps to reproduce the behavior: | ||||
| 1. Go to '...' | ||||
| 2. Click on '....' | ||||
| 3. Scroll down to '....' | ||||
| 4. See error | ||||
|  | ||||
| **Expected behavior** | ||||
| A clear and concise description of what you expected to happen. | ||||
|  | ||||
| **Screenshots** | ||||
| If applicable, add screenshots to help explain your problem. | ||||
|  | ||||
| **Additional context** | ||||
| Add any other context about the problem here. | ||||
							
								
								
									
										20
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| --- | ||||
| name: Feature request | ||||
| about: Suggest an idea for this project | ||||
| title: '' | ||||
| labels: '' | ||||
| assignees: '' | ||||
|  | ||||
| --- | ||||
|  | ||||
| **Is your feature request related to a problem? Please describe.** | ||||
| A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] | ||||
|  | ||||
| **Describe the solution you'd like** | ||||
| A clear and concise description of what you want to happen. | ||||
|  | ||||
| **Describe alternatives you've considered** | ||||
| A clear and concise description of any alternative solutions or features you've considered. | ||||
|  | ||||
| **Additional context** | ||||
| Add any other context or screenshots about the feature request here. | ||||
							
								
								
									
										70
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | ||||
| # 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 | ||||
							
								
								
									
										22
									
								
								.github/workflows/deploy-docs.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								.github/workflows/deploy-docs.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| name: Deploy Docs | ||||
| on: | ||||
|   push: | ||||
|     branches: | ||||
|       - master | ||||
|  | ||||
| defaults: | ||||
|   run: | ||||
|     working-directory: docs | ||||
|  | ||||
| jobs: | ||||
|   deploy: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|       - uses: actions/setup-python@v2 | ||||
|         with: | ||||
|           python-version: 3.x | ||||
|       - run: pip install --upgrade pip | ||||
|       - run: pip install --upgrade setuptools wheel | ||||
|       - run: pip install mkdocs mkdocs-material pymdown-extensions | ||||
|       - run: mkdocs gh-deploy --force | ||||
							
								
								
									
										34
									
								
								.github/workflows/devskim-analysis.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								.github/workflows/devskim-analysis.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| # 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 | ||||
							
								
								
									
										10
									
								
								.github/workflows/docker-build-push.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								.github/workflows/docker-build-push.yml
									
									
									
									
										vendored
									
									
								
							| @@ -57,16 +57,6 @@ jobs: | ||||
|           platforms: linux/amd64 | ||||
|           tags: tacticalrmm/tactical-nats:${{ steps.prep.outputs.version }},tacticalrmm/tactical-nats:latest | ||||
|            | ||||
|       - name: Build and Push Tactical Salt Image | ||||
|         uses: docker/build-push-action@v2 | ||||
|         with: | ||||
|           context: . | ||||
|           push: true | ||||
|           pull: true | ||||
|           file: ./docker/containers/tactical-salt/dockerfile | ||||
|           platforms: linux/amd64 | ||||
|           tags: tacticalrmm/tactical-salt:${{ steps.prep.outputs.version }},tacticalrmm/tactical-salt:latest | ||||
|            | ||||
|       - name: Build and Push Tactical Frontend Image | ||||
|         uses: docker/build-push-action@v2 | ||||
|         with: | ||||
|   | ||||
							
								
								
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -45,3 +45,9 @@ htmlcov/ | ||||
| docker-compose.dev.yml | ||||
| docs/.vuepress/dist | ||||
| nats-rmm.conf | ||||
| .mypy_cache | ||||
| docs/site/ | ||||
| reset_db.sh | ||||
| run_go_cmd.py | ||||
| nats-api.conf | ||||
|  | ||||
|   | ||||
							
								
								
									
										7
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							| @@ -3,7 +3,14 @@ | ||||
|     "python.languageServer": "Pylance", | ||||
|     "python.analysis.extraPaths": [ | ||||
|         "api/tacticalrmm", | ||||
|         "api/env", | ||||
|     ], | ||||
|     "python.analysis.diagnosticSeverityOverrides": { | ||||
|         "reportUnusedImport": "error", | ||||
|         "reportDuplicateImport": "error", | ||||
|     }, | ||||
|     "python.analysis.memory.keepLibraryAst": true, | ||||
|     "python.linting.mypyEnabled": true, | ||||
|     "python.analysis.typeCheckingMode": "basic", | ||||
|     "python.formatting.provider": "black", | ||||
|     "editor.formatOnSave": true, | ||||
|   | ||||
							
								
								
									
										107
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										107
									
								
								README.md
									
									
									
									
									
								
							| @@ -6,15 +6,15 @@ | ||||
| [](https://github.com/python/black) | ||||
|  | ||||
| Tactical RMM is a remote monitoring & management tool for Windows computers, built with Django and Vue.\ | ||||
| It uses an [agent](https://github.com/wh1te909/rmmagent) written in golang, as well as the [SaltStack](https://github.com/saltstack/salt) api and [MeshCentral](https://github.com/Ylianst/MeshCentral) | ||||
| It uses an [agent](https://github.com/wh1te909/rmmagent) written in golang and integrates with [MeshCentral](https://github.com/Ylianst/MeshCentral) | ||||
|  | ||||
| # [LIVE DEMO](https://rmm.xlawgaming.com/) | ||||
| Demo database resets every hour. Alot of features are disabled for obvious reasons due to the nature of this app. | ||||
|  | ||||
| *Tactical RMM is currently in alpha and subject to breaking changes. Use in production at your own risk.* | ||||
| # [LIVE DEMO](https://rmm.tacticalrmm.io/) | ||||
| 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) | ||||
|  | ||||
| ### [Documentation](https://wh1te909.github.io/tacticalrmm/) | ||||
|  | ||||
| ## Features | ||||
|  | ||||
| - Teamviewer-like remote desktop control | ||||
| @@ -33,99 +33,6 @@ Demo database resets every hour. Alot of features are disabled for obvious reaso | ||||
|  | ||||
| - Windows 7, 8.1, 10, Server 2008R2, 2012R2, 2016, 2019 | ||||
|  | ||||
| ## Installation | ||||
| ## Installation / Backup / Restore / Usage | ||||
|  | ||||
| ### Requirements | ||||
| - VPS with 4GB ram (an install script is provided for Ubuntu Server 20.04 / Debian 10) | ||||
| - A domain you own with at least 3 subdomains | ||||
| - Google Authenticator app (2 factor is NOT optional) | ||||
|  | ||||
| ### Docker | ||||
| Refer to the [docker setup](docker/readme.md) | ||||
|  | ||||
|  | ||||
| ### Installation example (Ubuntu server 20.04 LTS) | ||||
|  | ||||
| Fresh VPS with latest updates\ | ||||
| login as root and create a user and add to sudoers group (we will be creating a user called tactical) | ||||
| ``` | ||||
| apt update && apt -y upgrade | ||||
| adduser tactical | ||||
| usermod -a -G sudo tactical | ||||
| ``` | ||||
|  | ||||
| switch to the tactical user and setup the firewall | ||||
| ``` | ||||
| su - tactical | ||||
| sudo ufw default deny incoming | ||||
| sudo ufw default allow outgoing | ||||
| sudo ufw allow ssh | ||||
| sudo ufw allow http | ||||
| sudo ufw allow https | ||||
| sudo ufw allow proto tcp from any to any port 4505,4506 | ||||
| sudo ufw allow proto tcp from any to any port 4222 | ||||
| sudo ufw enable && sudo ufw reload | ||||
| ``` | ||||
|  | ||||
| Our domain for this example is tacticalrmm.com | ||||
|  | ||||
| In the DNS manager of wherever our domain is hosted, we will create three A records, all pointing to the public IP address of our VPS | ||||
|  | ||||
| Create A record ```api.tacticalrmm.com``` for the django rest backend\ | ||||
| Create A record ```rmm.tacticalrmm.com``` for the vue frontend\ | ||||
| Create A record ```mesh.tacticalrmm.com``` for meshcentral | ||||
|  | ||||
| Download the install script and run it | ||||
|  | ||||
| ``` | ||||
| wget https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/install.sh | ||||
| chmod +x install.sh | ||||
| ./install.sh | ||||
| ``` | ||||
|  | ||||
|  Links will be provided at the end of the install script.\ | ||||
|  Download the executable from the first link, then open ```rmm.tacticalrmm.com``` and login.\ | ||||
|  Upload the executable when prompted during the initial setup page. | ||||
|  | ||||
|  | ||||
| ### Install an agent | ||||
| From the app's dashboard, choose Agents > Install Agent to generate an installer. | ||||
|  | ||||
| ## Updating | ||||
| Download and run [update.sh](https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/update.sh) | ||||
| ``` | ||||
| wget https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/update.sh | ||||
| chmod +x update.sh | ||||
| ./update.sh | ||||
| ``` | ||||
|  | ||||
| ## Backup | ||||
| Download [backup.sh](https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/backup.sh) | ||||
| ``` | ||||
| wget https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/backup.sh | ||||
| ``` | ||||
| Change the postgres username and password at the top of the file (you can find them in `/rmm/api/tacticalrmm/tacticalrmm/local_settings.py` under the DATABASES section) | ||||
|  | ||||
| Run it | ||||
| ``` | ||||
| chmod +x backup.sh | ||||
| ./backup.sh | ||||
| ``` | ||||
|  | ||||
| ## Restore | ||||
| Change your 3 A records to point to new server's public IP | ||||
|  | ||||
| Create same linux user account as old server and add to sudoers group and setup firewall (see install instructions above) | ||||
|  | ||||
| Copy backup file to new server | ||||
|  | ||||
| Download the restore script, and edit the postgres username/password at the top of the file. Same instructions as above in the backup steps. | ||||
| ``` | ||||
| wget https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/restore.sh | ||||
| ``` | ||||
|  | ||||
| Run the restore script, passing it the backup tar file as the first argument | ||||
| ``` | ||||
| chmod +x restore.sh | ||||
| ./restore.sh rmm-backup-xxxxxxx.tar | ||||
| ``` | ||||
| ### Refer to the [documentation](https://wh1te909.github.io/tacticalrmm/) | ||||
|   | ||||
							
								
								
									
										19
									
								
								SECURITY.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								SECURITY.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| # Security Policy | ||||
|  | ||||
| ## Supported Versions | ||||
|  | ||||
| Use this section to tell people about which versions of your project are | ||||
| currently being supported with security updates. | ||||
|  | ||||
| | Version | Supported          | | ||||
| | ------- | ------------------ | | ||||
| | 0.10.4   | :white_check_mark: | | ||||
| | < 0.10.4| :x:                | | ||||
|  | ||||
| ## Reporting a Vulnerability | ||||
|  | ||||
| Use this section to tell people how to report a vulnerability. | ||||
|  | ||||
| Tell them where to go, how often they can expect to get an update on a | ||||
| reported vulnerability, what to expect if the vulnerability is accepted or | ||||
| declined, etc. | ||||
| @@ -1,457 +0,0 @@ | ||||
| from __future__ import absolute_import | ||||
| import psutil | ||||
| import os | ||||
| import datetime | ||||
| import zlib | ||||
| import json | ||||
| import base64 | ||||
| import wmi | ||||
| import win32evtlog | ||||
| import win32con | ||||
| import win32evtlogutil | ||||
| import winerror | ||||
| from time import sleep | ||||
| import requests | ||||
| import subprocess | ||||
| import random | ||||
| import platform | ||||
|  | ||||
| ARCH = "64" if platform.machine().endswith("64") else "32" | ||||
| PROGRAM_DIR = os.path.join(os.environ["ProgramFiles"], "TacticalAgent") | ||||
| TAC_RMM = os.path.join(PROGRAM_DIR, "tacticalrmm.exe") | ||||
| NSSM = os.path.join(PROGRAM_DIR, "nssm.exe" if ARCH == "64" else "nssm-x86.exe") | ||||
| TEMP_DIR = os.path.join(os.environ["WINDIR"], "Temp") | ||||
| SYS_DRIVE = os.environ["SystemDrive"] | ||||
| PY_BIN = os.path.join(SYS_DRIVE, "\\salt", "bin", "python.exe") | ||||
| SALT_CALL = os.path.join(SYS_DRIVE, "\\salt", "salt-call.bat") | ||||
|  | ||||
|  | ||||
| def get_services(): | ||||
|     # see https://github.com/wh1te909/tacticalrmm/issues/38 | ||||
|     # for why I am manually implementing the svc.as_dict() method of psutil | ||||
|     ret = [] | ||||
|     for svc in psutil.win_service_iter(): | ||||
|         i = {} | ||||
|         try: | ||||
|             i["display_name"] = svc.display_name() | ||||
|             i["binpath"] = svc.binpath() | ||||
|             i["username"] = svc.username() | ||||
|             i["start_type"] = svc.start_type() | ||||
|             i["status"] = svc.status() | ||||
|             i["pid"] = svc.pid() | ||||
|             i["name"] = svc.name() | ||||
|             i["description"] = svc.description() | ||||
|         except Exception: | ||||
|             continue | ||||
|         else: | ||||
|             ret.append(i) | ||||
|  | ||||
|     return ret | ||||
|  | ||||
|  | ||||
| def run_python_script(filename, timeout, script_type="userdefined"): | ||||
|     # no longer used in agent version 0.11.0 | ||||
|     file_path = os.path.join(TEMP_DIR, filename) | ||||
|  | ||||
|     if os.path.exists(file_path): | ||||
|         try: | ||||
|             os.remove(file_path) | ||||
|         except: | ||||
|             pass | ||||
|  | ||||
|     if script_type == "userdefined": | ||||
|         __salt__["cp.get_file"](f"salt://scripts/userdefined/{filename}", file_path) | ||||
|     else: | ||||
|         __salt__["cp.get_file"](f"salt://scripts/{filename}", file_path) | ||||
|  | ||||
|     return __salt__["cmd.run_all"](f"{PY_BIN} {file_path}", timeout=timeout) | ||||
|  | ||||
|  | ||||
| def run_script(filepath, filename, shell, timeout, args=[], bg=False): | ||||
|     if shell == "powershell" or shell == "cmd": | ||||
|         if args: | ||||
|             return __salt__["cmd.script"]( | ||||
|                 source=filepath, | ||||
|                 args=" ".join(map(lambda x: f'"{x}"', args)), | ||||
|                 shell=shell, | ||||
|                 timeout=timeout, | ||||
|                 bg=bg, | ||||
|             ) | ||||
|         else: | ||||
|             return __salt__["cmd.script"]( | ||||
|                 source=filepath, shell=shell, timeout=timeout, bg=bg | ||||
|             ) | ||||
|  | ||||
|     elif shell == "python": | ||||
|         file_path = os.path.join(TEMP_DIR, filename) | ||||
|  | ||||
|         if os.path.exists(file_path): | ||||
|             try: | ||||
|                 os.remove(file_path) | ||||
|             except: | ||||
|                 pass | ||||
|  | ||||
|         __salt__["cp.get_file"](filepath, file_path) | ||||
|  | ||||
|         salt_cmd = "cmd.run_bg" if bg else "cmd.run_all" | ||||
|  | ||||
|         if args: | ||||
|             a = " ".join(map(lambda x: f'"{x}"', args)) | ||||
|             cmd = f"{PY_BIN} {file_path} {a}" | ||||
|             return __salt__[salt_cmd](cmd, timeout=timeout) | ||||
|         else: | ||||
|             return __salt__[salt_cmd](f"{PY_BIN} {file_path}", timeout=timeout) | ||||
|  | ||||
|  | ||||
| def uninstall_agent(): | ||||
|     remove_exe = os.path.join(PROGRAM_DIR, "unins000.exe") | ||||
|     __salt__["cmd.run_bg"]([remove_exe, "/VERYSILENT", "/SUPPRESSMSGBOXES"]) | ||||
|     return "ok" | ||||
|  | ||||
|  | ||||
| def update_salt(): | ||||
|     for p in psutil.process_iter(): | ||||
|         with p.oneshot(): | ||||
|             if p.name() == "tacticalrmm.exe" and "updatesalt" in p.cmdline(): | ||||
|                 return "running" | ||||
|  | ||||
|     from subprocess import Popen, PIPE | ||||
|  | ||||
|     CREATE_NEW_PROCESS_GROUP = 0x00000200 | ||||
|     DETACHED_PROCESS = 0x00000008 | ||||
|     cmd = [TAC_RMM, "-m", "updatesalt"] | ||||
|     p = Popen( | ||||
|         cmd, | ||||
|         stdin=PIPE, | ||||
|         stdout=PIPE, | ||||
|         stderr=PIPE, | ||||
|         close_fds=True, | ||||
|         creationflags=DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP, | ||||
|     ) | ||||
|     return p.pid | ||||
|  | ||||
|  | ||||
| def run_manual_checks(): | ||||
|     __salt__["cmd.run_bg"]([TAC_RMM, "-m", "runchecks"]) | ||||
|     return "ok" | ||||
|  | ||||
|  | ||||
| def install_updates(): | ||||
|     for p in psutil.process_iter(): | ||||
|         with p.oneshot(): | ||||
|             if p.name() == "tacticalrmm.exe" and "winupdater" in p.cmdline(): | ||||
|                 return "running" | ||||
|  | ||||
|     return __salt__["cmd.run_bg"]([TAC_RMM, "-m", "winupdater"]) | ||||
|  | ||||
|  | ||||
| def _wait_for_service(svc, status, retries=10): | ||||
|     attempts = 0 | ||||
|     while 1: | ||||
|         try: | ||||
|             service = psutil.win_service_get(svc) | ||||
|         except psutil.NoSuchProcess: | ||||
|             stat = "fail" | ||||
|             attempts += 1 | ||||
|             sleep(5) | ||||
|         else: | ||||
|             stat = service.status() | ||||
|             if stat != status: | ||||
|                 attempts += 1 | ||||
|                 sleep(5) | ||||
|             else: | ||||
|                 attempts = 0 | ||||
|  | ||||
|         if attempts == 0 or attempts > retries: | ||||
|             break | ||||
|  | ||||
|     return stat | ||||
|  | ||||
|  | ||||
| def agent_update_v2(inno, url): | ||||
|     # make sure another instance of the update is not running | ||||
|     # this function spawns 2 instances of itself (because we call it twice with salt run_bg) | ||||
|     # so if more than 2 running, don't continue as an update is already running | ||||
|     count = 0 | ||||
|     for p in psutil.process_iter(): | ||||
|         try: | ||||
|             with p.oneshot(): | ||||
|                 if "win_agent.agent_update_v2" in p.cmdline(): | ||||
|                     count += 1 | ||||
|         except Exception: | ||||
|             continue | ||||
|  | ||||
|     if count > 2: | ||||
|         return "already running" | ||||
|  | ||||
|     sleep(random.randint(1, 20))  # don't flood the rmm | ||||
|  | ||||
|     exe = os.path.join(TEMP_DIR, inno) | ||||
|  | ||||
|     if os.path.exists(exe): | ||||
|         try: | ||||
|             os.remove(exe) | ||||
|         except: | ||||
|             pass | ||||
|  | ||||
|     try: | ||||
|         r = requests.get(url, stream=True, timeout=600) | ||||
|     except Exception: | ||||
|         return "failed" | ||||
|  | ||||
|     if r.status_code != 200: | ||||
|         return "failed" | ||||
|  | ||||
|     with open(exe, "wb") as f: | ||||
|         for chunk in r.iter_content(chunk_size=1024): | ||||
|             if chunk: | ||||
|                 f.write(chunk) | ||||
|     del r | ||||
|  | ||||
|     ret = subprocess.run([exe, "/VERYSILENT", "/SUPPRESSMSGBOXES"], timeout=120) | ||||
|  | ||||
|     tac = _wait_for_service(svc="tacticalagent", status="running") | ||||
|     if tac != "running": | ||||
|         subprocess.run([NSSM, "start", "tacticalagent"], timeout=30) | ||||
|  | ||||
|     chk = _wait_for_service(svc="checkrunner", status="running") | ||||
|     if chk != "running": | ||||
|         subprocess.run([NSSM, "start", "checkrunner"], timeout=30) | ||||
|  | ||||
|     return "ok" | ||||
|  | ||||
|  | ||||
| def do_agent_update_v2(inno, url): | ||||
|     return __salt__["cmd.run_bg"]( | ||||
|         [ | ||||
|             SALT_CALL, | ||||
|             "win_agent.agent_update_v2", | ||||
|             f"inno={inno}", | ||||
|             f"url={url}", | ||||
|             "--local", | ||||
|         ] | ||||
|     ) | ||||
|  | ||||
|  | ||||
| def agent_update(version, url): | ||||
|     # make sure another instance of the update is not running | ||||
|     # this function spawns 2 instances of itself so if more than 2 running, | ||||
|     # don't continue as an update is already running | ||||
|     count = 0 | ||||
|     for p in psutil.process_iter(): | ||||
|         try: | ||||
|             with p.oneshot(): | ||||
|                 if "win_agent.agent_update" in p.cmdline(): | ||||
|                     count += 1 | ||||
|         except Exception: | ||||
|             continue | ||||
|  | ||||
|     if count > 2: | ||||
|         return "already running" | ||||
|  | ||||
|     sleep(random.randint(1, 60))  # don't flood the rmm | ||||
|     try: | ||||
|         r = requests.get(url, stream=True, timeout=600) | ||||
|     except Exception: | ||||
|         return "failed" | ||||
|  | ||||
|     if r.status_code != 200: | ||||
|         return "failed" | ||||
|  | ||||
|     exe = os.path.join(TEMP_DIR, f"winagent-v{version}.exe") | ||||
|  | ||||
|     with open(exe, "wb") as f: | ||||
|         for chunk in r.iter_content(chunk_size=1024): | ||||
|             if chunk: | ||||
|                 f.write(chunk) | ||||
|     del r | ||||
|  | ||||
|     services = ("tacticalagent", "checkrunner") | ||||
|  | ||||
|     for svc in services: | ||||
|         subprocess.run([NSSM, "stop", svc], timeout=120) | ||||
|  | ||||
|     sleep(10) | ||||
|     r = subprocess.run([exe, "/VERYSILENT", "/SUPPRESSMSGBOXES"], timeout=300) | ||||
|     sleep(30) | ||||
|  | ||||
|     for svc in services: | ||||
|         subprocess.run([NSSM, "start", svc], timeout=120) | ||||
|  | ||||
|     return "ok" | ||||
|  | ||||
|  | ||||
| def do_agent_update(version, url): | ||||
|     return __salt__["cmd.run_bg"]( | ||||
|         [ | ||||
|             SALT_CALL, | ||||
|             "win_agent.agent_update", | ||||
|             f"version={version}", | ||||
|             f"url={url}", | ||||
|             "--local", | ||||
|         ] | ||||
|     ) | ||||
|  | ||||
|  | ||||
| class SystemDetail: | ||||
|     def __init__(self): | ||||
|         self.c = wmi.WMI() | ||||
|         self.comp_sys_prod = self.c.Win32_ComputerSystemProduct() | ||||
|         self.comp_sys = self.c.Win32_ComputerSystem() | ||||
|         self.memory = self.c.Win32_PhysicalMemory() | ||||
|         self.os = self.c.Win32_OperatingSystem() | ||||
|         self.base_board = self.c.Win32_BaseBoard() | ||||
|         self.bios = self.c.Win32_BIOS() | ||||
|         self.disk = self.c.Win32_DiskDrive() | ||||
|         self.network_adapter = self.c.Win32_NetworkAdapter() | ||||
|         self.network_config = self.c.Win32_NetworkAdapterConfiguration() | ||||
|         self.desktop_monitor = self.c.Win32_DesktopMonitor() | ||||
|         self.cpu = self.c.Win32_Processor() | ||||
|         self.usb = self.c.Win32_USBController() | ||||
|  | ||||
|     def get_all(self, obj): | ||||
|         ret = [] | ||||
|         for i in obj: | ||||
|             tmp = [ | ||||
|                 {j: getattr(i, j)} | ||||
|                 for j in list(i.properties) | ||||
|                 if getattr(i, j) is not None | ||||
|             ] | ||||
|             ret.append(tmp) | ||||
|  | ||||
|         return ret | ||||
|  | ||||
|  | ||||
| def system_info(): | ||||
|     info = SystemDetail() | ||||
|     return { | ||||
|         "comp_sys_prod": info.get_all(info.comp_sys_prod), | ||||
|         "comp_sys": info.get_all(info.comp_sys), | ||||
|         "mem": info.get_all(info.memory), | ||||
|         "os": info.get_all(info.os), | ||||
|         "base_board": info.get_all(info.base_board), | ||||
|         "bios": info.get_all(info.bios), | ||||
|         "disk": info.get_all(info.disk), | ||||
|         "network_adapter": info.get_all(info.network_adapter), | ||||
|         "network_config": info.get_all(info.network_config), | ||||
|         "desktop_monitor": info.get_all(info.desktop_monitor), | ||||
|         "cpu": info.get_all(info.cpu), | ||||
|         "usb": info.get_all(info.usb), | ||||
|     } | ||||
|  | ||||
|  | ||||
| def local_sys_info(): | ||||
|     return __salt__["cmd.run_bg"]([TAC_RMM, "-m", "sysinfo"]) | ||||
|  | ||||
|  | ||||
| def get_procs(): | ||||
|     ret = [] | ||||
|  | ||||
|     # setup | ||||
|     for proc in psutil.process_iter(): | ||||
|         with proc.oneshot(): | ||||
|             proc.cpu_percent(interval=None) | ||||
|  | ||||
|     # need time for psutil to record cpu percent | ||||
|     sleep(1) | ||||
|  | ||||
|     for c, proc in enumerate(psutil.process_iter(), 1): | ||||
|         x = {} | ||||
|         with proc.oneshot(): | ||||
|             if proc.pid == 0 or not proc.name(): | ||||
|                 continue | ||||
|  | ||||
|             x["name"] = proc.name() | ||||
|             x["cpu_percent"] = proc.cpu_percent(interval=None) / psutil.cpu_count() | ||||
|             x["memory_percent"] = proc.memory_percent() | ||||
|             x["pid"] = proc.pid | ||||
|             x["ppid"] = proc.ppid() | ||||
|             x["status"] = proc.status() | ||||
|             x["username"] = proc.username() | ||||
|             x["id"] = c | ||||
|  | ||||
|         ret.append(x) | ||||
|  | ||||
|     return ret | ||||
|  | ||||
|  | ||||
| def _compress_json(j): | ||||
|     return { | ||||
|         "wineventlog": base64.b64encode( | ||||
|             zlib.compress(json.dumps(j).encode("utf-8", errors="ignore")) | ||||
|         ).decode("ascii", errors="ignore") | ||||
|     } | ||||
|  | ||||
|  | ||||
| def get_eventlog(logtype, last_n_days): | ||||
|  | ||||
|     start_time = datetime.datetime.now() - datetime.timedelta(days=last_n_days) | ||||
|     flags = win32evtlog.EVENTLOG_BACKWARDS_READ | win32evtlog.EVENTLOG_SEQUENTIAL_READ | ||||
|  | ||||
|     status_dict = { | ||||
|         win32con.EVENTLOG_AUDIT_FAILURE: "AUDIT_FAILURE", | ||||
|         win32con.EVENTLOG_AUDIT_SUCCESS: "AUDIT_SUCCESS", | ||||
|         win32con.EVENTLOG_INFORMATION_TYPE: "INFO", | ||||
|         win32con.EVENTLOG_WARNING_TYPE: "WARNING", | ||||
|         win32con.EVENTLOG_ERROR_TYPE: "ERROR", | ||||
|         0: "INFO", | ||||
|     } | ||||
|  | ||||
|     computer = "localhost" | ||||
|     hand = win32evtlog.OpenEventLog(computer, logtype) | ||||
|     total = win32evtlog.GetNumberOfEventLogRecords(hand) | ||||
|     log = [] | ||||
|     uid = 0 | ||||
|     done = False | ||||
|  | ||||
|     try: | ||||
|         while 1: | ||||
|             events = win32evtlog.ReadEventLog(hand, flags, 0) | ||||
|             for ev_obj in events: | ||||
|  | ||||
|                 uid += 1 | ||||
|                 # return once total number of events reach or we'll be stuck in an infinite loop | ||||
|                 if uid >= total: | ||||
|                     done = True | ||||
|                     break | ||||
|  | ||||
|                 the_time = ev_obj.TimeGenerated.Format() | ||||
|                 time_obj = datetime.datetime.strptime(the_time, "%c") | ||||
|                 if time_obj < start_time: | ||||
|                     done = True | ||||
|                     break | ||||
|  | ||||
|                 computer = str(ev_obj.ComputerName) | ||||
|                 src = str(ev_obj.SourceName) | ||||
|                 evt_type = str(status_dict[ev_obj.EventType]) | ||||
|                 evt_id = str(winerror.HRESULT_CODE(ev_obj.EventID)) | ||||
|                 evt_category = str(ev_obj.EventCategory) | ||||
|                 record = str(ev_obj.RecordNumber) | ||||
|                 msg = ( | ||||
|                     str(win32evtlogutil.SafeFormatMessage(ev_obj, logtype)) | ||||
|                     .replace("<", "") | ||||
|                     .replace(">", "") | ||||
|                 ) | ||||
|  | ||||
|                 event_dict = { | ||||
|                     "computer": computer, | ||||
|                     "source": src, | ||||
|                     "eventType": evt_type, | ||||
|                     "eventID": evt_id, | ||||
|                     "eventCategory": evt_category, | ||||
|                     "message": msg, | ||||
|                     "time": the_time, | ||||
|                     "record": record, | ||||
|                     "uid": uid, | ||||
|                 } | ||||
|  | ||||
|                 log.append(event_dict) | ||||
|  | ||||
|             if done: | ||||
|                 break | ||||
|  | ||||
|     except Exception: | ||||
|         pass | ||||
|  | ||||
|     win32evtlog.CloseEventLog(hand) | ||||
|     return _compress_json(log) | ||||
| @@ -21,4 +21,6 @@ omit = | ||||
|     */tests.py | ||||
|     */test.py | ||||
|     checks/utils.py | ||||
|     */asgi.py | ||||
|     */demo_views.py | ||||
|      | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| from django.contrib import admin | ||||
|  | ||||
| from rest_framework.authtoken.admin import TokenAdmin | ||||
|  | ||||
| from .models import User | ||||
| from .models import User, Role | ||||
|  | ||||
| admin.site.register(User) | ||||
| TokenAdmin.raw_id_fields = ("user",) | ||||
| admin.site.register(Role) | ||||
|   | ||||
| @@ -0,0 +1,19 @@ | ||||
| import uuid | ||||
|  | ||||
| from django.core.management.base import BaseCommand | ||||
| from accounts.models import User | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     help = "Creates the installer user" | ||||
|  | ||||
|     def handle(self, *args, **kwargs): | ||||
|         if User.objects.filter(is_installer_user=True).exists(): | ||||
|             return | ||||
|  | ||||
|         User.objects.create_user(  # type: ignore | ||||
|             username=uuid.uuid4().hex, | ||||
|             is_installer_user=True, | ||||
|             password=User.objects.make_random_password(60),  # type: ignore | ||||
|             block_dashboard_login=True, | ||||
|         ) | ||||
| @@ -1,6 +1,5 @@ | ||||
| from django.utils import timezone as djangotime | ||||
|  | ||||
| from django.core.management.base import BaseCommand | ||||
| from django.utils import timezone as djangotime | ||||
| from knox.models import AuthToken | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -1,11 +1,13 @@ | ||||
| import pyotp | ||||
| import subprocess | ||||
|  | ||||
| import pyotp | ||||
| from django.core.management.base import BaseCommand | ||||
|  | ||||
| from accounts.models import User | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     help = "Generates barcode for Google Authenticator and creates totp for user" | ||||
|     help = "Generates barcode for Authenticator and creates totp for user" | ||||
|  | ||||
|     def add_arguments(self, parser): | ||||
|         parser.add_argument("code", type=str) | ||||
| @@ -24,12 +26,10 @@ class Command(BaseCommand): | ||||
|         url = pyotp.totp.TOTP(code).provisioning_uri(username, issuer_name=domain) | ||||
|         subprocess.run(f'qr "{url}"', shell=True) | ||||
|         self.stdout.write( | ||||
|             self.style.SUCCESS( | ||||
|                 "Scan the barcode above with your google authenticator app" | ||||
|             ) | ||||
|             self.style.SUCCESS("Scan the barcode above with your authenticator app") | ||||
|         ) | ||||
|         self.stdout.write( | ||||
|             self.style.SUCCESS( | ||||
|                 f"If that doesn't work you may manually enter the key: {code}" | ||||
|                 f"If that doesn't work you may manually enter the setup key: {code}" | ||||
|             ) | ||||
|         ) | ||||
|   | ||||
							
								
								
									
										57
									
								
								api/tacticalrmm/accounts/management/commands/reset_2fa.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								api/tacticalrmm/accounts/management/commands/reset_2fa.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| import os | ||||
| import subprocess | ||||
|  | ||||
| import pyotp | ||||
| from django.core.management.base import BaseCommand | ||||
|  | ||||
| from accounts.models import User | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     help = "Reset 2fa" | ||||
|  | ||||
|     def add_arguments(self, parser): | ||||
|         parser.add_argument("username", type=str) | ||||
|  | ||||
|     def handle(self, *args, **kwargs): | ||||
|         username = kwargs["username"] | ||||
|         try: | ||||
|             user = User.objects.get(username=username) | ||||
|         except User.DoesNotExist: | ||||
|             self.stdout.write(self.style.ERROR(f"User {username} doesn't exist")) | ||||
|             return | ||||
|  | ||||
|         domain = "Tactical RMM" | ||||
|         nginx = "/etc/nginx/sites-available/frontend.conf" | ||||
|         found = None | ||||
|         if os.path.exists(nginx): | ||||
|             try: | ||||
|                 with open(nginx, "r") as f: | ||||
|                     for line in f: | ||||
|                         if "server_name" in line: | ||||
|                             found = line | ||||
|                             break | ||||
|  | ||||
|                 if found: | ||||
|                     rep = found.replace("server_name", "").replace(";", "") | ||||
|                     domain = "".join(rep.split()) | ||||
|             except: | ||||
|                 pass | ||||
|  | ||||
|         code = pyotp.random_base32() | ||||
|         user.totp_key = code | ||||
|         user.save(update_fields=["totp_key"]) | ||||
|  | ||||
|         url = pyotp.totp.TOTP(code).provisioning_uri(username, issuer_name=domain) | ||||
|         subprocess.run(f'qr "{url}"', shell=True) | ||||
|         self.stdout.write( | ||||
|             self.style.WARNING("Scan the barcode above with your authenticator app") | ||||
|         ) | ||||
|         self.stdout.write( | ||||
|             self.style.WARNING( | ||||
|                 f"If that doesn't work you may manually enter the setup key: {code}" | ||||
|             ) | ||||
|         ) | ||||
|         self.stdout.write( | ||||
|             self.style.SUCCESS(f"2fa was successfully reset for user {username}") | ||||
|         ) | ||||
| @@ -0,0 +1,22 @@ | ||||
| from django.core.management.base import BaseCommand | ||||
| from accounts.models import User | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     help = "Reset password for user" | ||||
|  | ||||
|     def add_arguments(self, parser): | ||||
|         parser.add_argument("username", type=str) | ||||
|  | ||||
|     def handle(self, *args, **kwargs): | ||||
|         username = kwargs["username"] | ||||
|         try: | ||||
|             user = User.objects.get(username=username) | ||||
|         except User.DoesNotExist: | ||||
|             self.stdout.write(self.style.ERROR(f"User {username} doesn't exist")) | ||||
|             return | ||||
|  | ||||
|         passwd = input("Enter new password: ") | ||||
|         user.set_password(passwd) | ||||
|         user.save() | ||||
|         self.stdout.write(self.style.SUCCESS(f"Password for {username} was reset!")) | ||||
| @@ -2,8 +2,8 @@ | ||||
|  | ||||
| import django.contrib.auth.models | ||||
| import django.contrib.auth.validators | ||||
| from django.db import migrations, models | ||||
| import django.utils.timezone | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| # Generated by Django 3.1.2 on 2020-11-10 20:24 | ||||
|  | ||||
| from django.db import migrations, models | ||||
| import django.db.models.deletion | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|   | ||||
| @@ -0,0 +1,18 @@ | ||||
| # Generated by Django 3.1.7 on 2021-02-28 06:38 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('accounts', '0011_user_default_agent_tbl_tab'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='agents_per_page', | ||||
|             field=models.PositiveIntegerField(default=50), | ||||
|         ), | ||||
|     ] | ||||
| @@ -0,0 +1,18 @@ | ||||
| # Generated by Django 3.1.7 on 2021-03-09 02:33 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('accounts', '0012_user_agents_per_page'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='client_tree_sort', | ||||
|             field=models.CharField(choices=[('alphafail', 'Move failing clients to the top'), ('alpha', 'Sort alphabetically')], default='alphafail', max_length=50), | ||||
|         ), | ||||
|     ] | ||||
| @@ -0,0 +1,18 @@ | ||||
| # Generated by Django 3.2 on 2021-04-11 01:43 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('accounts', '0013_user_client_tree_sort'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='client_tree_splitter', | ||||
|             field=models.PositiveIntegerField(default=11), | ||||
|         ), | ||||
|     ] | ||||
| @@ -0,0 +1,18 @@ | ||||
| # Generated by Django 3.2 on 2021-04-11 03:03 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('accounts', '0014_user_client_tree_splitter'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='loading_bar_color', | ||||
|             field=models.CharField(default='red', max_length=255), | ||||
|         ), | ||||
|     ] | ||||
| @@ -0,0 +1,25 @@ | ||||
| # Generated by Django 3.2.1 on 2021-05-07 15:26 | ||||
|  | ||||
| import django.db.models.deletion | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('core', '0022_urlaction'), | ||||
|         ('accounts', '0015_user_loading_bar_color'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='url_action', | ||||
|             field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='user', to='core.urlaction'), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='user', | ||||
|             name='agent_dblclick_action', | ||||
|             field=models.CharField(choices=[('editagent', 'Edit Agent'), ('takecontrol', 'Take Control'), ('remotebg', 'Remote Background'), ('urlaction', 'URL Action')], default='editagent', max_length=50), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										173
									
								
								api/tacticalrmm/accounts/migrations/0017_auto_20210508_1716.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								api/tacticalrmm/accounts/migrations/0017_auto_20210508_1716.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,173 @@ | ||||
| # Generated by Django 3.2.1 on 2021-05-08 17:16 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('accounts', '0016_auto_20210507_1526'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='can_code_sign', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='can_do_server_maint', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='can_edit_agent', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='can_edit_core_settings', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='can_install_agents', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='can_manage_accounts', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='can_manage_alerts', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='can_manage_automation_policies', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='can_manage_autotasks', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='can_manage_checks', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='can_manage_clients', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='can_manage_deployments', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='can_manage_notes', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='can_manage_pendingactions', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='can_manage_procs', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='can_manage_scripts', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='can_manage_sites', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='can_manage_software', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='can_manage_winsvcs', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='can_manage_winupdates', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='can_reboot_agents', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='can_run_autotasks', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='can_run_bulk', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='can_run_checks', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='can_run_scripts', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='can_send_cmd', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='can_uninstall_agents', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='can_update_agents', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='can_use_mesh', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='can_view_auditlogs', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='can_view_debuglogs', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='can_view_eventlogs', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										181
									
								
								api/tacticalrmm/accounts/migrations/0018_auto_20210511_0233.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										181
									
								
								api/tacticalrmm/accounts/migrations/0018_auto_20210511_0233.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,181 @@ | ||||
| # Generated by Django 3.2.1 on 2021-05-11 02:33 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('accounts', '0017_auto_20210508_1716'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.CreateModel( | ||||
|             name='Role', | ||||
|             fields=[ | ||||
|                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||
|                 ('name', models.CharField(max_length=255, unique=True)), | ||||
|                 ('is_superuser', models.BooleanField(default=False)), | ||||
|                 ('can_use_mesh', models.BooleanField(default=False)), | ||||
|                 ('can_uninstall_agents', models.BooleanField(default=False)), | ||||
|                 ('can_update_agents', models.BooleanField(default=False)), | ||||
|                 ('can_edit_agent', models.BooleanField(default=False)), | ||||
|                 ('can_manage_procs', models.BooleanField(default=False)), | ||||
|                 ('can_view_eventlogs', models.BooleanField(default=False)), | ||||
|                 ('can_send_cmd', models.BooleanField(default=False)), | ||||
|                 ('can_reboot_agents', models.BooleanField(default=False)), | ||||
|                 ('can_install_agents', models.BooleanField(default=False)), | ||||
|                 ('can_run_scripts', models.BooleanField(default=False)), | ||||
|                 ('can_run_bulk', models.BooleanField(default=False)), | ||||
|                 ('can_manage_notes', models.BooleanField(default=False)), | ||||
|                 ('can_edit_core_settings', models.BooleanField(default=False)), | ||||
|                 ('can_do_server_maint', models.BooleanField(default=False)), | ||||
|                 ('can_code_sign', models.BooleanField(default=False)), | ||||
|                 ('can_manage_checks', models.BooleanField(default=False)), | ||||
|                 ('can_run_checks', models.BooleanField(default=False)), | ||||
|                 ('can_manage_clients', models.BooleanField(default=False)), | ||||
|                 ('can_manage_sites', models.BooleanField(default=False)), | ||||
|                 ('can_manage_deployments', models.BooleanField(default=False)), | ||||
|                 ('can_manage_automation_policies', models.BooleanField(default=False)), | ||||
|                 ('can_manage_autotasks', models.BooleanField(default=False)), | ||||
|                 ('can_run_autotasks', models.BooleanField(default=False)), | ||||
|                 ('can_view_auditlogs', models.BooleanField(default=False)), | ||||
|                 ('can_manage_pendingactions', models.BooleanField(default=False)), | ||||
|                 ('can_view_debuglogs', models.BooleanField(default=False)), | ||||
|                 ('can_manage_scripts', models.BooleanField(default=False)), | ||||
|                 ('can_manage_alerts', models.BooleanField(default=False)), | ||||
|                 ('can_manage_winsvcs', models.BooleanField(default=False)), | ||||
|                 ('can_manage_software', models.BooleanField(default=False)), | ||||
|                 ('can_manage_winupdates', models.BooleanField(default=False)), | ||||
|                 ('can_manage_accounts', models.BooleanField(default=False)), | ||||
|             ], | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='user', | ||||
|             name='can_code_sign', | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='user', | ||||
|             name='can_do_server_maint', | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='user', | ||||
|             name='can_edit_agent', | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='user', | ||||
|             name='can_edit_core_settings', | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='user', | ||||
|             name='can_install_agents', | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='user', | ||||
|             name='can_manage_accounts', | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='user', | ||||
|             name='can_manage_alerts', | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='user', | ||||
|             name='can_manage_automation_policies', | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='user', | ||||
|             name='can_manage_autotasks', | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='user', | ||||
|             name='can_manage_checks', | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='user', | ||||
|             name='can_manage_clients', | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='user', | ||||
|             name='can_manage_deployments', | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='user', | ||||
|             name='can_manage_notes', | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='user', | ||||
|             name='can_manage_pendingactions', | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='user', | ||||
|             name='can_manage_procs', | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='user', | ||||
|             name='can_manage_scripts', | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='user', | ||||
|             name='can_manage_sites', | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='user', | ||||
|             name='can_manage_software', | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='user', | ||||
|             name='can_manage_winsvcs', | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='user', | ||||
|             name='can_manage_winupdates', | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='user', | ||||
|             name='can_reboot_agents', | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='user', | ||||
|             name='can_run_autotasks', | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='user', | ||||
|             name='can_run_bulk', | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='user', | ||||
|             name='can_run_checks', | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='user', | ||||
|             name='can_run_scripts', | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='user', | ||||
|             name='can_send_cmd', | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='user', | ||||
|             name='can_uninstall_agents', | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='user', | ||||
|             name='can_update_agents', | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='user', | ||||
|             name='can_use_mesh', | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='user', | ||||
|             name='can_view_auditlogs', | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='user', | ||||
|             name='can_view_debuglogs', | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='user', | ||||
|             name='can_view_eventlogs', | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										25
									
								
								api/tacticalrmm/accounts/migrations/0019_user_role.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								api/tacticalrmm/accounts/migrations/0019_user_role.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| # Generated by Django 3.2.1 on 2021-05-11 02:33 | ||||
|  | ||||
| from django.db import migrations, models | ||||
| import django.db.models.deletion | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("accounts", "0018_auto_20210511_0233"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name="user", | ||||
|             name="role", | ||||
|             field=models.ForeignKey( | ||||
|                 blank=True, | ||||
|                 null=True, | ||||
|                 on_delete=django.db.models.deletion.SET_NULL, | ||||
|                 related_name="roles", | ||||
|                 to="accounts.role", | ||||
|             ), | ||||
|         ), | ||||
|     ] | ||||
| @@ -0,0 +1,18 @@ | ||||
| # Generated by Django 3.2.1 on 2021-05-11 17:37 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('accounts', '0019_user_role'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_manage_roles', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|     ] | ||||
| @@ -0,0 +1,18 @@ | ||||
| # Generated by Django 3.2.4 on 2021-06-17 04:29 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('accounts', '0020_role_can_manage_roles'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_view_core_settings', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|     ] | ||||
| @@ -0,0 +1,18 @@ | ||||
| # Generated by Django 3.2.4 on 2021-06-28 05:01 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('accounts', '0021_role_can_view_core_settings'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='clear_search_when_switching', | ||||
|             field=models.BooleanField(default=True), | ||||
|         ), | ||||
|     ] | ||||
| @@ -0,0 +1,18 @@ | ||||
| # Generated by Django 3.2.4 on 2021-06-30 03:22 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('accounts', '0022_user_clear_search_when_switching'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='is_installer_user', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|     ] | ||||
| @@ -0,0 +1,18 @@ | ||||
| # Generated by Django 3.2.1 on 2021-07-20 20:26 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('accounts', '0023_user_is_installer_user'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='last_login_ip', | ||||
|             field=models.GenericIPAddressField(blank=True, default=None, null=True), | ||||
|         ), | ||||
|     ] | ||||
| @@ -0,0 +1,33 @@ | ||||
| # Generated by Django 3.2.1 on 2021-07-21 04:24 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('accounts', '0024_user_last_login_ip'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='created_by', | ||||
|             field=models.CharField(blank=True, max_length=100, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='created_time', | ||||
|             field=models.DateTimeField(auto_now_add=True, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='modified_by', | ||||
|             field=models.CharField(blank=True, max_length=100, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='modified_time', | ||||
|             field=models.DateTimeField(auto_now=True, null=True), | ||||
|         ), | ||||
|     ] | ||||
| @@ -0,0 +1,34 @@ | ||||
| # Generated by Django 3.2.6 on 2021-09-01 12:47 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('accounts', '0025_auto_20210721_0424'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.CreateModel( | ||||
|             name='APIKey', | ||||
|             fields=[ | ||||
|                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||
|                 ('created_by', models.CharField(blank=True, max_length=100, null=True)), | ||||
|                 ('created_time', models.DateTimeField(auto_now_add=True, null=True)), | ||||
|                 ('modified_by', models.CharField(blank=True, max_length=100, null=True)), | ||||
|                 ('modified_time', models.DateTimeField(auto_now=True, null=True)), | ||||
|                 ('name', models.CharField(max_length=25, unique=True)), | ||||
|                 ('key', models.CharField(blank=True, max_length=48, unique=True)), | ||||
|                 ('expiration', models.DateTimeField(blank=True, default=None, null=True)), | ||||
|             ], | ||||
|             options={ | ||||
|                 'abstract': False, | ||||
|             }, | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_manage_api_keys', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|     ] | ||||
| @@ -0,0 +1,25 @@ | ||||
| # Generated by Django 3.2.6 on 2021-09-03 00:54 | ||||
|  | ||||
| from django.db import migrations, models | ||||
| import django.db.models.deletion | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('accounts', '0026_auto_20210901_1247'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='apikey', | ||||
|             name='user', | ||||
|             field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='api_key', to='accounts.user'), | ||||
|             preserve_default=False, | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='block_dashboard_login', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										150
									
								
								api/tacticalrmm/accounts/migrations/0028_auto_20211010_0249.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										150
									
								
								api/tacticalrmm/accounts/migrations/0028_auto_20211010_0249.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,150 @@ | ||||
| # Generated by Django 3.2.6 on 2021-10-10 02:49 | ||||
|  | ||||
| from django.db import migrations, models | ||||
| import django.db.models.deletion | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('clients', '0018_auto_20211010_0249'), | ||||
|         ('accounts', '0027_auto_20210903_0054'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_list_accounts', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_list_agent_history', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_list_agents', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_list_alerts', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_list_api_keys', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_list_automation_policies', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_list_autotasks', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_list_checks', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_list_clients', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_list_deployments', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_list_notes', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_list_pendingactions', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_list_roles', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_list_scripts', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_list_sites', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_list_software', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_ping_agents', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_recover_agents', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_view_clients', | ||||
|             field=models.ManyToManyField(blank=True, related_name='role_clients', to='clients.Client'), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_view_sites', | ||||
|             field=models.ManyToManyField(blank=True, related_name='role_sites', to='clients.Site'), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='apikey', | ||||
|             name='created_by', | ||||
|             field=models.CharField(blank=True, max_length=255, null=True), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='apikey', | ||||
|             name='modified_by', | ||||
|             field=models.CharField(blank=True, max_length=255, null=True), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='role', | ||||
|             name='created_by', | ||||
|             field=models.CharField(blank=True, max_length=255, null=True), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='role', | ||||
|             name='modified_by', | ||||
|             field=models.CharField(blank=True, max_length=255, null=True), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='user', | ||||
|             name='created_by', | ||||
|             field=models.CharField(blank=True, max_length=255, null=True), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='user', | ||||
|             name='modified_by', | ||||
|             field=models.CharField(blank=True, max_length=255, null=True), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='user', | ||||
|             name='role', | ||||
|             field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='users', to='accounts.role'), | ||||
|         ), | ||||
|     ] | ||||
| @@ -0,0 +1,28 @@ | ||||
| # Generated by Django 3.2.6 on 2021-10-22 22:45 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('accounts', '0028_auto_20211010_0249'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_list_alerttemplates', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_manage_alerttemplates', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_run_urlactions', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|     ] | ||||
| @@ -0,0 +1,23 @@ | ||||
| # Generated by Django 3.2.6 on 2021-11-04 02:21 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('accounts', '0029_auto_20211022_2245'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_manage_customfields', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='role', | ||||
|             name='can_view_customfields', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|     ] | ||||
| @@ -1,5 +1,6 @@ | ||||
| from django.db import models | ||||
| from django.contrib.auth.models import AbstractUser | ||||
| from django.db import models | ||||
| from django.db.models.fields import CharField, DateTimeField | ||||
|  | ||||
| from logs.models import BaseAuditModel | ||||
|  | ||||
| @@ -7,6 +8,7 @@ AGENT_DBLCLICK_CHOICES = [ | ||||
|     ("editagent", "Edit Agent"), | ||||
|     ("takecontrol", "Take Control"), | ||||
|     ("remotebg", "Remote Background"), | ||||
|     ("urlaction", "URL Action"), | ||||
| ] | ||||
|  | ||||
| AGENT_TBL_TAB_CHOICES = [ | ||||
| @@ -15,18 +17,40 @@ AGENT_TBL_TAB_CHOICES = [ | ||||
|     ("mixed", "Mixed"), | ||||
| ] | ||||
|  | ||||
| CLIENT_TREE_SORT_CHOICES = [ | ||||
|     ("alphafail", "Move failing clients to the top"), | ||||
|     ("alpha", "Sort alphabetically"), | ||||
| ] | ||||
|  | ||||
|  | ||||
| class User(AbstractUser, BaseAuditModel): | ||||
|     is_active = models.BooleanField(default=True) | ||||
|     block_dashboard_login = models.BooleanField(default=False) | ||||
|     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" | ||||
|     ) | ||||
|     url_action = models.ForeignKey( | ||||
|         "core.URLAction", | ||||
|         related_name="user", | ||||
|         null=True, | ||||
|         blank=True, | ||||
|         on_delete=models.SET_NULL, | ||||
|     ) | ||||
|     default_agent_tbl_tab = models.CharField( | ||||
|         max_length=50, choices=AGENT_TBL_TAB_CHOICES, default="server" | ||||
|     ) | ||||
|     agents_per_page = models.PositiveIntegerField(default=50)  # not currently used | ||||
|     client_tree_sort = models.CharField( | ||||
|         max_length=50, choices=CLIENT_TREE_SORT_CHOICES, default="alphafail" | ||||
|     ) | ||||
|     client_tree_splitter = models.PositiveIntegerField(default=11) | ||||
|     loading_bar_color = models.CharField(max_length=255, default="red") | ||||
|     clear_search_when_switching = models.BooleanField(default=True) | ||||
|     is_installer_user = models.BooleanField(default=False) | ||||
|     last_login_ip = models.GenericIPAddressField(default=None, blank=True, null=True) | ||||
|  | ||||
|     agent = models.OneToOneField( | ||||
|         "agents.Agent", | ||||
| @@ -36,9 +60,141 @@ class User(AbstractUser, BaseAuditModel): | ||||
|         on_delete=models.CASCADE, | ||||
|     ) | ||||
|  | ||||
|     role = models.ForeignKey( | ||||
|         "accounts.Role", | ||||
|         null=True, | ||||
|         blank=True, | ||||
|         related_name="users", | ||||
|         on_delete=models.SET_NULL, | ||||
|     ) | ||||
|  | ||||
|     @staticmethod | ||||
|     def serialize(user): | ||||
|         # serializes the task and returns json | ||||
|         from .serializers import UserSerializer | ||||
|  | ||||
|         return UserSerializer(user).data | ||||
|  | ||||
|  | ||||
| class Role(BaseAuditModel): | ||||
|     name = models.CharField(max_length=255, unique=True) | ||||
|     is_superuser = models.BooleanField(default=False) | ||||
|  | ||||
|     # 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) | ||||
|     can_edit_agent = models.BooleanField(default=False) | ||||
|     can_manage_procs = models.BooleanField(default=False) | ||||
|     can_view_eventlogs = models.BooleanField(default=False) | ||||
|     can_send_cmd = models.BooleanField(default=False) | ||||
|     can_reboot_agents = models.BooleanField(default=False) | ||||
|     can_install_agents = models.BooleanField(default=False) | ||||
|     can_run_scripts = models.BooleanField(default=False) | ||||
|     can_run_bulk = models.BooleanField(default=False) | ||||
|     can_recover_agents = models.BooleanField(default=False) | ||||
|     can_list_agent_history = models.BooleanField(default=False) | ||||
|  | ||||
|     # core | ||||
|     can_list_notes = models.BooleanField(default=False) | ||||
|     can_manage_notes = models.BooleanField(default=False) | ||||
|     can_view_core_settings = models.BooleanField(default=False) | ||||
|     can_edit_core_settings = models.BooleanField(default=False) | ||||
|     can_do_server_maint = models.BooleanField(default=False) | ||||
|     can_code_sign = models.BooleanField(default=False) | ||||
|     can_run_urlactions = models.BooleanField(default=False) | ||||
|     can_view_customfields = models.BooleanField(default=False) | ||||
|     can_manage_customfields = models.BooleanField(default=False) | ||||
|  | ||||
|     # checks | ||||
|     can_list_checks = models.BooleanField(default=False) | ||||
|     can_manage_checks = models.BooleanField(default=False) | ||||
|     can_run_checks = models.BooleanField(default=False) | ||||
|  | ||||
|     # clients | ||||
|     can_list_clients = models.BooleanField(default=False) | ||||
|     can_manage_clients = models.BooleanField(default=False) | ||||
|     can_list_sites = models.BooleanField(default=False) | ||||
|     can_manage_sites = models.BooleanField(default=False) | ||||
|     can_list_deployments = models.BooleanField(default=False) | ||||
|     can_manage_deployments = models.BooleanField(default=False) | ||||
|     can_view_clients = models.ManyToManyField( | ||||
|         "clients.Client", related_name="role_clients", blank=True | ||||
|     ) | ||||
|     can_view_sites = models.ManyToManyField( | ||||
|         "clients.Site", related_name="role_sites", blank=True | ||||
|     ) | ||||
|  | ||||
|     # automation | ||||
|     can_list_automation_policies = models.BooleanField(default=False) | ||||
|     can_manage_automation_policies = models.BooleanField(default=False) | ||||
|  | ||||
|     # automated tasks | ||||
|     can_list_autotasks = models.BooleanField(default=False) | ||||
|     can_manage_autotasks = models.BooleanField(default=False) | ||||
|     can_run_autotasks = models.BooleanField(default=False) | ||||
|  | ||||
|     # logs | ||||
|     can_view_auditlogs = models.BooleanField(default=False) | ||||
|     can_list_pendingactions = models.BooleanField(default=False) | ||||
|     can_manage_pendingactions = models.BooleanField(default=False) | ||||
|     can_view_debuglogs = models.BooleanField(default=False) | ||||
|  | ||||
|     # scripts | ||||
|     can_list_scripts = models.BooleanField(default=False) | ||||
|     can_manage_scripts = models.BooleanField(default=False) | ||||
|  | ||||
|     # alerts | ||||
|     can_list_alerts = models.BooleanField(default=False) | ||||
|     can_manage_alerts = models.BooleanField(default=False) | ||||
|     can_list_alerttemplates = models.BooleanField(default=False) | ||||
|     can_manage_alerttemplates = models.BooleanField(default=False) | ||||
|  | ||||
|     # win services | ||||
|     can_manage_winsvcs = models.BooleanField(default=False) | ||||
|  | ||||
|     # software | ||||
|     can_list_software = models.BooleanField(default=False) | ||||
|     can_manage_software = models.BooleanField(default=False) | ||||
|  | ||||
|     # windows updates | ||||
|     can_manage_winupdates = models.BooleanField(default=False) | ||||
|  | ||||
|     # accounts | ||||
|     can_list_accounts = models.BooleanField(default=False) | ||||
|     can_manage_accounts = models.BooleanField(default=False) | ||||
|     can_list_roles = models.BooleanField(default=False) | ||||
|     can_manage_roles = models.BooleanField(default=False) | ||||
|  | ||||
|     # authentication | ||||
|     can_list_api_keys = models.BooleanField(default=False) | ||||
|     can_manage_api_keys = models.BooleanField(default=False) | ||||
|  | ||||
|     def __str__(self): | ||||
|         return self.name | ||||
|  | ||||
|     @staticmethod | ||||
|     def serialize(role): | ||||
|         # serializes the agent and returns json | ||||
|         from .serializers import RoleAuditSerializer | ||||
|  | ||||
|         return RoleAuditSerializer(role).data | ||||
|  | ||||
|  | ||||
| class APIKey(BaseAuditModel): | ||||
|     name = CharField(unique=True, max_length=25) | ||||
|     key = CharField(unique=True, blank=True, max_length=48) | ||||
|     expiration = DateTimeField(blank=True, null=True, default=None) | ||||
|     user = models.ForeignKey( | ||||
|         "accounts.User", | ||||
|         related_name="api_key", | ||||
|         on_delete=models.CASCADE, | ||||
|     ) | ||||
|  | ||||
|     @staticmethod | ||||
|     def serialize(apikey): | ||||
|         from .serializers import APIKeyAuditSerializer | ||||
|  | ||||
|         return APIKeyAuditSerializer(apikey).data | ||||
|   | ||||
							
								
								
									
										43
									
								
								api/tacticalrmm/accounts/permissions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								api/tacticalrmm/accounts/permissions.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| from rest_framework import permissions | ||||
|  | ||||
| from tacticalrmm.permissions import _has_perm | ||||
|  | ||||
|  | ||||
| class AccountsPerms(permissions.BasePermission): | ||||
|     def has_permission(self, r, view): | ||||
|         if r.method == "GET": | ||||
|             return _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/"] | ||||
|  | ||||
|             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 | ||||
|  | ||||
|             return _has_perm(r, "can_manage_accounts") | ||||
|  | ||||
|  | ||||
| class RolesPerms(permissions.BasePermission): | ||||
|     def has_permission(self, r, view): | ||||
|         if r.method == "GET": | ||||
|             return _has_perm(r, "can_list_roles") | ||||
|         else: | ||||
|             return _has_perm(r, "can_manage_roles") | ||||
|  | ||||
|  | ||||
| class APIKeyPerms(permissions.BasePermission): | ||||
|     def has_permission(self, r, view): | ||||
|         if r.method == "GET": | ||||
|             return _has_perm(r, "can_list_api_keys") | ||||
|  | ||||
|         return _has_perm(r, "can_manage_api_keys") | ||||
| @@ -1,17 +1,34 @@ | ||||
| import pyotp | ||||
|  | ||||
| from rest_framework.serializers import ( | ||||
|     ModelSerializer, | ||||
|     SerializerMethodField, | ||||
|     ReadOnlyField, | ||||
| ) | ||||
|  | ||||
| from .models import User | ||||
| from .models import APIKey, User, Role | ||||
|  | ||||
|  | ||||
| class UserUISerializer(ModelSerializer): | ||||
|     class Meta: | ||||
|         model = User | ||||
|         fields = [ | ||||
|             "dark_mode", | ||||
|             "show_community_scripts", | ||||
|             "agent_dblclick_action", | ||||
|             "url_action", | ||||
|             "default_agent_tbl_tab", | ||||
|             "client_tree_sort", | ||||
|             "client_tree_splitter", | ||||
|             "loading_bar_color", | ||||
|             "clear_search_when_switching", | ||||
|             "block_dashboard_login", | ||||
|         ] | ||||
|  | ||||
|  | ||||
| class UserSerializer(ModelSerializer): | ||||
|     class Meta: | ||||
|         model = User | ||||
|         fields = ( | ||||
|         fields = [ | ||||
|             "id", | ||||
|             "username", | ||||
|             "first_name", | ||||
| @@ -19,7 +36,10 @@ class UserSerializer(ModelSerializer): | ||||
|             "email", | ||||
|             "is_active", | ||||
|             "last_login", | ||||
|         ) | ||||
|             "last_login_ip", | ||||
|             "role", | ||||
|             "block_dashboard_login", | ||||
|         ] | ||||
|  | ||||
|  | ||||
| class TOTPSetupSerializer(ModelSerializer): | ||||
| @@ -38,3 +58,41 @@ class TOTPSetupSerializer(ModelSerializer): | ||||
|         return pyotp.totp.TOTP(obj.totp_key).provisioning_uri( | ||||
|             obj.username, issuer_name="Tactical RMM" | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class RoleSerializer(ModelSerializer): | ||||
|     user_count = SerializerMethodField() | ||||
|  | ||||
|     class Meta: | ||||
|         model = Role | ||||
|         fields = "__all__" | ||||
|  | ||||
|     def get_user_count(self, obj): | ||||
|         return obj.users.count() | ||||
|  | ||||
|  | ||||
| class RoleAuditSerializer(ModelSerializer): | ||||
|     class Meta: | ||||
|         model = Role | ||||
|         fields = "__all__" | ||||
|  | ||||
|  | ||||
| class APIKeySerializer(ModelSerializer): | ||||
|  | ||||
|     username = ReadOnlyField(source="user.username") | ||||
|  | ||||
|     class Meta: | ||||
|         model = APIKey | ||||
|         fields = "__all__" | ||||
|  | ||||
|  | ||||
| class APIKeyAuditSerializer(ModelSerializer): | ||||
|     username = ReadOnlyField(source="user.username") | ||||
|  | ||||
|     class Meta: | ||||
|         model = APIKey | ||||
|         fields = [ | ||||
|             "name", | ||||
|             "username", | ||||
|             "expiration", | ||||
|         ] | ||||
|   | ||||
| @@ -1,8 +1,11 @@ | ||||
| from unittest.mock import patch | ||||
| from django.test import override_settings | ||||
|  | ||||
| from django.test import override_settings | ||||
| from model_bakery import baker, seq | ||||
| from accounts.models import User, APIKey | ||||
| from tacticalrmm.test import TacticalTestCase | ||||
| from accounts.models import User | ||||
|  | ||||
| from accounts.serializers import APIKeySerializer | ||||
|  | ||||
|  | ||||
| class TestAccounts(TacticalTestCase): | ||||
| @@ -24,12 +27,12 @@ class TestAccounts(TacticalTestCase): | ||||
|         data = {"username": "bob", "password": "a3asdsa2314"} | ||||
|         r = self.client.post(url, data, format="json") | ||||
|         self.assertEqual(r.status_code, 400) | ||||
|         self.assertEqual(r.data, "bad credentials") | ||||
|         self.assertEqual(r.data, "Bad credentials") | ||||
|  | ||||
|         data = {"username": "billy", "password": "hunter2"} | ||||
|         r = self.client.post(url, data, format="json") | ||||
|         self.assertEqual(r.status_code, 400) | ||||
|         self.assertEqual(r.data, "bad credentials") | ||||
|         self.assertEqual(r.data, "Bad credentials") | ||||
|  | ||||
|         self.bob.totp_key = "AB5RI6YPFTZAS52G" | ||||
|         self.bob.save() | ||||
| @@ -38,6 +41,12 @@ class TestAccounts(TacticalTestCase): | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         self.assertEqual(r.data, "ok") | ||||
|  | ||||
|         # test user set to block dashboard logins | ||||
|         self.bob.block_dashboard_login = True | ||||
|         self.bob.save() | ||||
|         r = self.client.post(url, data, format="json") | ||||
|         self.assertEqual(r.status_code, 400) | ||||
|  | ||||
|     @patch("pyotp.TOTP.verify") | ||||
|     def test_login_view(self, mock_verify): | ||||
|         url = "/login/" | ||||
| @@ -52,7 +61,7 @@ class TestAccounts(TacticalTestCase): | ||||
|         mock_verify.return_value = False | ||||
|         r = self.client.post(url, data, format="json") | ||||
|         self.assertEqual(r.status_code, 400) | ||||
|         self.assertEqual(r.data, "bad credentials") | ||||
|         self.assertEqual(r.data, "Bad credentials") | ||||
|  | ||||
|         mock_verify.return_value = True | ||||
|         data = {"username": "bob", "password": "asd234234asd", "twofactor": "123456"} | ||||
| @@ -270,29 +279,85 @@ class TestUserAction(TacticalTestCase): | ||||
|  | ||||
|     def test_user_ui(self): | ||||
|         url = "/accounts/users/ui/" | ||||
|         data = {"dark_mode": False} | ||||
|         r = self.client.patch(url, data, format="json") | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|  | ||||
|         data = {"show_community_scripts": True} | ||||
|         r = self.client.patch(url, data, format="json") | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|  | ||||
|         data = {"agent_dblclick_action": "editagent"} | ||||
|         r = self.client.patch(url, data, format="json") | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|  | ||||
|         data = {"agent_dblclick_action": "remotebg"} | ||||
|         r = self.client.patch(url, data, format="json") | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|  | ||||
|         data = {"agent_dblclick_action": "takecontrol"} | ||||
|         data = { | ||||
|             "dark_mode": True, | ||||
|             "show_community_scripts": True, | ||||
|             "agent_dblclick_action": "editagent", | ||||
|             "default_agent_tbl_tab": "mixed", | ||||
|             "client_tree_sort": "alpha", | ||||
|             "client_tree_splitter": 14, | ||||
|             "loading_bar_color": "green", | ||||
|             "clear_search_when_switching": False, | ||||
|         } | ||||
|         r = self.client.patch(url, data, format="json") | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|  | ||||
|         self.check_not_authenticated("patch", url) | ||||
|  | ||||
|  | ||||
| class TestAPIKeyViews(TacticalTestCase): | ||||
|     def setUp(self): | ||||
|         self.setup_coresettings() | ||||
|         self.authenticate() | ||||
|  | ||||
|     def test_get_api_keys(self): | ||||
|         url = "/accounts/apikeys/" | ||||
|         apikeys = baker.make("accounts.APIKey", key=seq("APIKEY"), _quantity=3) | ||||
|  | ||||
|         serializer = APIKeySerializer(apikeys, many=True) | ||||
|         resp = self.client.get(url, format="json") | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|         self.assertEqual(serializer.data, resp.data)  # type: ignore | ||||
|  | ||||
|         self.check_not_authenticated("get", url) | ||||
|  | ||||
|     def test_add_api_keys(self): | ||||
|         url = "/accounts/apikeys/" | ||||
|  | ||||
|         user = baker.make("accounts.User") | ||||
|         data = {"name": "Name", "user": user.id, "expiration": None} | ||||
|  | ||||
|         resp = self.client.post(url, data, format="json") | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|         self.assertTrue(APIKey.objects.filter(name="Name").exists()) | ||||
|         self.assertTrue(APIKey.objects.get(name="Name").key) | ||||
|  | ||||
|         self.check_not_authenticated("post", url) | ||||
|  | ||||
|     def test_modify_api_key(self): | ||||
|         # test a call where api key doesn't exist | ||||
|         resp = self.client.put("/accounts/apikeys/500/", format="json") | ||||
|         self.assertEqual(resp.status_code, 404) | ||||
|  | ||||
|         apikey = baker.make("accounts.APIKey", name="Test") | ||||
|         url = f"/accounts/apikeys/{apikey.pk}/"  # type: ignore | ||||
|  | ||||
|         data = {"name": "New Name"}  # type: ignore | ||||
|  | ||||
|         resp = self.client.put(url, data, format="json") | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|         apikey = APIKey.objects.get(pk=apikey.pk)  # type: ignore | ||||
|         self.assertEquals(apikey.name, "New Name") | ||||
|  | ||||
|         self.check_not_authenticated("put", url) | ||||
|  | ||||
|     def test_delete_api_key(self): | ||||
|         # test a call where api key doesn't exist | ||||
|         resp = self.client.delete("/accounts/apikeys/500/", format="json") | ||||
|         self.assertEqual(resp.status_code, 404) | ||||
|  | ||||
|         # test delete api key | ||||
|         apikey = baker.make("accounts.APIKey") | ||||
|         url = f"/accounts/apikeys/{apikey.pk}/"  # type: ignore | ||||
|         resp = self.client.delete(url, format="json") | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|  | ||||
|         self.assertFalse(APIKey.objects.filter(pk=apikey.pk).exists())  # type: ignore | ||||
|  | ||||
|         self.check_not_authenticated("delete", url) | ||||
|  | ||||
|  | ||||
| class TestTOTPSetup(TacticalTestCase): | ||||
|     def setUp(self): | ||||
|         self.authenticate() | ||||
| @@ -318,3 +383,29 @@ class TestTOTPSetup(TacticalTestCase): | ||||
|         r = self.client.post(url) | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         self.assertEqual(r.data, "totp token already set") | ||||
|  | ||||
|  | ||||
| class TestAPIAuthentication(TacticalTestCase): | ||||
|     def setUp(self): | ||||
|         # create User and associate to API Key | ||||
|         self.user = User.objects.create(username="api_user", is_superuser=True) | ||||
|         self.api_key = APIKey.objects.create( | ||||
|             name="Test Token", key="123456", user=self.user | ||||
|         ) | ||||
|  | ||||
|         self.client_setup() | ||||
|  | ||||
|     def test_api_auth(self): | ||||
|         url = "/clients/" | ||||
|         # auth should fail if no header set | ||||
|         self.check_not_authenticated("get", url) | ||||
|  | ||||
|         # invalid api key in header should return code 400 | ||||
|         self.client.credentials(HTTP_X_API_KEY="000000") | ||||
|         r = self.client.get(url, format="json") | ||||
|         self.assertEqual(r.status_code, 401) | ||||
|  | ||||
|         # valid api key in header should return code 200 | ||||
|         self.client.credentials(HTTP_X_API_KEY="123456") | ||||
|         r = self.client.get(url, format="json") | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| from django.urls import path | ||||
|  | ||||
| from . import views | ||||
|  | ||||
| urlpatterns = [ | ||||
| @@ -8,4 +9,8 @@ urlpatterns = [ | ||||
|     path("users/reset_totp/", views.UserActions.as_view()), | ||||
|     path("users/setup_totp/", views.TOTPSetup.as_view()), | ||||
|     path("users/ui/", views.UserUI.as_view()), | ||||
|     path("roles/", views.GetAddRoles.as_view()), | ||||
|     path("roles/<int:pk>/", views.GetUpdateDeleteRole.as_view()), | ||||
|     path("apikeys/", views.GetAddAPIKeys.as_view()), | ||||
|     path("apikeys/<int:pk>/", views.GetUpdateDeleteAPIKey.as_view()), | ||||
| ] | ||||
|   | ||||
| @@ -1,23 +1,38 @@ | ||||
| import pyotp | ||||
|  | ||||
| from django.contrib.auth import login | ||||
| from django.conf import settings | ||||
| from django.shortcuts import get_object_or_404 | ||||
| from django.contrib.auth import login | ||||
| from django.db import IntegrityError | ||||
|  | ||||
| from rest_framework.views import APIView | ||||
| from rest_framework.authtoken.serializers import AuthTokenSerializer | ||||
| from django.shortcuts import get_object_or_404 | ||||
| from ipware import get_client_ip | ||||
| from knox.views import LoginView as KnoxLoginView | ||||
| from rest_framework.permissions import AllowAny | ||||
| from rest_framework.response import Response | ||||
| from rest_framework import status | ||||
|  | ||||
| from .models import User | ||||
| from agents.models import Agent | ||||
| from logs.models import AuditLog | ||||
| 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 .serializers import UserSerializer, TOTPSetupSerializer | ||||
| from .models import APIKey, Role, User | ||||
| from .permissions import APIKeyPerms, AccountsPerms, RolesPerms | ||||
| from .serializers import ( | ||||
|     APIKeySerializer, | ||||
|     RoleSerializer, | ||||
|     TOTPSetupSerializer, | ||||
|     UserSerializer, | ||||
|     UserUISerializer, | ||||
| ) | ||||
|  | ||||
|  | ||||
| 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): | ||||
| @@ -29,11 +44,16 @@ class CheckCreds(KnoxLoginView): | ||||
|         # check credentials | ||||
|         serializer = AuthTokenSerializer(data=request.data) | ||||
|         if not serializer.is_valid(): | ||||
|             AuditLog.audit_user_failed_login(request.data["username"]) | ||||
|             return Response("bad credentials", status=status.HTTP_400_BAD_REQUEST) | ||||
|             AuditLog.audit_user_failed_login( | ||||
|                 request.data["username"], debug_info={"ip": request._client_ip} | ||||
|             ) | ||||
|             return notify_error("Bad credentials") | ||||
|  | ||||
|         user = serializer.validated_data["user"] | ||||
|  | ||||
|         if user.block_dashboard_login: | ||||
|             return notify_error("Bad credentials") | ||||
|  | ||||
|         # if totp token not set modify response to notify frontend | ||||
|         if not user.totp_key: | ||||
|             login(request, user) | ||||
| @@ -55,33 +75,57 @@ class LoginView(KnoxLoginView): | ||||
|         serializer.is_valid(raise_exception=True) | ||||
|         user = serializer.validated_data["user"] | ||||
|  | ||||
|         if user.block_dashboard_login: | ||||
|             return notify_error("Bad credentials") | ||||
|  | ||||
|         token = request.data["twofactor"] | ||||
|         totp = pyotp.TOTP(user.totp_key) | ||||
|  | ||||
|         if settings.DEBUG and token == "sekret": | ||||
|             valid = True | ||||
|         elif getattr(settings, "DEMO", False): | ||||
|             valid = True | ||||
|         elif totp.verify(token, valid_window=10): | ||||
|             valid = True | ||||
|  | ||||
|         if valid: | ||||
|             login(request, user) | ||||
|             AuditLog.audit_user_login_successful(request.data["username"]) | ||||
|  | ||||
|             # save ip information | ||||
|             client_ip, is_routable = get_client_ip(request) | ||||
|             user.last_login_ip = client_ip | ||||
|             user.save() | ||||
|  | ||||
|             AuditLog.audit_user_login_successful( | ||||
|                 request.data["username"], debug_info={"ip": request._client_ip} | ||||
|             ) | ||||
|             return super(LoginView, self).post(request, format=None) | ||||
|         else: | ||||
|             AuditLog.audit_user_failed_twofactor(request.data["username"]) | ||||
|             return Response("bad credentials", status=status.HTTP_400_BAD_REQUEST) | ||||
|             AuditLog.audit_user_failed_twofactor( | ||||
|                 request.data["username"], debug_info={"ip": request._client_ip} | ||||
|             ) | ||||
|             return notify_error("Bad credentials") | ||||
|  | ||||
|  | ||||
| class GetAddUsers(APIView): | ||||
|     permission_classes = [IsAuthenticated, AccountsPerms] | ||||
|  | ||||
|     def get(self, request): | ||||
|         users = User.objects.filter(agent=None) | ||||
|         search = request.GET.get("search", None) | ||||
|  | ||||
|         if search: | ||||
|             users = User.objects.filter(agent=None, is_installer_user=False).filter( | ||||
|                 username__icontains=search | ||||
|             ) | ||||
|         else: | ||||
|             users = User.objects.filter(agent=None, is_installer_user=False) | ||||
|  | ||||
|         return Response(UserSerializer(users, many=True).data) | ||||
|  | ||||
|     def post(self, request): | ||||
|         # add new user | ||||
|         try: | ||||
|             user = User.objects.create_user( | ||||
|             user = User.objects.create_user(  # type: ignore | ||||
|                 request.data["username"], | ||||
|                 request.data["email"], | ||||
|                 request.data["password"], | ||||
| @@ -91,15 +135,21 @@ class GetAddUsers(APIView): | ||||
|                 f"ERROR: User {request.data['username']} already exists!" | ||||
|             ) | ||||
|  | ||||
|         user.first_name = request.data["first_name"] | ||||
|         user.last_name = request.data["last_name"] | ||||
|         # Can be changed once permissions and groups are introduced | ||||
|         user.is_superuser = True | ||||
|         if "first_name" in request.data.keys(): | ||||
|             user.first_name = request.data["first_name"] | ||||
|         if "last_name" in request.data.keys(): | ||||
|             user.last_name = request.data["last_name"] | ||||
|         if "role" in request.data.keys() and isinstance(request.data["role"], int): | ||||
|             role = get_object_or_404(Role, pk=request.data["role"]) | ||||
|             user.role = role | ||||
|  | ||||
|         user.save() | ||||
|         return Response(user.username) | ||||
|  | ||||
|  | ||||
| class GetUpdateDeleteUser(APIView): | ||||
|     permission_classes = [IsAuthenticated, AccountsPerms] | ||||
|  | ||||
|     def get(self, request, pk): | ||||
|         user = get_object_or_404(User, pk=pk) | ||||
|  | ||||
| @@ -108,11 +158,7 @@ class GetUpdateDeleteUser(APIView): | ||||
|     def put(self, request, pk): | ||||
|         user = get_object_or_404(User, pk=pk) | ||||
|  | ||||
|         if ( | ||||
|             hasattr(settings, "ROOT_USER") | ||||
|             and request.user != user | ||||
|             and user.username == settings.ROOT_USER | ||||
|         ): | ||||
|         if _is_root_user(request, user): | ||||
|             return notify_error("The root user cannot be modified from the UI") | ||||
|  | ||||
|         serializer = UserSerializer(instance=user, data=request.data, partial=True) | ||||
| @@ -123,11 +169,7 @@ class GetUpdateDeleteUser(APIView): | ||||
|  | ||||
|     def delete(self, request, pk): | ||||
|         user = get_object_or_404(User, pk=pk) | ||||
|         if ( | ||||
|             hasattr(settings, "ROOT_USER") | ||||
|             and request.user != user | ||||
|             and user.username == settings.ROOT_USER | ||||
|         ): | ||||
|         if _is_root_user(request, user): | ||||
|             return notify_error("The root user cannot be deleted from the UI") | ||||
|  | ||||
|         user.delete() | ||||
| @@ -136,15 +178,11 @@ class GetUpdateDeleteUser(APIView): | ||||
|  | ||||
|  | ||||
| class UserActions(APIView): | ||||
|  | ||||
|     permission_classes = [IsAuthenticated, AccountsPerms] | ||||
|     # reset password | ||||
|     def post(self, request): | ||||
|         user = get_object_or_404(User, pk=request.data["id"]) | ||||
|         if ( | ||||
|             hasattr(settings, "ROOT_USER") | ||||
|             and request.user != user | ||||
|             and user.username == settings.ROOT_USER | ||||
|         ): | ||||
|         if _is_root_user(request, user): | ||||
|             return notify_error("The root user cannot be modified from the UI") | ||||
|  | ||||
|         user.set_password(request.data["password"]) | ||||
| @@ -155,11 +193,7 @@ class UserActions(APIView): | ||||
|     # reset two factor token | ||||
|     def put(self, request): | ||||
|         user = get_object_or_404(User, pk=request.data["id"]) | ||||
|         if ( | ||||
|             hasattr(settings, "ROOT_USER") | ||||
|             and request.user != user | ||||
|             and user.username == settings.ROOT_USER | ||||
|         ): | ||||
|         if _is_root_user(request, user): | ||||
|             return notify_error("The root user cannot be modified from the UI") | ||||
|  | ||||
|         user.totp_key = "" | ||||
| @@ -187,19 +221,82 @@ class TOTPSetup(APIView): | ||||
|  | ||||
| class UserUI(APIView): | ||||
|     def patch(self, request): | ||||
|         user = request.user | ||||
|  | ||||
|         if "dark_mode" in request.data.keys(): | ||||
|             user.dark_mode = request.data["dark_mode"] | ||||
|             user.save(update_fields=["dark_mode"]) | ||||
|  | ||||
|         if "show_community_scripts" in request.data.keys(): | ||||
|             user.show_community_scripts = request.data["show_community_scripts"] | ||||
|             user.save(update_fields=["show_community_scripts"]) | ||||
|  | ||||
|         if "userui" in request.data.keys(): | ||||
|             user.agent_dblclick_action = request.data["agent_dblclick_action"] | ||||
|             user.default_agent_tbl_tab = request.data["default_agent_tbl_tab"] | ||||
|             user.save(update_fields=["agent_dblclick_action", "default_agent_tbl_tab"]) | ||||
|  | ||||
|         serializer = UserUISerializer( | ||||
|             instance=request.user, data=request.data, partial=True | ||||
|         ) | ||||
|         serializer.is_valid(raise_exception=True) | ||||
|         serializer.save() | ||||
|         return Response("ok") | ||||
|  | ||||
|  | ||||
| class GetAddRoles(APIView): | ||||
|     permission_classes = [IsAuthenticated, RolesPerms] | ||||
|  | ||||
|     def get(self, request): | ||||
|         roles = Role.objects.all() | ||||
|         return Response(RoleSerializer(roles, many=True).data) | ||||
|  | ||||
|     def post(self, request): | ||||
|         serializer = RoleSerializer(data=request.data) | ||||
|         serializer.is_valid(raise_exception=True) | ||||
|         serializer.save() | ||||
|         return Response("Role was added") | ||||
|  | ||||
|  | ||||
| class GetUpdateDeleteRole(APIView): | ||||
|     permission_classes = [IsAuthenticated, RolesPerms] | ||||
|  | ||||
|     def get(self, request, pk): | ||||
|         role = get_object_or_404(Role, pk=pk) | ||||
|         return Response(RoleSerializer(role).data) | ||||
|  | ||||
|     def put(self, request, pk): | ||||
|         role = get_object_or_404(Role, pk=pk) | ||||
|         serializer = RoleSerializer(instance=role, data=request.data) | ||||
|         serializer.is_valid(raise_exception=True) | ||||
|         serializer.save() | ||||
|         return Response("Role was edited") | ||||
|  | ||||
|     def delete(self, request, pk): | ||||
|         role = get_object_or_404(Role, pk=pk) | ||||
|         role.delete() | ||||
|         return Response("Role was removed") | ||||
|  | ||||
|  | ||||
| class GetAddAPIKeys(APIView): | ||||
|     permission_classes = [IsAuthenticated, APIKeyPerms] | ||||
|  | ||||
|     def get(self, request): | ||||
|         apikeys = APIKey.objects.all() | ||||
|         return Response(APIKeySerializer(apikeys, many=True).data) | ||||
|  | ||||
|     def post(self, request): | ||||
|         # generate a random API Key | ||||
|         from django.utils.crypto import get_random_string | ||||
|  | ||||
|         request.data["key"] = get_random_string(length=32).upper() | ||||
|         serializer = APIKeySerializer(data=request.data) | ||||
|         serializer.is_valid(raise_exception=True) | ||||
|         obj = serializer.save() | ||||
|         return Response("The API Key was added") | ||||
|  | ||||
|  | ||||
| class GetUpdateDeleteAPIKey(APIView): | ||||
|     permission_classes = [IsAuthenticated, APIKeyPerms] | ||||
|  | ||||
|     def put(self, request, pk): | ||||
|         apikey = get_object_or_404(APIKey, pk=pk) | ||||
|  | ||||
|         # remove API key is present in request data | ||||
|         if "key" in request.data.keys(): | ||||
|             request.data.pop("key") | ||||
|  | ||||
|         serializer = APIKeySerializer(instance=apikey, data=request.data, partial=True) | ||||
|         serializer.is_valid(raise_exception=True) | ||||
|         serializer.save() | ||||
|         return Response("The API Key was edited") | ||||
|  | ||||
|     def delete(self, request, pk): | ||||
|         apikey = get_object_or_404(APIKey, pk=pk) | ||||
|         apikey.delete() | ||||
|         return Response("The API Key was deleted") | ||||
|   | ||||
| @@ -1,8 +1,9 @@ | ||||
| from django.contrib import admin | ||||
|  | ||||
| from .models import Agent, AgentOutage, RecoveryAction, Note | ||||
| from .models import Agent, AgentCustomField, Note, RecoveryAction, AgentHistory | ||||
|  | ||||
| admin.site.register(Agent) | ||||
| admin.site.register(AgentOutage) | ||||
| admin.site.register(RecoveryAction) | ||||
| admin.site.register(Note) | ||||
| admin.site.register(AgentCustomField) | ||||
| admin.site.register(AgentHistory) | ||||
|   | ||||
| @@ -1,14 +1,12 @@ | ||||
| import json | ||||
| import os | ||||
| import random | ||||
| import string | ||||
| import os | ||||
| import json | ||||
|  | ||||
| from model_bakery.recipe import Recipe, seq | ||||
| from itertools import cycle | ||||
| from django.utils import timezone as djangotime | ||||
| from django.conf import settings | ||||
|  | ||||
| from .models import Agent | ||||
| from django.conf import settings | ||||
| from django.utils import timezone as djangotime | ||||
| from model_bakery.recipe import Recipe, foreign_key, seq | ||||
|  | ||||
|  | ||||
| def generate_agent_id(hostname): | ||||
| @@ -16,6 +14,9 @@ def generate_agent_id(hostname): | ||||
|     return f"{rand}-{hostname}" | ||||
|  | ||||
|  | ||||
| site = Recipe("clients.Site") | ||||
|  | ||||
|  | ||||
| def get_wmi_data(): | ||||
|     with open( | ||||
|         os.path.join(settings.BASE_DIR, "tacticalrmm/test_data/wmi_python_agent.json") | ||||
| @@ -24,12 +25,13 @@ def get_wmi_data(): | ||||
|  | ||||
|  | ||||
| agent = Recipe( | ||||
|     Agent, | ||||
|     "agents.Agent", | ||||
|     site=foreign_key(site), | ||||
|     hostname="DESKTOP-TEST123", | ||||
|     version="1.1.1", | ||||
|     version="1.3.0", | ||||
|     monitoring_type=cycle(["workstation", "server"]), | ||||
|     salt_id=generate_agent_id("DESKTOP-TEST123"), | ||||
|     agent_id="71AHC-AA813-HH1BC-AAHH5-00013|DESKTOP-TEST123", | ||||
|     agent_id=seq(generate_agent_id("DESKTOP-TEST123")), | ||||
|     last_seen=djangotime.now() - djangotime.timedelta(days=5), | ||||
| ) | ||||
|  | ||||
| server_agent = agent.extend( | ||||
| @@ -42,8 +44,12 @@ workstation_agent = agent.extend( | ||||
|  | ||||
| online_agent = agent.extend(last_seen=djangotime.now()) | ||||
|  | ||||
| offline_agent = agent.extend( | ||||
|     last_seen=djangotime.now() - djangotime.timedelta(minutes=7) | ||||
| ) | ||||
|  | ||||
| overdue_agent = agent.extend( | ||||
|     last_seen=djangotime.now() - djangotime.timedelta(minutes=6) | ||||
|     last_seen=djangotime.now() - djangotime.timedelta(minutes=35) | ||||
| ) | ||||
|  | ||||
| agent_with_services = agent.extend( | ||||
|   | ||||
| @@ -0,0 +1,93 @@ | ||||
| from django.core.management.base import BaseCommand | ||||
|  | ||||
| from agents.models import Agent | ||||
| from clients.models import Client, Site | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     help = "Bulk update agent offline/overdue time" | ||||
|  | ||||
|     def add_arguments(self, parser): | ||||
|         parser.add_argument("time", type=int, help="Time in minutes") | ||||
|         parser.add_argument( | ||||
|             "--client", | ||||
|             type=str, | ||||
|             help="Client Name", | ||||
|         ) | ||||
|         parser.add_argument( | ||||
|             "--site", | ||||
|             type=str, | ||||
|             help="Site Name", | ||||
|         ) | ||||
|         parser.add_argument( | ||||
|             "--offline", | ||||
|             action="store_true", | ||||
|             help="Offline", | ||||
|         ) | ||||
|         parser.add_argument( | ||||
|             "--overdue", | ||||
|             action="store_true", | ||||
|             help="Overdue", | ||||
|         ) | ||||
|         parser.add_argument( | ||||
|             "--all", | ||||
|             action="store_true", | ||||
|             help="All agents", | ||||
|         ) | ||||
|  | ||||
|     def handle(self, *args, **kwargs): | ||||
|         time = kwargs["time"] | ||||
|         client_name = kwargs["client"] | ||||
|         site_name = kwargs["site"] | ||||
|         all_agents = kwargs["all"] | ||||
|         offline = kwargs["offline"] | ||||
|         overdue = kwargs["overdue"] | ||||
|         agents = None | ||||
|  | ||||
|         if offline and time < 2: | ||||
|             self.stdout.write(self.style.ERROR("Minimum offline time is 2 minutes")) | ||||
|             return | ||||
|  | ||||
|         if overdue and time < 3: | ||||
|             self.stdout.write(self.style.ERROR("Minimum overdue time is 3 minutes")) | ||||
|             return | ||||
|  | ||||
|         if client_name: | ||||
|             try: | ||||
|                 client = Client.objects.get(name=client_name) | ||||
|             except Client.DoesNotExist: | ||||
|                 self.stdout.write( | ||||
|                     self.style.ERROR(f"Client {client_name} doesn't exist") | ||||
|                 ) | ||||
|                 return | ||||
|  | ||||
|             agents = Agent.objects.filter(site__client=client) | ||||
|  | ||||
|         elif site_name: | ||||
|             try: | ||||
|                 site = Site.objects.get(name=site_name) | ||||
|             except Site.DoesNotExist: | ||||
|                 self.stdout.write(self.style.ERROR(f"Site {site_name} doesn't exist")) | ||||
|                 return | ||||
|  | ||||
|             agents = Agent.objects.filter(site=site) | ||||
|  | ||||
|         elif all_agents: | ||||
|             agents = Agent.objects.all() | ||||
|  | ||||
|         if agents: | ||||
|             if offline: | ||||
|                 agents.update(offline_time=time) | ||||
|                 self.stdout.write( | ||||
|                     self.style.SUCCESS( | ||||
|                         f"Changed offline time on {len(agents)} agents to {time} minutes" | ||||
|                     ) | ||||
|                 ) | ||||
|  | ||||
|             if overdue: | ||||
|                 agents.update(overdue_time=time) | ||||
|                 self.stdout.write( | ||||
|                     self.style.SUCCESS( | ||||
|                         f"Changed overdue time on {len(agents)} agents to {time} minutes" | ||||
|                     ) | ||||
|                 ) | ||||
| @@ -0,0 +1,81 @@ | ||||
| import asyncio | ||||
|  | ||||
| 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.utils import AGENT_DEFER, reload_nats | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     help = "Delete old agents" | ||||
|  | ||||
|     def add_arguments(self, parser): | ||||
|         parser.add_argument( | ||||
|             "--days", | ||||
|             type=int, | ||||
|             help="Delete agents that have not checked in for this many days", | ||||
|         ) | ||||
|         parser.add_argument( | ||||
|             "--agentver", | ||||
|             type=str, | ||||
|             help="Delete agents that equal to or less than this version", | ||||
|         ) | ||||
|         parser.add_argument( | ||||
|             "--delete", | ||||
|             action="store_true", | ||||
|             help="This will delete agents", | ||||
|         ) | ||||
|  | ||||
|     def handle(self, *args, **kwargs): | ||||
|         days = kwargs["days"] | ||||
|         agentver = kwargs["agentver"] | ||||
|         delete = kwargs["delete"] | ||||
|  | ||||
|         if not days and not agentver: | ||||
|             self.stdout.write( | ||||
|                 self.style.ERROR("Must have at least one parameter: days or agentver") | ||||
|             ) | ||||
|             return | ||||
|  | ||||
|         q = Agent.objects.defer(*AGENT_DEFER) | ||||
|  | ||||
|         agents = [] | ||||
|         if days: | ||||
|             overdue = djangotime.now() - djangotime.timedelta(days=days) | ||||
|             agents = [i for i in q if i.last_seen < overdue] | ||||
|  | ||||
|         if agentver: | ||||
|             agents = [i for i in q if pyver.parse(i.version) <= pyver.parse(agentver)] | ||||
|  | ||||
|         if not agents: | ||||
|             self.stdout.write(self.style.ERROR("No agents matched")) | ||||
|             return | ||||
|  | ||||
|         deleted_count = 0 | ||||
|         for agent in agents: | ||||
|             s = f"{agent.hostname} | Version {agent.version} | Last Seen {agent.last_seen} | {agent.client} > {agent.site}" | ||||
|             if delete: | ||||
|                 s = "Deleting " + s | ||||
|                 self.stdout.write(self.style.SUCCESS(s)) | ||||
|                 asyncio.run(agent.nats_cmd({"func": "uninstall"}, wait=False)) | ||||
|                 try: | ||||
|                     agent.delete() | ||||
|                 except Exception as e: | ||||
|                     err = f"Failed to delete agent {agent.hostname}: {str(e)}" | ||||
|                     self.stdout.write(self.style.ERROR(err)) | ||||
|                 else: | ||||
|                     deleted_count += 1 | ||||
|             else: | ||||
|                 self.stdout.write(self.style.WARNING(s)) | ||||
|  | ||||
|         if delete: | ||||
|             reload_nats() | ||||
|             self.stdout.write(self.style.SUCCESS(f"Deleted {deleted_count} agents")) | ||||
|         else: | ||||
|             self.stdout.write( | ||||
|                 self.style.SUCCESS( | ||||
|                     "The above agents would be deleted. Run again with --delete to actually delete them." | ||||
|                 ) | ||||
|             ) | ||||
							
								
								
									
										36
									
								
								api/tacticalrmm/agents/management/commands/demo_cron.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								api/tacticalrmm/agents/management/commands/demo_cron.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| # import datetime as dt | ||||
| import random | ||||
|  | ||||
| from django.core.management.base import BaseCommand | ||||
| from django.utils import timezone as djangotime | ||||
| from agents.models import Agent | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     help = "stuff for demo site in cron" | ||||
|  | ||||
|     def handle(self, *args, **kwargs): | ||||
|  | ||||
|         random_dates = [] | ||||
|         now = djangotime.now() | ||||
|  | ||||
|         for _ in range(20): | ||||
|             rand = now - djangotime.timedelta(minutes=random.randint(1, 2)) | ||||
|             random_dates.append(rand) | ||||
|  | ||||
|         for _ in range(5): | ||||
|             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"]) | ||||
							
								
								
									
										668
									
								
								api/tacticalrmm/agents/management/commands/fake_agents.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										668
									
								
								api/tacticalrmm/agents/management/commands/fake_agents.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,668 @@ | ||||
| import json | ||||
| import random | ||||
| import string | ||||
| import datetime as dt | ||||
|  | ||||
| from django.core.management.base import BaseCommand | ||||
| from django.utils import timezone as djangotime | ||||
| from django.conf import settings | ||||
|  | ||||
| from accounts.models import User | ||||
| from agents.models import Agent, AgentHistory | ||||
| from clients.models import Client, Site | ||||
| from software.models import InstalledSoftware | ||||
| from winupdate.models import WinUpdate, WinUpdatePolicy | ||||
| from checks.models import Check, CheckHistory | ||||
| from scripts.models import Script | ||||
| from autotasks.models import AutomatedTask | ||||
| from automation.models import Policy | ||||
| from logs.models import PendingAction, AuditLog | ||||
|  | ||||
| from tacticalrmm.demo_data import ( | ||||
|     disks, | ||||
|     temp_dir_stdout, | ||||
|     spooler_stdout, | ||||
|     ping_fail_output, | ||||
|     ping_success_output, | ||||
| ) | ||||
|  | ||||
| AGENTS_TO_GENERATE = 250 | ||||
|  | ||||
| SVCS = settings.BASE_DIR.joinpath("tacticalrmm/test_data/winsvcs.json") | ||||
| WMI_1 = settings.BASE_DIR.joinpath("tacticalrmm/test_data/wmi1.json") | ||||
| WMI_2 = settings.BASE_DIR.joinpath("tacticalrmm/test_data/wmi2.json") | ||||
| WMI_3 = settings.BASE_DIR.joinpath("tacticalrmm/test_data/wmi3.json") | ||||
| SW_1 = settings.BASE_DIR.joinpath("tacticalrmm/test_data/software1.json") | ||||
| SW_2 = settings.BASE_DIR.joinpath("tacticalrmm/test_data/software2.json") | ||||
| WIN_UPDATES = settings.BASE_DIR.joinpath("tacticalrmm/test_data/winupdates.json") | ||||
| EVT_LOG_FAIL = settings.BASE_DIR.joinpath( | ||||
|     "tacticalrmm/test_data/eventlog_check_fail.json" | ||||
| ) | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     help = "populate database with fake agents" | ||||
|  | ||||
|     def rand_string(self, length): | ||||
|         chars = string.ascii_letters | ||||
|         return "".join(random.choice(chars) for _ in range(length)) | ||||
|  | ||||
|     def handle(self, *args, **kwargs): | ||||
|  | ||||
|         user = User.objects.first() | ||||
|         user.totp_key = "ABSA234234" | ||||
|         user.save(update_fields=["totp_key"]) | ||||
|  | ||||
|         Client.objects.all().delete() | ||||
|         Agent.objects.all().delete() | ||||
|         Check.objects.all().delete() | ||||
|         Script.objects.all().delete() | ||||
|         AutomatedTask.objects.all().delete() | ||||
|         CheckHistory.objects.all().delete() | ||||
|         Policy.objects.all().delete() | ||||
|         AuditLog.objects.all().delete() | ||||
|         PendingAction.objects.all().delete() | ||||
|  | ||||
|         Script.load_community_scripts() | ||||
|  | ||||
|         # policies | ||||
|         check_policy = Policy() | ||||
|         check_policy.name = "Demo Checks Policy" | ||||
|         check_policy.desc = "Demo Checks Policy" | ||||
|         check_policy.active = True | ||||
|         check_policy.enforced = True | ||||
|         check_policy.save() | ||||
|  | ||||
|         patch_policy = Policy() | ||||
|         patch_policy.name = "Demo Patch Policy" | ||||
|         patch_policy.desc = "Demo Patch Policy" | ||||
|         patch_policy.active = True | ||||
|         patch_policy.enforced = True | ||||
|         patch_policy.save() | ||||
|  | ||||
|         update_policy = WinUpdatePolicy() | ||||
|         update_policy.policy = patch_policy | ||||
|         update_policy.critical = "approve" | ||||
|         update_policy.important = "approve" | ||||
|         update_policy.moderate = "approve" | ||||
|         update_policy.low = "ignore" | ||||
|         update_policy.other = "ignore" | ||||
|         update_policy.run_time_days = [6, 0, 2] | ||||
|         update_policy.run_time_day = 1 | ||||
|         update_policy.reboot_after_install = "required" | ||||
|         update_policy.reprocess_failed = True | ||||
|         update_policy.email_if_fail = True | ||||
|         update_policy.save() | ||||
|  | ||||
|         clients = [ | ||||
|             "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"] | ||||
|  | ||||
|         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.save() | ||||
|         client2.save() | ||||
|         client3.save() | ||||
|         client4.save() | ||||
|         client5.save() | ||||
|         client6.save() | ||||
|  | ||||
|         for site in sites1: | ||||
|             Site(client=client1, name=site).save() | ||||
|  | ||||
|         for site in sites2: | ||||
|             Site(client=client2, name=site).save() | ||||
|  | ||||
|         for site in sites3: | ||||
|             Site(client=client3, name=site).save() | ||||
|  | ||||
|         for site in sites4: | ||||
|             Site(client=client4, name=site).save() | ||||
|  | ||||
|         for site in sites5: | ||||
|             Site(client=client5, name=site).save() | ||||
|  | ||||
|         for site in sites6: | ||||
|             Site(client=client6, name=site).save() | ||||
|  | ||||
|         hostnames = [ | ||||
|             "DC-1", | ||||
|             "DC-2", | ||||
|             "FSV-1", | ||||
|             "FSV-2", | ||||
|             "WSUS", | ||||
|             "DESKTOP-12345", | ||||
|             "LAPTOP-55443", | ||||
|         ] | ||||
|         descriptions = ["Bob's computer", "Primary DC", "File Server", "Karen's Laptop"] | ||||
|         modes = ["server", "workstation"] | ||||
|         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 = [ | ||||
|             "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"] | ||||
|  | ||||
|         total_rams = [4, 8, 16, 32, 64, 128] | ||||
|         used_rams = [10, 13, 60, 25, 76, 34, 56, 34, 39] | ||||
|  | ||||
|         now = dt.datetime.now() | ||||
|  | ||||
|         boot_times = [] | ||||
|  | ||||
|         for _ in range(15): | ||||
|             rand_hour = now - dt.timedelta(hours=random.randint(1, 22)) | ||||
|             boot_times.append(str(rand_hour.timestamp())) | ||||
|  | ||||
|         for _ in range(5): | ||||
|             rand_days = now - dt.timedelta(days=random.randint(2, 50)) | ||||
|             boot_times.append(str(rand_days.timestamp())) | ||||
|  | ||||
|         user_names = ["None", "Karen", "Steve", "jsmith", "jdoe"] | ||||
|  | ||||
|         with open(SVCS) as f: | ||||
|             services = json.load(f) | ||||
|  | ||||
|         # WMI | ||||
|         with open(WMI_1) as f: | ||||
|             wmi1 = json.load(f) | ||||
|  | ||||
|         with open(WMI_2) as f: | ||||
|             wmi2 = json.load(f) | ||||
|  | ||||
|         with open(WMI_3) as f: | ||||
|             wmi3 = json.load(f) | ||||
|  | ||||
|         wmi_details = [] | ||||
|         wmi_details.append(wmi1) | ||||
|         wmi_details.append(wmi2) | ||||
|         wmi_details.append(wmi3) | ||||
|  | ||||
|         # software | ||||
|         with open(SW_1) as f: | ||||
|             software1 = json.load(f) | ||||
|  | ||||
|         with open(SW_2) as f: | ||||
|             software2 = json.load(f) | ||||
|  | ||||
|         softwares = [] | ||||
|         softwares.append(software1) | ||||
|         softwares.append(software2) | ||||
|  | ||||
|         # windows updates | ||||
|         with open(WIN_UPDATES) as f: | ||||
|             windows_updates = json.load(f)["samplecomputer"] | ||||
|  | ||||
|         # event log check fail data | ||||
|         with open(EVT_LOG_FAIL) as f: | ||||
|             eventlog_check_fail_data = json.load(f) | ||||
|  | ||||
|         # create scripts | ||||
|  | ||||
|         clear_spool = Script() | ||||
|         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.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.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.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.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.save() | ||||
|  | ||||
|         for count_agents in range(AGENTS_TO_GENERATE): | ||||
|  | ||||
|             client = random.choice(clients) | ||||
|  | ||||
|             if client == "Company 1": | ||||
|                 site = random.choice(sites1) | ||||
|             elif client == "Company 2": | ||||
|                 site = random.choice(sites2) | ||||
|             elif client == "Company 3": | ||||
|                 site = random.choice(sites3) | ||||
|             elif client == "Company 4": | ||||
|                 site = random.choice(sites4) | ||||
|             elif client == "Company 5": | ||||
|                 site = random.choice(sites5) | ||||
|             elif client == "Company 6": | ||||
|                 site = random.choice(sites6) | ||||
|  | ||||
|             agent = Agent() | ||||
|  | ||||
|             mode = random.choice(modes) | ||||
|             if mode == "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.salt_ver = "1.1.0" | ||||
|             agent.site = Site.objects.get(name=site) | ||||
|             agent.agent_id = self.rand_string(25) | ||||
|             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.total_ram = random.choice(total_rams) | ||||
|             agent.used_ram = random.choice(used_rams) | ||||
|             agent.boot_time = random.choice(boot_times) | ||||
|             agent.logged_in_username = random.choice(user_names) | ||||
|             agent.antivirus = "windowsdefender" | ||||
|             agent.mesh_node_id = ( | ||||
|                 "3UiLhe420@kaVQ0rswzBeonW$WY0xrFFUDBQlcYdXoriLXzvPmBpMrV99vRHXFlb" | ||||
|             ) | ||||
|             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.salt_id = "not-used" | ||||
|  | ||||
|             agent.save() | ||||
|  | ||||
|             InstalledSoftware(agent=agent, software=random.choice(softwares)).save() | ||||
|  | ||||
|             if mode == "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], | ||||
|                     mandatory=windows_updates[i]["Mandatory"], | ||||
|                     title=windows_updates[i]["Title"], | ||||
|                     needs_reboot=windows_updates[i]["NeedsReboot"], | ||||
|                     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.command = "ping google.com" | ||||
|             hist.username = "demo" | ||||
|             hist.results = ping_success_output | ||||
|             hist.save() | ||||
|  | ||||
|             hist1 = AgentHistory() | ||||
|             hist1.agent = agent | ||||
|             hist1.type = "script_run" | ||||
|             hist1.script = clear_spool | ||||
|             hist1.script_results = { | ||||
|                 "id": 1, | ||||
|                 "stderr": "", | ||||
|                 "stdout": spooler_stdout, | ||||
|                 "execution_time": 3.5554593, | ||||
|                 "retcode": 0, | ||||
|             } | ||||
|             hist1.save() | ||||
|  | ||||
|             # disk space check | ||||
|             check1 = Check() | ||||
|             check1.agent = agent | ||||
|             check1.check_type = "diskspace" | ||||
|             check1.status = "passing" | ||||
|             check1.last_run = djangotime.now() | ||||
|             check1.more_info = "Total: 498.7GB, Free: 287.4GB" | ||||
|             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() | ||||
|  | ||||
|             for i in range(30): | ||||
|                 check1_history = CheckHistory() | ||||
|                 check1_history.check_id = check1.id | ||||
|                 check1_history.x = djangotime.now() - djangotime.timedelta( | ||||
|                     minutes=i * 2 | ||||
|                 ) | ||||
|                 check1_history.y = random.randint(13, 40) | ||||
|                 check1_history.save() | ||||
|  | ||||
|             # ping check | ||||
|             check2 = Check() | ||||
|             check2.agent = agent | ||||
|             check2.check_type = "ping" | ||||
|             check2.last_run = djangotime.now() | ||||
|             check2.email_alert = random.choice([True, False]) | ||||
|             check2.text_alert = random.choice([True, False]) | ||||
|  | ||||
|             if site in sites5: | ||||
|                 check2.name = "Synology NAS" | ||||
|                 check2.status = "failing" | ||||
|                 check2.ip = "172.17.14.26" | ||||
|                 check2.more_info = ping_fail_output | ||||
|             else: | ||||
|                 check2.name = "Google" | ||||
|                 check2.status = "passing" | ||||
|                 check2.ip = "8.8.8.8" | ||||
|                 check2.more_info = ping_success_output | ||||
|  | ||||
|             check2.save() | ||||
|  | ||||
|             for i in range(30): | ||||
|                 check2_history = CheckHistory() | ||||
|                 check2_history.check_id = check2.id | ||||
|                 check2_history.x = djangotime.now() - djangotime.timedelta( | ||||
|                     minutes=i * 2 | ||||
|                 ) | ||||
|                 if site in sites5: | ||||
|                     check2_history.y = 1 | ||||
|                     check2_history.results = ping_fail_output | ||||
|                 else: | ||||
|                     check2_history.y = 0 | ||||
|                     check2_history.results = ping_success_output | ||||
|                 check2_history.save() | ||||
|  | ||||
|             # cpu load check | ||||
|             check3 = Check() | ||||
|             check3.agent = agent | ||||
|             check3.check_type = "cpuload" | ||||
|             check3.status = "passing" | ||||
|             check3.last_run = djangotime.now() | ||||
|             check3.warning_threshold = 70 | ||||
|             check3.error_threshold = 90 | ||||
|             check3.history = [15, 23, 16, 22, 22, 27, 15, 23, 23, 20, 10, 10, 13, 34] | ||||
|             check3.email_alert = random.choice([True, False]) | ||||
|             check3.text_alert = random.choice([True, False]) | ||||
|             check3.save() | ||||
|  | ||||
|             for i in range(30): | ||||
|                 check3_history = CheckHistory() | ||||
|                 check3_history.check_id = check3.id | ||||
|                 check3_history.x = djangotime.now() - djangotime.timedelta( | ||||
|                     minutes=i * 2 | ||||
|                 ) | ||||
|                 check3_history.y = random.randint(2, 79) | ||||
|                 check3_history.save() | ||||
|  | ||||
|             # memory check | ||||
|             check4 = Check() | ||||
|             check4.agent = agent | ||||
|             check4.check_type = "memory" | ||||
|             check4.status = "passing" | ||||
|             check4.warning_threshold = 70 | ||||
|             check4.error_threshold = 85 | ||||
|             check4.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() | ||||
|  | ||||
|             for i in range(30): | ||||
|                 check4_history = CheckHistory() | ||||
|                 check4_history.check_id = check4.id | ||||
|                 check4_history.x = djangotime.now() - djangotime.timedelta( | ||||
|                     minutes=i * 2 | ||||
|                 ) | ||||
|                 check4_history.y = random.randint(2, 79) | ||||
|                 check4_history.save() | ||||
|  | ||||
|             # script check storage pool | ||||
|             check5 = Check() | ||||
|             check5.agent = agent | ||||
|             check5.check_type = "script" | ||||
|             check5.status = "passing" | ||||
|             check5.last_run = djangotime.now() | ||||
|             check5.email_alert = random.choice([True, False]) | ||||
|             check5.text_alert = random.choice([True, False]) | ||||
|             check5.timeout = 120 | ||||
|             check5.retcode = 0 | ||||
|             check5.execution_time = "4.0000" | ||||
|             check5.script = check_pool_health | ||||
|             check5.save() | ||||
|  | ||||
|             for i in range(30): | ||||
|                 check5_history = CheckHistory() | ||||
|                 check5_history.check_id = check5.id | ||||
|                 check5_history.x = djangotime.now() - djangotime.timedelta( | ||||
|                     minutes=i * 2 | ||||
|                 ) | ||||
|                 if i == 10 or i == 18: | ||||
|                     check5_history.y = 1 | ||||
|                 else: | ||||
|                     check5_history.y = 0 | ||||
|                 check5_history.save() | ||||
|  | ||||
|             check6 = Check() | ||||
|             check6.agent = agent | ||||
|             check6.check_type = "script" | ||||
|             check6.status = "passing" | ||||
|             check6.last_run = djangotime.now() | ||||
|             check6.email_alert = random.choice([True, False]) | ||||
|             check6.text_alert = random.choice([True, False]) | ||||
|             check6.timeout = 120 | ||||
|             check6.retcode = 0 | ||||
|             check6.execution_time = "4.0000" | ||||
|             check6.script = check_net_aware | ||||
|             check6.save() | ||||
|  | ||||
|             for i in range(30): | ||||
|                 check6_history = CheckHistory() | ||||
|                 check6_history.check_id = check6.id | ||||
|                 check6_history.x = djangotime.now() - djangotime.timedelta( | ||||
|                     minutes=i * 2 | ||||
|                 ) | ||||
|                 check6_history.y = 0 | ||||
|                 check6_history.save() | ||||
|  | ||||
|             nla_task = AutomatedTask() | ||||
|             nla_task.agent = agent | ||||
|             nla_task.script = restart_nla | ||||
|             nla_task.assigned_check = check6 | ||||
|             nla_task.name = "Restart NLA" | ||||
|             nla_task.task_type = "checkfailure" | ||||
|             nla_task.win_task_name = "demotask123" | ||||
|             nla_task.execution_time = "1.8443" | ||||
|             nla_task.last_run = djangotime.now() | ||||
|             nla_task.stdout = "no stdout" | ||||
|             nla_task.retcode = 0 | ||||
|             nla_task.sync_status = "synced" | ||||
|             nla_task.save() | ||||
|  | ||||
|             spool_task = AutomatedTask() | ||||
|             spool_task.agent = agent | ||||
|             spool_task.script = clear_spool | ||||
|             spool_task.name = "Clear the print spooler" | ||||
|             spool_task.task_type = "scheduled" | ||||
|             spool_task.run_time_bit_weekdays = 127 | ||||
|             spool_task.run_time_minute = "04:45" | ||||
|             spool_task.win_task_name = "demospool123" | ||||
|             spool_task.last_run = djangotime.now() | ||||
|             spool_task.retcode = 0 | ||||
|             spool_task.stdout = spooler_stdout | ||||
|             spool_task.sync_status = "synced" | ||||
|             spool_task.save() | ||||
|  | ||||
|             tmp_dir_task = AutomatedTask() | ||||
|             tmp_dir_task.agent = agent | ||||
|             tmp_dir_task.name = "show temp dir files" | ||||
|             tmp_dir_task.script = show_tmp_dir_script | ||||
|             tmp_dir_task.task_type = "manual" | ||||
|             tmp_dir_task.win_task_name = "demotemp" | ||||
|             tmp_dir_task.last_run = djangotime.now() | ||||
|             tmp_dir_task.stdout = temp_dir_stdout | ||||
|             tmp_dir_task.retcode = 0 | ||||
|             tmp_dir_task.sync_status = "synced" | ||||
|             tmp_dir_task.save() | ||||
|  | ||||
|             check7 = Check() | ||||
|             check7.agent = agent | ||||
|             check7.check_type = "script" | ||||
|             check7.status = "passing" | ||||
|             check7.last_run = djangotime.now() | ||||
|             check7.email_alert = random.choice([True, False]) | ||||
|             check7.text_alert = random.choice([True, False]) | ||||
|             check7.timeout = 120 | ||||
|             check7.retcode = 0 | ||||
|             check7.execution_time = "3.1337" | ||||
|             check7.script = clear_spool | ||||
|             check7.stdout = spooler_stdout | ||||
|             check7.save() | ||||
|  | ||||
|             for i in range(30): | ||||
|                 check7_history = CheckHistory() | ||||
|                 check7_history.check_id = check7.id | ||||
|                 check7_history.x = djangotime.now() - djangotime.timedelta( | ||||
|                     minutes=i * 2 | ||||
|                 ) | ||||
|                 check7_history.y = 0 | ||||
|                 check7_history.save() | ||||
|  | ||||
|             check8 = Check() | ||||
|             check8.agent = agent | ||||
|             check8.check_type = "winsvc" | ||||
|             check8.status = "passing" | ||||
|             check8.last_run = djangotime.now() | ||||
|             check8.email_alert = random.choice([True, False]) | ||||
|             check8.text_alert = random.choice([True, False]) | ||||
|             check8.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() | ||||
|  | ||||
|             for i in range(30): | ||||
|                 check8_history = CheckHistory() | ||||
|                 check8_history.check_id = check8.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" | ||||
|                 else: | ||||
|                     check8_history.y = 0 | ||||
|                     check8_history.results = "Status RUNNING" | ||||
|                 check8_history.save() | ||||
|  | ||||
|             check9 = Check() | ||||
|             check9.agent = agent | ||||
|             check9.check_type = "eventlog" | ||||
|             check9.name = "unexpected shutdown" | ||||
|  | ||||
|             check9.last_run = djangotime.now() | ||||
|             check9.email_alert = random.choice([True, False]) | ||||
|             check9.text_alert = random.choice([True, False]) | ||||
|             check9.fails_b4_alert = 2 | ||||
|  | ||||
|             if site in sites5: | ||||
|                 check9.extra_details = eventlog_check_fail_data | ||||
|                 check9.status = "failing" | ||||
|             else: | ||||
|                 check9.extra_details = {"log": []} | ||||
|                 check9.status = "passing" | ||||
|  | ||||
|             check9.log_name = "Application" | ||||
|             check9.event_id = 1001 | ||||
|             check9.event_type = "INFO" | ||||
|             check9.fail_when = "contains" | ||||
|             check9.search_last_days = 30 | ||||
|  | ||||
|             check9.save() | ||||
|  | ||||
|             for i in range(30): | ||||
|                 check9_history = CheckHistory() | ||||
|                 check9_history.check_id = check9.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() | ||||
|  | ||||
|             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() | ||||
|  | ||||
|             self.stdout.write(self.style.SUCCESS(f"Added agent # {count_agents + 1}")) | ||||
|  | ||||
|         self.stdout.write("done") | ||||
| @@ -0,0 +1,18 @@ | ||||
| from django.conf import settings | ||||
| from django.core.management.base import BaseCommand | ||||
|  | ||||
| from agents.models import Agent | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     help = "Shows online agents that are not on the latest version" | ||||
|  | ||||
|     def handle(self, *args, **kwargs): | ||||
|         q = Agent.objects.exclude(version=settings.LATEST_AGENT_VER).only( | ||||
|             "pk", "version", "last_seen", "overdue_time", "offline_time" | ||||
|         ) | ||||
|         agents = [i for i in q if i.status == "online"] | ||||
|         for agent in agents: | ||||
|             self.stdout.write( | ||||
|                 self.style.SUCCESS(f"{agent.hostname} - v{agent.version}") | ||||
|             ) | ||||
							
								
								
									
										25
									
								
								api/tacticalrmm/agents/management/commands/update_agents.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								api/tacticalrmm/agents/management/commands/update_agents.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| from django.conf import settings | ||||
| from django.core.management.base import BaseCommand | ||||
| from packaging import version as pyver | ||||
|  | ||||
| from agents.models import Agent | ||||
| from core.models import CoreSettings | ||||
| from agents.tasks import send_agent_update_task | ||||
| from tacticalrmm.utils import AGENT_DEFER | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     help = "Triggers an agent update task to run" | ||||
|  | ||||
|     def handle(self, *args, **kwargs): | ||||
|         core = CoreSettings.objects.first() | ||||
|         if not core.agent_auto_update:  # type: ignore | ||||
|             return | ||||
|  | ||||
|         q = Agent.objects.defer(*AGENT_DEFER).exclude(version=settings.LATEST_AGENT_VER) | ||||
|         agent_ids: list[str] = [ | ||||
|             i.agent_id | ||||
|             for i in q | ||||
|             if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER) | ||||
|         ] | ||||
|         send_agent_update_task.delay(agent_ids=agent_ids) | ||||
| @@ -1,8 +1,8 @@ | ||||
| # Generated by Django 3.0.6 on 2020-05-31 01:23 | ||||
|  | ||||
| import django.contrib.postgres.fields.jsonb | ||||
| from django.db import migrations, models | ||||
| import django.db.models.deletion | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| # Generated by Django 3.0.7 on 2020-06-09 16:07 | ||||
|  | ||||
| from django.db import migrations, models | ||||
| import django.db.models.deletion | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| # Generated by Django 3.0.8 on 2020-08-09 05:31 | ||||
|  | ||||
| from django.db import migrations, models | ||||
| import django.db.models.deletion | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| # Generated by Django 3.1.1 on 2020-09-22 20:57 | ||||
|  | ||||
| import django.db.models.deletion | ||||
| from django.conf import settings | ||||
| from django.db import migrations, models | ||||
| import django.db.models.deletion | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| # Generated by Django 3.1.2 on 2020-11-01 22:53 | ||||
|  | ||||
| from django.db import migrations, models | ||||
| import django.db.models.deletion | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|   | ||||
| @@ -0,0 +1,18 @@ | ||||
| # Generated by Django 3.1.4 on 2021-01-29 21:11 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('agents', '0026_auto_20201125_2334'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='agent', | ||||
|             name='overdue_dashboard_alert', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										23
									
								
								api/tacticalrmm/agents/migrations/0028_auto_20210206_1534.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								api/tacticalrmm/agents/migrations/0028_auto_20210206_1534.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| # Generated by Django 3.1.4 on 2021-02-06 15:34 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('agents', '0027_agent_overdue_dashboard_alert'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='agentoutage', | ||||
|             name='outage_email_sent_time', | ||||
|             field=models.DateTimeField(blank=True, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='agentoutage', | ||||
|             name='outage_sms_sent_time', | ||||
|             field=models.DateTimeField(blank=True, null=True), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										16
									
								
								api/tacticalrmm/agents/migrations/0029_delete_agentoutage.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								api/tacticalrmm/agents/migrations/0029_delete_agentoutage.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| # Generated by Django 3.1.4 on 2021-02-10 21:56 | ||||
|  | ||||
| from django.db import migrations | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('agents', '0028_auto_20210206_1534'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.DeleteModel( | ||||
|             name='AgentOutage', | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										18
									
								
								api/tacticalrmm/agents/migrations/0030_agent_offline_time.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								api/tacticalrmm/agents/migrations/0030_agent_offline_time.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| # Generated by Django 3.1.6 on 2021-02-16 08:50 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('agents', '0029_delete_agentoutage'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='agent', | ||||
|             name='offline_time', | ||||
|             field=models.PositiveIntegerField(default=4), | ||||
|         ), | ||||
|     ] | ||||
| @@ -0,0 +1,20 @@ | ||||
| # Generated by Django 3.1.7 on 2021-03-04 03:57 | ||||
|  | ||||
| import django.db.models.deletion | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('alerts', '0006_auto_20210217_1736'), | ||||
|         ('agents', '0030_agent_offline_time'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='agent', | ||||
|             name='alert_template', | ||||
|             field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='agents', to='alerts.alerttemplate'), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										24
									
								
								api/tacticalrmm/agents/migrations/0032_agentcustomfield.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								api/tacticalrmm/agents/migrations/0032_agentcustomfield.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| # Generated by Django 3.1.7 on 2021-03-17 14:45 | ||||
|  | ||||
| import django.db.models.deletion | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('core', '0014_customfield'), | ||||
|         ('agents', '0031_agent_alert_template'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.CreateModel( | ||||
|             name='AgentCustomField', | ||||
|             fields=[ | ||||
|                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||
|                 ('value', models.TextField(blank=True, null=True)), | ||||
|                 ('agent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='custom_fields', to='agents.agent')), | ||||
|                 ('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='agent_fields', to='core.customfield')), | ||||
|             ], | ||||
|         ), | ||||
|     ] | ||||
| @@ -0,0 +1,19 @@ | ||||
| # Generated by Django 3.1.7 on 2021-03-29 02:51 | ||||
|  | ||||
| import django.contrib.postgres.fields | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('agents', '0032_agentcustomfield'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='agentcustomfield', | ||||
|             name='multiple_value', | ||||
|             field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(blank=True, null=True), blank=True, default=list, null=True, size=None), | ||||
|         ), | ||||
|     ] | ||||
| @@ -0,0 +1,18 @@ | ||||
| # Generated by Django 3.1.7 on 2021-03-29 03:01 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('agents', '0033_agentcustomfield_multiple_value'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='agentcustomfield', | ||||
|             name='checkbox_value', | ||||
|             field=models.BooleanField(blank=True, default=False), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										23
									
								
								api/tacticalrmm/agents/migrations/0035_auto_20210329_1709.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								api/tacticalrmm/agents/migrations/0035_auto_20210329_1709.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| # Generated by Django 3.1.7 on 2021-03-29 17:09 | ||||
|  | ||||
| from django.db import migrations | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('agents', '0034_agentcustomfield_checkbox_value'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.RenameField( | ||||
|             model_name='agentcustomfield', | ||||
|             old_name='checkbox_value', | ||||
|             new_name='bool_value', | ||||
|         ), | ||||
|         migrations.RenameField( | ||||
|             model_name='agentcustomfield', | ||||
|             old_name='value', | ||||
|             new_name='string_value', | ||||
|         ), | ||||
|     ] | ||||
| @@ -0,0 +1,18 @@ | ||||
| # Generated by Django 3.1.7 on 2021-04-17 01:28 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('agents', '0035_auto_20210329_1709'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='agent', | ||||
|             name='block_policy_inheritance', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										23
									
								
								api/tacticalrmm/agents/migrations/0037_auto_20210627_0014.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								api/tacticalrmm/agents/migrations/0037_auto_20210627_0014.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| # Generated by Django 3.2.4 on 2021-06-27 00:14 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('agents', '0036_agent_block_policy_inheritance'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='agent', | ||||
|             name='has_patches_pending', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='agent', | ||||
|             name='pending_actions_count', | ||||
|             field=models.PositiveIntegerField(default=0), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										27
									
								
								api/tacticalrmm/agents/migrations/0038_agenthistory.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								api/tacticalrmm/agents/migrations/0038_agenthistory.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| # Generated by Django 3.2.1 on 2021-07-06 02:01 | ||||
|  | ||||
| from django.db import migrations, models | ||||
| import django.db.models.deletion | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('agents', '0037_auto_20210627_0014'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.CreateModel( | ||||
|             name='AgentHistory', | ||||
|             fields=[ | ||||
|                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||
|                 ('time', models.DateTimeField(auto_now_add=True)), | ||||
|                 ('type', models.CharField(choices=[('task_run', 'Task Run'), ('script_run', 'Script Run'), ('cmd_run', 'CMD Run')], default='cmd_run', max_length=50)), | ||||
|                 ('command', models.TextField(blank=True, null=True)), | ||||
|                 ('status', models.CharField(choices=[('success', 'Success'), ('failure', 'Failure')], default='success', max_length=50)), | ||||
|                 ('username', models.CharField(default='system', max_length=50)), | ||||
|                 ('results', models.TextField(blank=True, null=True)), | ||||
|                 ('agent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='history', to='agents.agent')), | ||||
|             ], | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										25
									
								
								api/tacticalrmm/agents/migrations/0039_auto_20210714_0738.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								api/tacticalrmm/agents/migrations/0039_auto_20210714_0738.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| # Generated by Django 3.2.5 on 2021-07-14 07:38 | ||||
|  | ||||
| from django.db import migrations, models | ||||
| import django.db.models.deletion | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('scripts', '0008_script_guid'), | ||||
|         ('agents', '0038_agenthistory'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='agenthistory', | ||||
|             name='script', | ||||
|             field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='history', to='scripts.script'), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='agenthistory', | ||||
|             name='script_results', | ||||
|             field=models.JSONField(blank=True, null=True), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										28
									
								
								api/tacticalrmm/agents/migrations/0040_auto_20211010_0249.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								api/tacticalrmm/agents/migrations/0040_auto_20211010_0249.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| # Generated by Django 3.2.6 on 2021-10-10 02:49 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('agents', '0039_auto_20210714_0738'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name='agent', | ||||
|             name='agent_id', | ||||
|             field=models.CharField(max_length=200, unique=True), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='agent', | ||||
|             name='created_by', | ||||
|             field=models.CharField(blank=True, max_length=255, null=True), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='agent', | ||||
|             name='modified_by', | ||||
|             field=models.CharField(blank=True, max_length=255, null=True), | ||||
|         ), | ||||
|     ] | ||||
| @@ -0,0 +1,18 @@ | ||||
| # Generated by Django 3.2.6 on 2021-10-18 03:04 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('agents', '0040_auto_20211010_0249'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name='agenthistory', | ||||
|             name='username', | ||||
|             field=models.CharField(default='system', max_length=255), | ||||
|         ), | ||||
|     ] | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										123
									
								
								api/tacticalrmm/agents/permissions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								api/tacticalrmm/agents/permissions.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,123 @@ | ||||
| from rest_framework import permissions | ||||
|  | ||||
| from tacticalrmm.permissions import _has_perm, _has_perm_on_agent | ||||
|  | ||||
|  | ||||
| class AgentPerms(permissions.BasePermission): | ||||
|     def has_permission(self, r, view): | ||||
|         if r.method == "GET": | ||||
|             if "agent_id" in view.kwargs.keys(): | ||||
|                 return _has_perm(r, "can_list_agents") and _has_perm_on_agent( | ||||
|                     r.user, view.kwargs["agent_id"] | ||||
|                 ) | ||||
|             else: | ||||
|                 return _has_perm(r, "can_list_agents") | ||||
|         elif r.method == "DELETE": | ||||
|             return _has_perm(r, "can_uninstall_agents") and _has_perm_on_agent( | ||||
|                 r.user, view.kwargs["agent_id"] | ||||
|             ) | ||||
|         else: | ||||
|             if r.path == "/agents/maintenance/bulk/": | ||||
|                 return _has_perm(r, "can_edit_agent") | ||||
|             else: | ||||
|                 return _has_perm(r, "can_edit_agent") and _has_perm_on_agent( | ||||
|                     r.user, view.kwargs["agent_id"] | ||||
|                 ) | ||||
|  | ||||
|  | ||||
| class RecoverAgentPerms(permissions.BasePermission): | ||||
|     def has_permission(self, r, view): | ||||
|         return _has_perm(r, "can_recover_agents") and _has_perm_on_agent( | ||||
|             r.user, view.kwargs["agent_id"] | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class MeshPerms(permissions.BasePermission): | ||||
|     def has_permission(self, r, view): | ||||
|         return _has_perm(r, "can_use_mesh") and _has_perm_on_agent( | ||||
|             r.user, view.kwargs["agent_id"] | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class UpdateAgentPerms(permissions.BasePermission): | ||||
|     def has_permission(self, r, view): | ||||
|         return _has_perm(r, "can_update_agents") | ||||
|  | ||||
|  | ||||
| class PingAgentPerms(permissions.BasePermission): | ||||
|     def has_permission(self, r, view): | ||||
|         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): | ||||
|         return _has_perm(r, "can_manage_procs") and _has_perm_on_agent( | ||||
|             r.user, view.kwargs["agent_id"] | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class EvtLogPerms(permissions.BasePermission): | ||||
|     def has_permission(self, r, view): | ||||
|         return _has_perm(r, "can_view_eventlogs") and _has_perm_on_agent( | ||||
|             r.user, view.kwargs["agent_id"] | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class SendCMDPerms(permissions.BasePermission): | ||||
|     def has_permission(self, r, view): | ||||
|         return _has_perm(r, "can_send_cmd") and _has_perm_on_agent( | ||||
|             r.user, view.kwargs["agent_id"] | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class RebootAgentPerms(permissions.BasePermission): | ||||
|     def has_permission(self, r, view): | ||||
|         return _has_perm(r, "can_reboot_agents") and _has_perm_on_agent( | ||||
|             r.user, view.kwargs["agent_id"] | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class InstallAgentPerms(permissions.BasePermission): | ||||
|     def has_permission(self, r, view): | ||||
|         return _has_perm(r, "can_install_agents") | ||||
|  | ||||
|  | ||||
| class RunScriptPerms(permissions.BasePermission): | ||||
|     def has_permission(self, r, view): | ||||
|         return _has_perm(r, "can_run_scripts") and _has_perm_on_agent( | ||||
|             r.user, view.kwargs["agent_id"] | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class AgentNotesPerms(permissions.BasePermission): | ||||
|     def has_permission(self, r, view): | ||||
|  | ||||
|         # 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( | ||||
|                     r.user, view.kwargs["agent_id"] | ||||
|                 ) | ||||
|             else: | ||||
|                 return _has_perm(r, "can_list_notes") | ||||
|         else: | ||||
|             return _has_perm(r, "can_manage_notes") | ||||
|  | ||||
|  | ||||
| class RunBulkPerms(permissions.BasePermission): | ||||
|     def has_permission(self, r, view): | ||||
|         return _has_perm(r, "can_run_bulk") | ||||
|  | ||||
|  | ||||
| class AgentHistoryPerms(permissions.BasePermission): | ||||
|     def has_permission(self, r, view): | ||||
|         if "agent_id" in view.kwargs.keys(): | ||||
|             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") | ||||
| @@ -1,42 +1,55 @@ | ||||
| import pytz | ||||
|  | ||||
| from rest_framework import serializers | ||||
| from rest_framework.fields import ReadOnlyField | ||||
|  | ||||
| from .models import Agent, Note | ||||
|  | ||||
| from winupdate.serializers import WinUpdatePolicySerializer | ||||
| from clients.serializers import ClientSerializer | ||||
|  | ||||
| from .models import Agent, AgentCustomField, Note, AgentHistory | ||||
|  | ||||
|  | ||||
| class AgentCustomFieldSerializer(serializers.ModelSerializer): | ||||
|     class Meta: | ||||
|         model = AgentCustomField | ||||
|         fields = ( | ||||
|             "id", | ||||
|             "field", | ||||
|             "agent", | ||||
|             "value", | ||||
|             "string_value", | ||||
|             "bool_value", | ||||
|             "multiple_value", | ||||
|         ) | ||||
|         extra_kwargs = { | ||||
|             "string_value": {"write_only": True}, | ||||
|             "bool_value": {"write_only": True}, | ||||
|             "multiple_value": {"write_only": True}, | ||||
|         } | ||||
|  | ||||
|  | ||||
| class AgentSerializer(serializers.ModelSerializer): | ||||
|     # for vue | ||||
|     patches_pending = serializers.ReadOnlyField(source="has_patches_pending") | ||||
|     winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True) | ||||
|     status = serializers.ReadOnlyField() | ||||
|     cpu_model = serializers.ReadOnlyField() | ||||
|     local_ips = serializers.ReadOnlyField() | ||||
|     make_model = serializers.ReadOnlyField() | ||||
|     physical_disks = serializers.ReadOnlyField() | ||||
|     graphics = serializers.ReadOnlyField() | ||||
|     checks = serializers.ReadOnlyField() | ||||
|     timezone = serializers.ReadOnlyField() | ||||
|     all_timezones = serializers.SerializerMethodField() | ||||
|     client_name = serializers.ReadOnlyField(source="client.name") | ||||
|     client = serializers.ReadOnlyField(source="client.name") | ||||
|     site_name = serializers.ReadOnlyField(source="site.name") | ||||
|     custom_fields = AgentCustomFieldSerializer(many=True, read_only=True) | ||||
|     patches_last_installed = serializers.ReadOnlyField() | ||||
|     last_seen = serializers.ReadOnlyField() | ||||
|  | ||||
|     def get_all_timezones(self, obj): | ||||
|         return pytz.all_timezones | ||||
|  | ||||
|     class Meta: | ||||
|         model = Agent | ||||
|         exclude = [ | ||||
|             "last_seen", | ||||
|         ] | ||||
|         exclude = ["id"] | ||||
|  | ||||
|  | ||||
| class AgentTableSerializer(serializers.ModelSerializer): | ||||
|     patches_pending = serializers.ReadOnlyField(source="has_patches_pending") | ||||
|     pending_actions = serializers.SerializerMethodField() | ||||
|     status = serializers.ReadOnlyField() | ||||
|     checks = serializers.ReadOnlyField() | ||||
|     last_seen = serializers.SerializerMethodField() | ||||
| @@ -44,9 +57,20 @@ class AgentTableSerializer(serializers.ModelSerializer): | ||||
|     site_name = serializers.ReadOnlyField(source="site.name") | ||||
|     logged_username = serializers.SerializerMethodField() | ||||
|     italic = serializers.SerializerMethodField() | ||||
|     policy = serializers.ReadOnlyField(source="policy.id") | ||||
|     alert_template = serializers.SerializerMethodField() | ||||
|  | ||||
|     def get_pending_actions(self, obj): | ||||
|         return obj.pendingactions.filter(status="pending").count() | ||||
|     def get_alert_template(self, obj): | ||||
|  | ||||
|         if not obj.alert_template: | ||||
|             return None | ||||
|         else: | ||||
|             return { | ||||
|                 "name": obj.alert_template.name, | ||||
|                 "always_email": obj.alert_template.agent_always_email, | ||||
|                 "always_text": obj.alert_template.agent_always_text, | ||||
|                 "always_alert": obj.alert_template.agent_always_alert, | ||||
|             } | ||||
|  | ||||
|     def get_last_seen(self, obj) -> str: | ||||
|         if obj.time_zone is not None: | ||||
| @@ -54,7 +78,7 @@ class AgentTableSerializer(serializers.ModelSerializer): | ||||
|         else: | ||||
|             agent_tz = self.context["default_tz"] | ||||
|  | ||||
|         return obj.last_seen.astimezone(agent_tz).strftime("%m %d %Y %H:%M:%S") | ||||
|         return obj.last_seen.astimezone(agent_tz).strftime("%m %d %Y %H:%M") | ||||
|  | ||||
|     def get_logged_username(self, obj) -> str: | ||||
|         if obj.logged_in_username == "None" and obj.status == "online": | ||||
| @@ -70,63 +94,33 @@ class AgentTableSerializer(serializers.ModelSerializer): | ||||
|     class Meta: | ||||
|         model = Agent | ||||
|         fields = [ | ||||
|             "id", | ||||
|             "hostname", | ||||
|             "agent_id", | ||||
|             "alert_template", | ||||
|             "hostname", | ||||
|             "site_name", | ||||
|             "client_name", | ||||
|             "monitoring_type", | ||||
|             "description", | ||||
|             "needs_reboot", | ||||
|             "patches_pending", | ||||
|             "pending_actions", | ||||
|             "has_patches_pending", | ||||
|             "pending_actions_count", | ||||
|             "status", | ||||
|             "overdue_text_alert", | ||||
|             "overdue_email_alert", | ||||
|             "overdue_dashboard_alert", | ||||
|             "last_seen", | ||||
|             "boot_time", | ||||
|             "checks", | ||||
|             "maintenance_mode", | ||||
|             "logged_username", | ||||
|             "italic", | ||||
|             "policy", | ||||
|             "block_policy_inheritance", | ||||
|         ] | ||||
|         depth = 2 | ||||
|  | ||||
|  | ||||
| class AgentEditSerializer(serializers.ModelSerializer): | ||||
|     winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True) | ||||
|     all_timezones = serializers.SerializerMethodField() | ||||
|     client = ClientSerializer(read_only=True) | ||||
|  | ||||
|     def get_all_timezones(self, obj): | ||||
|         return pytz.all_timezones | ||||
|  | ||||
|     class Meta: | ||||
|         model = Agent | ||||
|         fields = [ | ||||
|             "id", | ||||
|             "hostname", | ||||
|             "client", | ||||
|             "site", | ||||
|             "monitoring_type", | ||||
|             "description", | ||||
|             "time_zone", | ||||
|             "timezone", | ||||
|             "check_interval", | ||||
|             "overdue_time", | ||||
|             "overdue_text_alert", | ||||
|             "overdue_email_alert", | ||||
|             "all_timezones", | ||||
|             "winupdatepolicy", | ||||
|         ] | ||||
|  | ||||
|  | ||||
| class WinAgentSerializer(serializers.ModelSerializer): | ||||
|     # for the windows agent | ||||
|     patches_pending = serializers.ReadOnlyField(source="has_patches_pending") | ||||
|     winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True) | ||||
|     status = serializers.ReadOnlyField() | ||||
|  | ||||
|     class Meta: | ||||
|         model = Agent | ||||
|         fields = "__all__" | ||||
| @@ -139,24 +133,38 @@ class AgentHostnameSerializer(serializers.ModelSerializer): | ||||
|     class Meta: | ||||
|         model = Agent | ||||
|         fields = ( | ||||
|             "id", | ||||
|             "hostname", | ||||
|             "pk", | ||||
|             "agent_id", | ||||
|             "client", | ||||
|             "site", | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class NoteSerializer(serializers.ModelSerializer): | ||||
| class AgentNoteSerializer(serializers.ModelSerializer): | ||||
|     username = serializers.ReadOnlyField(source="user.username") | ||||
|     agent_id = serializers.ReadOnlyField(source="agent.agent_id") | ||||
|  | ||||
|     class Meta: | ||||
|         model = Note | ||||
|         fields = "__all__" | ||||
|         fields = ("pk", "entry_time", "agent", "user", "note", "username", "agent_id") | ||||
|         extra_kwargs = {"agent": {"write_only": True}, "user": {"write_only": True}} | ||||
|  | ||||
|  | ||||
| class NotesSerializer(serializers.ModelSerializer): | ||||
|     notes = NoteSerializer(many=True, read_only=True) | ||||
| class AgentHistorySerializer(serializers.ModelSerializer): | ||||
|     time = serializers.SerializerMethodField(read_only=True) | ||||
|     script_name = serializers.ReadOnlyField(source="script.name") | ||||
|  | ||||
|     class Meta: | ||||
|         model = AgentHistory | ||||
|         fields = "__all__" | ||||
|  | ||||
|     def get_time(self, history): | ||||
|         tz = self.context["default_tz"] | ||||
|         return history.time.astimezone(tz).strftime("%m %d %Y %H:%M:%S") | ||||
|  | ||||
|  | ||||
| class AgentAuditSerializer(serializers.ModelSerializer): | ||||
|     class Meta: | ||||
|         model = Agent | ||||
|         fields = ["hostname", "pk", "notes"] | ||||
|         exclude = ["disks", "services", "wmi_detail"] | ||||
|   | ||||
| @@ -1,120 +1,87 @@ | ||||
| import asyncio | ||||
| from loguru import logger | ||||
| from time import sleep | ||||
| import datetime as dt | ||||
| import random | ||||
| import requests | ||||
| from packaging import version as pyver | ||||
| from typing import List | ||||
| from time import sleep | ||||
| from typing import Union | ||||
|  | ||||
| from django.conf import settings | ||||
| from scripts.models import Script | ||||
|  | ||||
| from tacticalrmm.celery import app | ||||
| from agents.models import Agent, AgentOutage | ||||
| from core.models import CoreSettings | ||||
| from logs.models import PendingAction | ||||
| 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 scripts.models import Script | ||||
| from tacticalrmm.celery import app | ||||
|  | ||||
| logger.configure(**settings.LOG_CONFIG) | ||||
| from agents.models import Agent | ||||
| from agents.utils import get_winagent_url | ||||
|  | ||||
|  | ||||
| def _check_agent_service(pk: int) -> None: | ||||
|     agent = Agent.objects.get(pk=pk) | ||||
|     r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=2)) | ||||
|     if r == "pong": | ||||
|         logger.info( | ||||
|             f"Detected crashed tacticalagent service on {agent.hostname}, attempting recovery" | ||||
|         ) | ||||
|         data = {"func": "recover", "payload": {"mode": "tacagent"}} | ||||
|         asyncio.run(agent.nats_cmd(data, wait=False)) | ||||
| def agent_update(agent_id: str, force: bool = False) -> str: | ||||
|  | ||||
|     agent = Agent.objects.get(agent_id=agent_id) | ||||
|  | ||||
| def _check_in_full(pk: int) -> None: | ||||
|     agent = Agent.objects.get(pk=pk) | ||||
|     asyncio.run(agent.nats_cmd({"func": "checkinfull"}, wait=False)) | ||||
|     if pyver.parse(agent.version) <= pyver.parse("1.3.0"): | ||||
|         return "not supported" | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def check_in_task() -> None: | ||||
|     q = Agent.objects.only("pk", "version") | ||||
|     agents: List[int] = [ | ||||
|         i.pk for i in q if pyver.parse(i.version) == pyver.parse("1.1.12") | ||||
|     ] | ||||
|     chunks = (agents[i : i + 50] for i in range(0, len(agents), 50)) | ||||
|     for chunk in chunks: | ||||
|         for pk in chunk: | ||||
|             _check_in_full(pk) | ||||
|             sleep(0.1) | ||||
|         rand = random.randint(3, 7) | ||||
|         sleep(rand) | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def monitor_agents_task() -> None: | ||||
|     q = Agent.objects.all() | ||||
|     agents: List[int] = [i.pk for i in q if i.has_nats and i.status != "online"] | ||||
|     for agent in agents: | ||||
|         _check_agent_service(agent) | ||||
|  | ||||
|  | ||||
| def agent_update(pk: int) -> str: | ||||
|     agent = Agent.objects.get(pk=pk) | ||||
|     # skip if we can't determine the arch | ||||
|     if agent.arch is None: | ||||
|         logger.warning(f"Unable to determine arch on {agent.hostname}. Skipping.") | ||||
|         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 | ||||
|     url = agent.winagent_dl | ||||
|     inno = agent.win_inno_exe | ||||
|     url = get_winagent_url(agent.arch) | ||||
|  | ||||
|     if agent.has_nats: | ||||
|         if pyver.parse(agent.version) <= pyver.parse("1.1.11"): | ||||
|             if agent.pendingactions.filter( | ||||
|     if not force: | ||||
|         if agent.pendingactions.filter( | ||||
|             action_type="agentupdate", status="pending" | ||||
|         ).exists(): | ||||
|             agent.pendingactions.filter( | ||||
|                 action_type="agentupdate", status="pending" | ||||
|             ).exists(): | ||||
|                 action = agent.pendingactions.filter( | ||||
|                     action_type="agentupdate", status="pending" | ||||
|                 ).last() | ||||
|                 if pyver.parse(action.details["version"]) < pyver.parse(version): | ||||
|                     action.delete() | ||||
|                 else: | ||||
|                     return "pending" | ||||
|             ).delete() | ||||
|  | ||||
|             PendingAction.objects.create( | ||||
|                 agent=agent, | ||||
|                 action_type="agentupdate", | ||||
|                 details={ | ||||
|                     "url": url, | ||||
|                     "version": version, | ||||
|                     "inno": inno, | ||||
|                 }, | ||||
|             ) | ||||
|         else: | ||||
|             nats_data = { | ||||
|                 "func": "agentupdate", | ||||
|                 "payload": { | ||||
|                     "url": url, | ||||
|                     "version": version, | ||||
|                     "inno": inno, | ||||
|                 }, | ||||
|             } | ||||
|             asyncio.run(agent.nats_cmd(nats_data, wait=False)) | ||||
|         PendingAction.objects.create( | ||||
|             agent=agent, | ||||
|             action_type="agentupdate", | ||||
|             details={ | ||||
|                 "url": url, | ||||
|                 "version": version, | ||||
|                 "inno": inno, | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
|         return "created" | ||||
|  | ||||
|     return "not supported" | ||||
|     nats_data = { | ||||
|         "func": "agentupdate", | ||||
|         "payload": { | ||||
|             "url": url, | ||||
|             "version": version, | ||||
|             "inno": inno, | ||||
|         }, | ||||
|     } | ||||
|     asyncio.run(agent.nats_cmd(nats_data, wait=False)) | ||||
|     return "created" | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def send_agent_update_task(pks: List[int], version: str) -> None: | ||||
|     q = Agent.objects.filter(pk__in=pks) | ||||
|     agents: List[int] = [ | ||||
|         i.pk for i in q if pyver.parse(i.version) < pyver.parse(version) | ||||
|     ] | ||||
|     chunks = (agents[i : i + 30] for i in range(0, len(agents), 30)) | ||||
| def force_code_sign(agent_ids: list[str]) -> None: | ||||
|     chunks = (agent_ids[i : i + 50] for i in range(0, len(agent_ids), 50)) | ||||
|     for chunk in chunks: | ||||
|         for pk in chunk: | ||||
|             agent_update(pk) | ||||
|         for agent_id in chunk: | ||||
|             agent_update(agent_id=agent_id, force=True) | ||||
|             sleep(0.05) | ||||
|         sleep(4) | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def send_agent_update_task(agent_ids: list[str]) -> None: | ||||
|     chunks = (agent_ids[i : i + 50] for i in range(0, len(agent_ids), 50)) | ||||
|     for chunk in chunks: | ||||
|         for agent_id in chunk: | ||||
|             agent_update(agent_id) | ||||
|             sleep(0.05) | ||||
|         sleep(4) | ||||
|  | ||||
| @@ -122,243 +89,143 @@ def send_agent_update_task(pks: List[int], version: str) -> None: | ||||
| @app.task | ||||
| def auto_self_agent_update_task() -> None: | ||||
|     core = CoreSettings.objects.first() | ||||
|     if not core.agent_auto_update: | ||||
|     if not core.agent_auto_update:  # type:ignore | ||||
|         return | ||||
|  | ||||
|     q = Agent.objects.only("pk", "version") | ||||
|     pks: List[int] = [ | ||||
|         i.pk | ||||
|     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 = (pks[i : i + 30] for i in range(0, len(pks), 30)) | ||||
|     chunks = (agent_ids[i : i + 30] for i in range(0, len(agent_ids), 30)) | ||||
|     for chunk in chunks: | ||||
|         for pk in chunk: | ||||
|             agent_update(pk) | ||||
|         for agent_id in chunk: | ||||
|             agent_update(agent_id) | ||||
|             sleep(0.05) | ||||
|         sleep(4) | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def get_wmi_task(): | ||||
|     agents = Agent.objects.all() | ||||
|     online = [ | ||||
|         i | ||||
|         for i in agents | ||||
|         if pyver.parse(i.version) >= pyver.parse("1.2.0") and i.status == "online" | ||||
|     ] | ||||
|     chunks = (online[i : i + 50] for i in range(0, len(online), 50)) | ||||
|     for chunk in chunks: | ||||
|         for agent in chunk: | ||||
|             asyncio.run(agent.nats_cmd({"func": "wmi"}, wait=False)) | ||||
|             sleep(0.1) | ||||
|         rand = random.randint(3, 7) | ||||
|         sleep(rand) | ||||
| def agent_outage_email_task(pk: int, alert_interval: Union[float, None] = None) -> str: | ||||
|     from alerts.models import Alert | ||||
|  | ||||
|     alert = Alert.objects.get(pk=pk) | ||||
|  | ||||
| @app.task | ||||
| def sync_sysinfo_task(): | ||||
|     agents = Agent.objects.all() | ||||
|     online = [ | ||||
|         i | ||||
|         for i in agents | ||||
|         if pyver.parse(i.version) >= pyver.parse("1.1.3") | ||||
|         and pyver.parse(i.version) <= pyver.parse("1.1.12") | ||||
|         and i.status == "online" | ||||
|     ] | ||||
|  | ||||
|     chunks = (online[i : i + 50] for i in range(0, len(online), 50)) | ||||
|     for chunk in chunks: | ||||
|         for agent in chunk: | ||||
|             asyncio.run(agent.nats_cmd({"func": "sync"}, wait=False)) | ||||
|             sleep(0.1) | ||||
|         rand = random.randint(3, 7) | ||||
|         sleep(rand) | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def sync_salt_modules_task(pk): | ||||
|     agent = Agent.objects.get(pk=pk) | ||||
|     r = agent.salt_api_cmd(timeout=35, func="saltutil.sync_modules") | ||||
|     # successful sync if new/charnged files: {'return': [{'MINION-15': ['modules.get_eventlog', 'modules.win_agent', 'etc...']}]} | ||||
|     # successful sync with no new/changed files: {'return': [{'MINION-15': []}]} | ||||
|     if r == "timeout" or r == "error": | ||||
|         return f"Unable to sync modules {agent.salt_id}" | ||||
|  | ||||
|     return f"Successfully synced salt modules on {agent.hostname}" | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def batch_sync_modules_task(): | ||||
|     # sync modules, split into chunks of 50 agents to not overload salt | ||||
|     agents = Agent.objects.all() | ||||
|     online = [i.salt_id for i in agents] | ||||
|     chunks = (online[i : i + 50] for i in range(0, len(online), 50)) | ||||
|     for chunk in chunks: | ||||
|         Agent.salt_batch_async(minions=chunk, func="saltutil.sync_modules") | ||||
|         sleep(10) | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def uninstall_agent_task(salt_id, has_nats): | ||||
|     attempts = 0 | ||||
|     error = False | ||||
|  | ||||
|     if not has_nats: | ||||
|         while 1: | ||||
|             try: | ||||
|  | ||||
|                 r = requests.post( | ||||
|                     f"http://{settings.SALT_HOST}:8123/run", | ||||
|                     json=[ | ||||
|                         { | ||||
|                             "client": "local", | ||||
|                             "tgt": salt_id, | ||||
|                             "fun": "win_agent.uninstall_agent", | ||||
|                             "timeout": 8, | ||||
|                             "username": settings.SALT_USERNAME, | ||||
|                             "password": settings.SALT_PASSWORD, | ||||
|                             "eauth": "pam", | ||||
|                         } | ||||
|                     ], | ||||
|                     timeout=10, | ||||
|                 ) | ||||
|                 ret = r.json()["return"][0][salt_id] | ||||
|             except Exception: | ||||
|                 attempts += 1 | ||||
|             else: | ||||
|                 if ret != "ok": | ||||
|                     attempts += 1 | ||||
|                 else: | ||||
|                     attempts = 0 | ||||
|  | ||||
|             if attempts >= 10: | ||||
|                 error = True | ||||
|                 break | ||||
|             elif attempts == 0: | ||||
|                 break | ||||
|  | ||||
|     if error: | ||||
|         logger.error(f"{salt_id} uninstall failed") | ||||
|     if not alert.email_sent: | ||||
|         sleep(random.randint(1, 15)) | ||||
|         alert.agent.send_outage_email() | ||||
|         alert.email_sent = djangotime.now() | ||||
|         alert.save(update_fields=["email_sent"]) | ||||
|     else: | ||||
|         logger.info(f"{salt_id} was successfully uninstalled") | ||||
|  | ||||
|     try: | ||||
|         r = requests.post( | ||||
|             f"http://{settings.SALT_HOST}:8123/run", | ||||
|             json=[ | ||||
|                 { | ||||
|                     "client": "wheel", | ||||
|                     "fun": "key.delete", | ||||
|                     "match": salt_id, | ||||
|                     "username": settings.SALT_USERNAME, | ||||
|                     "password": settings.SALT_PASSWORD, | ||||
|                     "eauth": "pam", | ||||
|                 } | ||||
|             ], | ||||
|             timeout=30, | ||||
|         ) | ||||
|     except Exception: | ||||
|         logger.error(f"{salt_id} unable to remove salt-key") | ||||
|         if alert_interval: | ||||
|             # 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, 10)) | ||||
|                 alert.agent.send_outage_email() | ||||
|                 alert.email_sent = djangotime.now() | ||||
|                 alert.save(update_fields=["email_sent"]) | ||||
|  | ||||
|     return "ok" | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def agent_outage_email_task(pk): | ||||
| def agent_recovery_email_task(pk: int) -> str: | ||||
|     from alerts.models import Alert | ||||
|  | ||||
|     sleep(random.randint(1, 15)) | ||||
|     outage = AgentOutage.objects.get(pk=pk) | ||||
|     outage.send_outage_email() | ||||
|     outage.outage_email_sent = True | ||||
|     outage.save(update_fields=["outage_email_sent"]) | ||||
|     alert = Alert.objects.get(pk=pk) | ||||
|     alert.agent.send_recovery_email() | ||||
|     alert.resolved_email_sent = djangotime.now() | ||||
|     alert.save(update_fields=["resolved_email_sent"]) | ||||
|  | ||||
|     return "ok" | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def agent_recovery_email_task(pk): | ||||
|     sleep(random.randint(1, 15)) | ||||
|     outage = AgentOutage.objects.get(pk=pk) | ||||
|     outage.send_recovery_email() | ||||
|     outage.recovery_email_sent = True | ||||
|     outage.save(update_fields=["recovery_email_sent"]) | ||||
| def agent_outage_sms_task(pk: int, alert_interval: Union[float, None] = None) -> str: | ||||
|     from alerts.models import Alert | ||||
|  | ||||
|     alert = Alert.objects.get(pk=pk) | ||||
|  | ||||
|     if not alert.sms_sent: | ||||
|         sleep(random.randint(1, 15)) | ||||
|         alert.agent.send_outage_sms() | ||||
|         alert.sms_sent = djangotime.now() | ||||
|         alert.save(update_fields=["sms_sent"]) | ||||
|     else: | ||||
|         if alert_interval: | ||||
|             # send an sms only if the last sms sent is older than alert interval | ||||
|             delta = djangotime.now() - dt.timedelta(days=alert_interval) | ||||
|             if alert.sms_sent < delta: | ||||
|                 sleep(random.randint(1, 10)) | ||||
|                 alert.agent.send_outage_sms() | ||||
|                 alert.sms_sent = djangotime.now() | ||||
|                 alert.save(update_fields=["sms_sent"]) | ||||
|  | ||||
|     return "ok" | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def agent_outage_sms_task(pk): | ||||
| def agent_recovery_sms_task(pk: int) -> str: | ||||
|     from alerts.models import Alert | ||||
|  | ||||
|     sleep(random.randint(1, 3)) | ||||
|     outage = AgentOutage.objects.get(pk=pk) | ||||
|     outage.send_outage_sms() | ||||
|     outage.outage_sms_sent = True | ||||
|     outage.save(update_fields=["outage_sms_sent"]) | ||||
|     alert = Alert.objects.get(pk=pk) | ||||
|     alert.agent.send_recovery_sms() | ||||
|     alert.resolved_sms_sent = djangotime.now() | ||||
|     alert.save(update_fields=["resolved_sms_sent"]) | ||||
|  | ||||
|     return "ok" | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def agent_recovery_sms_task(pk): | ||||
|     sleep(random.randint(1, 3)) | ||||
|     outage = AgentOutage.objects.get(pk=pk) | ||||
|     outage.send_recovery_sms() | ||||
|     outage.recovery_sms_sent = True | ||||
|     outage.save(update_fields=["recovery_sms_sent"]) | ||||
| def agent_outages_task() -> None: | ||||
|     from alerts.models import Alert | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def agent_outages_task(): | ||||
|     agents = Agent.objects.only( | ||||
|         "pk", "last_seen", "overdue_time", "overdue_email_alert", "overdue_text_alert" | ||||
|         "pk", | ||||
|         "agent_id", | ||||
|         "last_seen", | ||||
|         "offline_time", | ||||
|         "overdue_time", | ||||
|         "overdue_email_alert", | ||||
|         "overdue_text_alert", | ||||
|         "overdue_dashboard_alert", | ||||
|     ) | ||||
|  | ||||
|     for agent in agents: | ||||
|         if agent.overdue_email_alert or agent.overdue_text_alert: | ||||
|             if agent.status == "overdue": | ||||
|                 outages = AgentOutage.objects.filter(agent=agent) | ||||
|                 if outages and outages.last().is_active: | ||||
|                     continue | ||||
|  | ||||
|                 outage = AgentOutage(agent=agent) | ||||
|                 outage.save() | ||||
|  | ||||
|                 # add a null check history to allow gaps in graph | ||||
|                 for check in agent.agentchecks.all(): | ||||
|                     check.add_check_history(None) | ||||
|  | ||||
|                 if agent.overdue_email_alert and not agent.maintenance_mode: | ||||
|                     agent_outage_email_task.delay(pk=outage.pk) | ||||
|  | ||||
|                 if agent.overdue_text_alert and not agent.maintenance_mode: | ||||
|                     agent_outage_sms_task.delay(pk=outage.pk) | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def install_salt_task(pk: int) -> None: | ||||
|     sleep(20) | ||||
|     agent = Agent.objects.get(pk=pk) | ||||
|     asyncio.run(agent.nats_cmd({"func": "installsalt"}, wait=False)) | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def handle_agent_recovery_task(pk: int) -> None: | ||||
|     sleep(10) | ||||
|     from agents.models import RecoveryAction | ||||
|  | ||||
|     action = RecoveryAction.objects.get(pk=pk) | ||||
|     if action.mode == "command": | ||||
|         data = {"func": "recoverycmd", "recoverycommand": action.command} | ||||
|     else: | ||||
|         data = {"func": "recover", "payload": {"mode": action.mode}} | ||||
|  | ||||
|     asyncio.run(action.agent.nats_cmd(data, wait=False)) | ||||
|         if agent.status == "overdue": | ||||
|             Alert.handle_alert_failure(agent) | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def run_script_email_results_task( | ||||
|     agentpk: int, scriptpk: int, nats_timeout: int, nats_data: dict, emails: List[str] | ||||
|     agentpk: int, | ||||
|     scriptpk: int, | ||||
|     nats_timeout: int, | ||||
|     emails: list[str], | ||||
|     args: list[str] = [], | ||||
|     history_pk: int = 0, | ||||
| ): | ||||
|     agent = Agent.objects.get(pk=agentpk) | ||||
|     script = Script.objects.get(pk=scriptpk) | ||||
|     nats_data["func"] = "runscriptfull" | ||||
|     r = asyncio.run(agent.nats_cmd(nats_data, timeout=nats_timeout)) | ||||
|     r = agent.run_script( | ||||
|         scriptpk=script.pk, | ||||
|         args=args, | ||||
|         full=True, | ||||
|         timeout=nats_timeout, | ||||
|         wait=True, | ||||
|         history_pk=history_pk, | ||||
|     ) | ||||
|     if r == "timeout": | ||||
|         logger.error(f"{agent.hostname} timed out running script.") | ||||
|         DebugLog.error( | ||||
|             agent=agent, | ||||
|             log_type="scripting", | ||||
|             message=f"{agent.hostname}({agent.pk}) timed out running script.", | ||||
|         ) | ||||
|         return | ||||
|  | ||||
|     CORE = CoreSettings.objects.first() | ||||
| @@ -374,25 +241,68 @@ def run_script_email_results_task( | ||||
|  | ||||
|     msg = EmailMessage() | ||||
|     msg["Subject"] = subject | ||||
|     msg["From"] = CORE.smtp_from_email | ||||
|     msg["From"] = CORE.smtp_from_email  # type:ignore | ||||
|  | ||||
|     if emails: | ||||
|         msg["To"] = ", ".join(emails) | ||||
|     else: | ||||
|         msg["To"] = ", ".join(CORE.email_alert_recipients) | ||||
|         msg["To"] = ", ".join(CORE.email_alert_recipients)  # type:ignore | ||||
|  | ||||
|     msg.set_content(body) | ||||
|  | ||||
|     try: | ||||
|         with smtplib.SMTP(CORE.smtp_host, CORE.smtp_port, timeout=20) as server: | ||||
|             if CORE.smtp_requires_auth: | ||||
|         with smtplib.SMTP( | ||||
|             CORE.smtp_host, CORE.smtp_port, timeout=20  # type:ignore | ||||
|         ) as server:  # type:ignore | ||||
|             if CORE.smtp_requires_auth:  # type:ignore | ||||
|                 server.ehlo() | ||||
|                 server.starttls() | ||||
|                 server.login(CORE.smtp_host_user, CORE.smtp_host_password) | ||||
|                 server.login( | ||||
|                     CORE.smtp_host_user, CORE.smtp_host_password  # type:ignore | ||||
|                 )  # type:ignore | ||||
|                 server.send_message(msg) | ||||
|                 server.quit() | ||||
|             else: | ||||
|                 server.send_message(msg) | ||||
|                 server.quit() | ||||
|     except Exception as e: | ||||
|         logger.error(e) | ||||
|         DebugLog.error(message=str(e)) | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def clear_faults_task(older_than_days: int) -> None: | ||||
|     # https://github.com/wh1te909/tacticalrmm/issues/484 | ||||
|     agents = Agent.objects.exclude(last_seen__isnull=True).filter( | ||||
|         last_seen__lt=djangotime.now() - djangotime.timedelta(days=older_than_days) | ||||
|     ) | ||||
|     for agent in agents: | ||||
|         if agent.agentchecks.exists(): | ||||
|             for check in agent.agentchecks.all(): | ||||
|                 # reset check status | ||||
|                 check.status = "passing" | ||||
|                 check.save(update_fields=["status"]) | ||||
|                 if check.alert.filter(resolved=False).exists(): | ||||
|                     check.alert.get(resolved=False).resolve() | ||||
|  | ||||
|         # reset overdue alerts | ||||
|         agent.overdue_email_alert = False | ||||
|         agent.overdue_text_alert = False | ||||
|         agent.overdue_dashboard_alert = False | ||||
|         agent.save( | ||||
|             update_fields=[ | ||||
|                 "overdue_email_alert", | ||||
|                 "overdue_text_alert", | ||||
|                 "overdue_dashboard_alert", | ||||
|             ] | ||||
|         ) | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def prune_agent_history(older_than_days: int) -> str: | ||||
|     from .models import AgentHistory | ||||
|  | ||||
|     AgentHistory.objects.filter( | ||||
|         time__lt=djangotime.now() - djangotime.timedelta(days=older_than_days) | ||||
|     ).delete() | ||||
|  | ||||
|     return "ok" | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,34 +1,44 @@ | ||||
| from django.urls import path | ||||
|  | ||||
| from . import views | ||||
| from checks.views import GetAddChecks | ||||
| from autotasks.views import GetAddAutoTasks | ||||
| from logs.views import PendingActions | ||||
|  | ||||
| urlpatterns = [ | ||||
|     path("listagents/", views.AgentsTableList.as_view()), | ||||
|     path("listagentsnodetail/", views.list_agents_no_detail), | ||||
|     path("<int:pk>/agenteditdetails/", views.agent_edit_details), | ||||
|     path("byclient/<int:clientpk>/", views.by_client), | ||||
|     path("bysite/<int:sitepk>/", views.by_site), | ||||
|     path("overdueaction/", views.overdue_action), | ||||
|     path("sendrawcmd/", views.send_raw_cmd), | ||||
|     path("<pk>/agentdetail/", views.agent_detail), | ||||
|     path("<int:pk>/meshcentral/", views.meshcentral), | ||||
|     # agent views | ||||
|     path("", views.GetAgents.as_view()), | ||||
|     path("<agent:agent_id>/", views.GetUpdateDeleteAgent.as_view()), | ||||
|     path("<agent:agent_id>/cmd/", views.send_raw_cmd), | ||||
|     path("<agent:agent_id>/runscript/", views.run_script), | ||||
|     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>/ping/", views.ping), | ||||
|     # alias for checks get view | ||||
|     path("<agent:agent_id>/checks/", GetAddChecks.as_view()), | ||||
|     # alias for autotasks get view | ||||
|     path("<agent:agent_id>/tasks/", GetAddAutoTasks.as_view()), | ||||
|     # alias for pending actions get view | ||||
|     path("<agent:agent_id>/pendingactions/", PendingActions.as_view()), | ||||
|     # agent remote background | ||||
|     path("<agent:agent_id>/meshcentral/", views.AgentMeshCentral.as_view()), | ||||
|     path("<agent:agent_id>/meshcentral/recover/", views.AgentMeshCentral.as_view()), | ||||
|     path("<agent:agent_id>/processes/", views.AgentProcesses.as_view()), | ||||
|     path("<agent:agent_id>/processes/<int:pid>/", views.AgentProcesses.as_view()), | ||||
|     path("<agent:agent_id>/eventlog/<str:logtype>/<int:days>/", views.get_event_log), | ||||
|     # agent history | ||||
|     path("history/", views.AgentHistoryView.as_view()), | ||||
|     path("<agent:agent_id>/history/", views.AgentHistoryView.as_view()), | ||||
|     # agent notes | ||||
|     path("notes/", views.GetAddNotes.as_view()), | ||||
|     path("notes/<int:pk>/", views.GetEditDeleteNote.as_view()), | ||||
|     path("<agent:agent_id>/notes/", views.GetAddNotes.as_view()), | ||||
|     # bulk actions | ||||
|     path("maintenance/bulk/", views.agent_maintenance), | ||||
|     path("actions/bulk/", views.bulk), | ||||
|     path("versions/", views.get_agent_versions), | ||||
|     path("update/", views.update_agents), | ||||
|     path("installer/", views.install_agent), | ||||
|     path("<str:arch>/getmeshexe/", views.get_mesh_exe), | ||||
|     path("uninstall/", views.uninstall), | ||||
|     path("editagent/", views.edit_agent), | ||||
|     path("<pk>/geteventlog/<logtype>/<days>/", views.get_event_log), | ||||
|     path("getagentversions/", views.get_agent_versions), | ||||
|     path("updateagents/", views.update_agents), | ||||
|     path("<pk>/getprocs/", views.get_processes), | ||||
|     path("<pk>/<pid>/killproc/", views.kill_proc), | ||||
|     path("reboot/", views.Reboot.as_view()), | ||||
|     path("installagent/", views.install_agent), | ||||
|     path("<int:pk>/ping/", views.ping), | ||||
|     path("recover/", views.recover), | ||||
|     path("runscript/", views.run_script), | ||||
|     path("<int:pk>/recovermesh/", views.recover_mesh), | ||||
|     path("<int:pk>/notes/", views.GetAddNotes.as_view()), | ||||
|     path("<int:pk>/note/", views.GetEditDeleteNote.as_view()), | ||||
|     path("bulk/", views.bulk), | ||||
|     path("agent_counts/", views.agent_counts), | ||||
|     path("maintenance/", views.agent_maintenance), | ||||
|     path("<int:pk>/wmi/", views.WMI.as_view()), | ||||
| ] | ||||
|   | ||||
							
								
								
									
										40
									
								
								api/tacticalrmm/agents/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								api/tacticalrmm/agents/utils.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| import random | ||||
| import urllib.parse | ||||
| import requests | ||||
|  | ||||
| from django.conf import settings | ||||
| from core.models import CodeSignToken | ||||
|  | ||||
|  | ||||
| def get_exegen_url() -> str: | ||||
|     urls: list[str] = settings.EXE_GEN_URLS | ||||
|     for url in urls: | ||||
|         try: | ||||
|             r = requests.get(url, timeout=10) | ||||
|         except: | ||||
|             continue | ||||
|  | ||||
|         if r.status_code == 200: | ||||
|             return url | ||||
|  | ||||
|     return random.choice(urls) | ||||
|  | ||||
|  | ||||
| def get_winagent_url(arch: str) -> str: | ||||
|  | ||||
|     dl_url = settings.DL_32 if arch == "32" else settings.DL_64 | ||||
|  | ||||
|     try: | ||||
|         t: CodeSignToken = CodeSignToken.objects.first()  # type: ignore | ||||
|         if t.is_valid: | ||||
|             base_url = get_exegen_url() + "/api/v1/winagents/?" | ||||
|             params = { | ||||
|                 "version": settings.LATEST_AGENT_VER, | ||||
|                 "arch": arch, | ||||
|                 "token": t.token, | ||||
|             } | ||||
|             dl_url = base_url + urllib.parse.urlencode(params) | ||||
|     except: | ||||
|         pass | ||||
|  | ||||
|     return dl_url | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,6 +1,6 @@ | ||||
| from django.contrib import admin | ||||
|  | ||||
| from .models import Alert | ||||
|  | ||||
| from .models import Alert, AlertTemplate | ||||
|  | ||||
| admin.site.register(Alert) | ||||
| admin.site.register(AlertTemplate) | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| # Generated by Django 3.1 on 2020-08-15 15:31 | ||||
|  | ||||
| from django.db import migrations, models | ||||
| import django.db.models.deletion | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
| @@ -42,4 +42,4 @@ class Migration(migrations.Migration): | ||||
|                 ), | ||||
|             ], | ||||
|         ), | ||||
|     ] | ||||
|     ] | ||||
| @@ -27,4 +27,4 @@ class Migration(migrations.Migration): | ||||
|                 max_length=100, | ||||
|             ), | ||||
|         ), | ||||
|     ] | ||||
|     ] | ||||
| @@ -1,7 +1,7 @@ | ||||
| # Generated by Django 3.1.2 on 2020-10-21 18:15 | ||||
|  | ||||
| from django.db import migrations, models | ||||
| import django.db.models.deletion | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
| @@ -28,4 +28,4 @@ class Migration(migrations.Migration): | ||||
|             name="alert_time", | ||||
|             field=models.DateTimeField(auto_now_add=True, null=True), | ||||
|         ), | ||||
|     ] | ||||
|     ] | ||||
							
								
								
									
										172
									
								
								api/tacticalrmm/alerts/migrations/0004_auto_20210212_1408.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								api/tacticalrmm/alerts/migrations/0004_auto_20210212_1408.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,172 @@ | ||||
| # Generated by Django 3.1.4 on 2021-02-12 14:08 | ||||
|  | ||||
| import django.contrib.postgres.fields | ||||
| import django.db.models.deletion | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('agents', '0029_delete_agentoutage'), | ||||
|         ('clients', '0008_auto_20201103_1430'), | ||||
|         ('autotasks', '0017_auto_20210210_1512'), | ||||
|         ('scripts', '0005_auto_20201207_1606'), | ||||
|         ('alerts', '0003_auto_20201021_1815'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='alert', | ||||
|             name='action_execution_time', | ||||
|             field=models.CharField(blank=True, max_length=100, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alert', | ||||
|             name='action_retcode', | ||||
|             field=models.IntegerField(blank=True, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alert', | ||||
|             name='action_run', | ||||
|             field=models.DateTimeField(blank=True, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alert', | ||||
|             name='action_stderr', | ||||
|             field=models.TextField(blank=True, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alert', | ||||
|             name='action_stdout', | ||||
|             field=models.TextField(blank=True, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alert', | ||||
|             name='action_timeout', | ||||
|             field=models.PositiveIntegerField(blank=True, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alert', | ||||
|             name='alert_type', | ||||
|             field=models.CharField(choices=[('availability', 'Availability'), ('check', 'Check'), ('task', 'Task'), ('custom', 'Custom')], default='availability', max_length=20), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alert', | ||||
|             name='assigned_task', | ||||
|             field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='alert', to='autotasks.automatedtask'), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alert', | ||||
|             name='email_sent', | ||||
|             field=models.DateTimeField(blank=True, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alert', | ||||
|             name='hidden', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alert', | ||||
|             name='resolved_action_execution_time', | ||||
|             field=models.CharField(blank=True, max_length=100, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alert', | ||||
|             name='resolved_action_retcode', | ||||
|             field=models.IntegerField(blank=True, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alert', | ||||
|             name='resolved_action_run', | ||||
|             field=models.DateTimeField(blank=True, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alert', | ||||
|             name='resolved_action_stderr', | ||||
|             field=models.TextField(blank=True, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alert', | ||||
|             name='resolved_action_stdout', | ||||
|             field=models.TextField(blank=True, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alert', | ||||
|             name='resolved_action_timeout', | ||||
|             field=models.PositiveIntegerField(blank=True, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alert', | ||||
|             name='resolved_email_sent', | ||||
|             field=models.DateTimeField(blank=True, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alert', | ||||
|             name='resolved_on', | ||||
|             field=models.DateTimeField(blank=True, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alert', | ||||
|             name='resolved_sms_sent', | ||||
|             field=models.DateTimeField(blank=True, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alert', | ||||
|             name='sms_sent', | ||||
|             field=models.DateTimeField(blank=True, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alert', | ||||
|             name='snoozed', | ||||
|             field=models.BooleanField(default=False), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='alert', | ||||
|             name='severity', | ||||
|             field=models.CharField(choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], default='info', max_length=30), | ||||
|         ), | ||||
|         migrations.CreateModel( | ||||
|             name='AlertTemplate', | ||||
|             fields=[ | ||||
|                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||
|                 ('name', models.CharField(max_length=100)), | ||||
|                 ('is_active', models.BooleanField(default=True)), | ||||
|                 ('action_args', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=255, null=True), blank=True, default=list, null=True, size=None)), | ||||
|                 ('resolved_action_args', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=255, null=True), blank=True, default=list, null=True, size=None)), | ||||
|                 ('email_recipients', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=100), blank=True, default=list, null=True, size=None)), | ||||
|                 ('text_recipients', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=100), blank=True, default=list, null=True, size=None)), | ||||
|                 ('email_from', models.EmailField(blank=True, max_length=254, null=True)), | ||||
|                 ('agent_email_on_resolved', models.BooleanField(blank=True, default=False, null=True)), | ||||
|                 ('agent_text_on_resolved', models.BooleanField(blank=True, default=False, null=True)), | ||||
|                 ('agent_include_desktops', models.BooleanField(blank=True, default=False, null=True)), | ||||
|                 ('agent_always_email', models.BooleanField(blank=True, default=False, null=True)), | ||||
|                 ('agent_always_text', models.BooleanField(blank=True, default=False, null=True)), | ||||
|                 ('agent_always_alert', models.BooleanField(blank=True, default=False, null=True)), | ||||
|                 ('agent_periodic_alert_days', models.PositiveIntegerField(blank=True, default=0, null=True)), | ||||
|                 ('check_email_alert_severity', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], max_length=25), blank=True, default=list, size=None)), | ||||
|                 ('check_text_alert_severity', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], max_length=25), blank=True, default=list, size=None)), | ||||
|                 ('check_dashboard_alert_severity', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], max_length=25), blank=True, default=list, size=None)), | ||||
|                 ('check_email_on_resolved', models.BooleanField(blank=True, default=False, null=True)), | ||||
|                 ('check_text_on_resolved', models.BooleanField(blank=True, default=False, null=True)), | ||||
|                 ('check_always_email', models.BooleanField(blank=True, default=False, null=True)), | ||||
|                 ('check_always_text', models.BooleanField(blank=True, default=False, null=True)), | ||||
|                 ('check_always_alert', models.BooleanField(blank=True, default=False, null=True)), | ||||
|                 ('check_periodic_alert_days', models.PositiveIntegerField(blank=True, default=0, null=True)), | ||||
|                 ('task_email_alert_severity', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], max_length=25), blank=True, default=list, size=None)), | ||||
|                 ('task_text_alert_severity', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], max_length=25), blank=True, default=list, size=None)), | ||||
|                 ('task_dashboard_alert_severity', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, choices=[('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], max_length=25), blank=True, default=list, size=None)), | ||||
|                 ('task_email_on_resolved', models.BooleanField(blank=True, default=False, null=True)), | ||||
|                 ('task_text_on_resolved', models.BooleanField(blank=True, default=False, null=True)), | ||||
|                 ('task_always_email', models.BooleanField(blank=True, default=False, null=True)), | ||||
|                 ('task_always_text', models.BooleanField(blank=True, default=False, null=True)), | ||||
|                 ('task_always_alert', models.BooleanField(blank=True, default=False, null=True)), | ||||
|                 ('task_periodic_alert_days', models.PositiveIntegerField(blank=True, default=0, null=True)), | ||||
|                 ('action', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='alert_template', to='scripts.script')), | ||||
|                 ('excluded_agents', models.ManyToManyField(blank=True, related_name='alert_exclusions', to='agents.Agent')), | ||||
|                 ('excluded_clients', models.ManyToManyField(blank=True, related_name='alert_exclusions', to='clients.Client')), | ||||
|                 ('excluded_sites', models.ManyToManyField(blank=True, related_name='alert_exclusions', to='clients.Site')), | ||||
|                 ('resolved_action', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='resolved_alert_template', to='scripts.script')), | ||||
|             ], | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										31
									
								
								api/tacticalrmm/alerts/migrations/0005_auto_20210212_1745.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								api/tacticalrmm/alerts/migrations/0005_auto_20210212_1745.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| # Generated by Django 3.1.4 on 2021-02-12 17:45 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('alerts', '0004_auto_20210212_1408'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.RemoveField( | ||||
|             model_name='alert', | ||||
|             name='action_timeout', | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='alert', | ||||
|             name='resolved_action_timeout', | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alerttemplate', | ||||
|             name='action_timeout', | ||||
|             field=models.PositiveIntegerField(default=15), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alerttemplate', | ||||
|             name='resolved_action_timeout', | ||||
|             field=models.PositiveIntegerField(default=15), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										72
									
								
								api/tacticalrmm/alerts/migrations/0006_auto_20210217_1736.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								api/tacticalrmm/alerts/migrations/0006_auto_20210217_1736.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | ||||
| # Generated by Django 3.1.6 on 2021-02-17 17:36 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('alerts', '0005_auto_20210212_1745'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.RemoveField( | ||||
|             model_name='alerttemplate', | ||||
|             name='agent_include_desktops', | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alerttemplate', | ||||
|             name='exclude_servers', | ||||
|             field=models.BooleanField(blank=True, default=False, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alerttemplate', | ||||
|             name='exclude_workstations', | ||||
|             field=models.BooleanField(blank=True, default=False, null=True), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='alerttemplate', | ||||
|             name='agent_always_alert', | ||||
|             field=models.BooleanField(blank=True, default=None, null=True), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='alerttemplate', | ||||
|             name='agent_always_email', | ||||
|             field=models.BooleanField(blank=True, default=None, null=True), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='alerttemplate', | ||||
|             name='agent_always_text', | ||||
|             field=models.BooleanField(blank=True, default=None, null=True), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='alerttemplate', | ||||
|             name='check_always_alert', | ||||
|             field=models.BooleanField(blank=True, default=None, null=True), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='alerttemplate', | ||||
|             name='check_always_email', | ||||
|             field=models.BooleanField(blank=True, default=None, null=True), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='alerttemplate', | ||||
|             name='check_always_text', | ||||
|             field=models.BooleanField(blank=True, default=None, null=True), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='alerttemplate', | ||||
|             name='task_always_alert', | ||||
|             field=models.BooleanField(blank=True, default=None, null=True), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='alerttemplate', | ||||
|             name='task_always_email', | ||||
|             field=models.BooleanField(blank=True, default=None, null=True), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='alerttemplate', | ||||
|             name='task_always_text', | ||||
|             field=models.BooleanField(blank=True, default=None, null=True), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										33
									
								
								api/tacticalrmm/alerts/migrations/0007_auto_20210721_0423.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								api/tacticalrmm/alerts/migrations/0007_auto_20210721_0423.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| # Generated by Django 3.2.1 on 2021-07-21 04:23 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('alerts', '0006_auto_20210217_1736'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='alerttemplate', | ||||
|             name='created_by', | ||||
|             field=models.CharField(blank=True, max_length=100, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alerttemplate', | ||||
|             name='created_time', | ||||
|             field=models.DateTimeField(auto_now_add=True, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alerttemplate', | ||||
|             name='modified_by', | ||||
|             field=models.CharField(blank=True, max_length=100, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alerttemplate', | ||||
|             name='modified_time', | ||||
|             field=models.DateTimeField(auto_now=True, null=True), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										28
									
								
								api/tacticalrmm/alerts/migrations/0008_auto_20210721_1757.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								api/tacticalrmm/alerts/migrations/0008_auto_20210721_1757.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| # Generated by Django 3.2.1 on 2021-07-21 17:57 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('alerts', '0007_auto_20210721_0423'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='alerttemplate', | ||||
|             name='agent_script_actions', | ||||
|             field=models.BooleanField(blank=True, default=None, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alerttemplate', | ||||
|             name='check_script_actions', | ||||
|             field=models.BooleanField(blank=True, default=None, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='alerttemplate', | ||||
|             name='task_script_actions', | ||||
|             field=models.BooleanField(blank=True, default=None, null=True), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										28
									
								
								api/tacticalrmm/alerts/migrations/0009_auto_20210721_1810.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								api/tacticalrmm/alerts/migrations/0009_auto_20210721_1810.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| # Generated by Django 3.2.1 on 2021-07-21 18:10 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('alerts', '0008_auto_20210721_1757'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name='alerttemplate', | ||||
|             name='agent_script_actions', | ||||
|             field=models.BooleanField(blank=True, default=True, null=True), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='alerttemplate', | ||||
|             name='check_script_actions', | ||||
|             field=models.BooleanField(blank=True, default=True, null=True), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='alerttemplate', | ||||
|             name='task_script_actions', | ||||
|             field=models.BooleanField(blank=True, default=True, null=True), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										23
									
								
								api/tacticalrmm/alerts/migrations/0010_auto_20210917_1954.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								api/tacticalrmm/alerts/migrations/0010_auto_20210917_1954.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| # Generated by Django 3.2.6 on 2021-09-17 19:54 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("alerts", "0009_auto_20210721_1810"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name="alerttemplate", | ||||
|             name="created_by", | ||||
|             field=models.CharField(blank=True, max_length=255, null=True), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name="alerttemplate", | ||||
|             name="modified_by", | ||||
|             field=models.CharField(blank=True, max_length=255, null=True), | ||||
|         ), | ||||
|     ] | ||||
| @@ -1,4 +1,20 @@ | ||||
| from __future__ import annotations | ||||
|  | ||||
| import re | ||||
| from typing import TYPE_CHECKING, Union | ||||
|  | ||||
| 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 tacticalrmm.models import PermissionQuerySet | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|     from agents.models import Agent | ||||
|     from autotasks.models import AutomatedTask | ||||
|     from checks.models import Check | ||||
|  | ||||
|  | ||||
| SEVERITY_CHOICES = [ | ||||
| @@ -7,8 +23,17 @@ SEVERITY_CHOICES = [ | ||||
|     ("error", "Error"), | ||||
| ] | ||||
|  | ||||
| ALERT_TYPE_CHOICES = [ | ||||
|     ("availability", "Availability"), | ||||
|     ("check", "Check"), | ||||
|     ("task", "Task"), | ||||
|     ("custom", "Custom"), | ||||
| ] | ||||
|  | ||||
|  | ||||
| class Alert(models.Model): | ||||
|     objects = PermissionQuerySet.as_manager() | ||||
|  | ||||
|     agent = models.ForeignKey( | ||||
|         "agents.Agent", | ||||
|         related_name="agent", | ||||
| @@ -23,21 +48,606 @@ class Alert(models.Model): | ||||
|         null=True, | ||||
|         blank=True, | ||||
|     ) | ||||
|     assigned_task = models.ForeignKey( | ||||
|         "autotasks.AutomatedTask", | ||||
|         related_name="alert", | ||||
|         on_delete=models.CASCADE, | ||||
|         null=True, | ||||
|         blank=True, | ||||
|     ) | ||||
|     alert_type = models.CharField( | ||||
|         max_length=20, choices=ALERT_TYPE_CHOICES, default="availability" | ||||
|     ) | ||||
|     message = models.TextField(null=True, blank=True) | ||||
|     alert_time = models.DateTimeField(auto_now_add=True, null=True) | ||||
|     alert_time = models.DateTimeField(auto_now_add=True, null=True, blank=True) | ||||
|     snoozed = models.BooleanField(default=False) | ||||
|     snooze_until = models.DateTimeField(null=True, blank=True) | ||||
|     resolved = models.BooleanField(default=False) | ||||
|     severity = models.CharField( | ||||
|         max_length=100, choices=SEVERITY_CHOICES, default="info" | ||||
|     resolved_on = models.DateTimeField(null=True, blank=True) | ||||
|     severity = models.CharField(max_length=30, choices=SEVERITY_CHOICES, default="info") | ||||
|     email_sent = models.DateTimeField(null=True, blank=True) | ||||
|     resolved_email_sent = models.DateTimeField(null=True, blank=True) | ||||
|     sms_sent = models.DateTimeField(null=True, blank=True) | ||||
|     resolved_sms_sent = models.DateTimeField(null=True, blank=True) | ||||
|     hidden = models.BooleanField(default=False) | ||||
|     action_run = models.DateTimeField(null=True, blank=True) | ||||
|     action_stdout = models.TextField(null=True, blank=True) | ||||
|     action_stderr = models.TextField(null=True, blank=True) | ||||
|     action_retcode = models.IntegerField(null=True, blank=True) | ||||
|     action_execution_time = models.CharField(max_length=100, null=True, blank=True) | ||||
|     resolved_action_run = models.DateTimeField(null=True, blank=True) | ||||
|     resolved_action_stdout = models.TextField(null=True, blank=True) | ||||
|     resolved_action_stderr = models.TextField(null=True, blank=True) | ||||
|     resolved_action_retcode = models.IntegerField(null=True, blank=True) | ||||
|     resolved_action_execution_time = models.CharField( | ||||
|         max_length=100, null=True, blank=True | ||||
|     ) | ||||
|  | ||||
|     def __str__(self): | ||||
|         return self.message | ||||
|  | ||||
|     @classmethod | ||||
|     def create_availability_alert(cls, agent): | ||||
|         pass | ||||
|     def resolve(self): | ||||
|         self.resolved = True | ||||
|         self.resolved_on = djangotime.now() | ||||
|         self.snoozed = False | ||||
|         self.snooze_until = None | ||||
|         self.save() | ||||
|  | ||||
|     @classmethod | ||||
|     def create_check_alert(cls, check): | ||||
|         pass | ||||
|     def create_or_return_availability_alert(cls, agent): | ||||
|         if not cls.objects.filter(agent=agent, resolved=False).exists(): | ||||
|             return cls.objects.create( | ||||
|                 agent=agent, | ||||
|                 alert_type="availability", | ||||
|                 severity="error", | ||||
|                 message=f"{agent.hostname} in {agent.client.name}\\{agent.site.name} is overdue.", | ||||
|                 hidden=True, | ||||
|             ) | ||||
|         else: | ||||
|             return cls.objects.get(agent=agent, resolved=False) | ||||
|  | ||||
|     @classmethod | ||||
|     def create_or_return_check_alert(cls, check): | ||||
|  | ||||
|         if not cls.objects.filter(assigned_check=check, resolved=False).exists(): | ||||
|             return cls.objects.create( | ||||
|                 assigned_check=check, | ||||
|                 alert_type="check", | ||||
|                 severity=check.alert_severity, | ||||
|                 message=f"{check.agent.hostname} has a {check.check_type} check: {check.readable_desc} that failed.", | ||||
|                 hidden=True, | ||||
|             ) | ||||
|         else: | ||||
|             return cls.objects.get(assigned_check=check, resolved=False) | ||||
|  | ||||
|     @classmethod | ||||
|     def create_or_return_task_alert(cls, task): | ||||
|  | ||||
|         if not cls.objects.filter(assigned_task=task, resolved=False).exists(): | ||||
|             return cls.objects.create( | ||||
|                 assigned_task=task, | ||||
|                 alert_type="task", | ||||
|                 severity=task.alert_severity, | ||||
|                 message=f"{task.agent.hostname} has task: {task.name} that failed.", | ||||
|                 hidden=True, | ||||
|             ) | ||||
|         else: | ||||
|             return cls.objects.get(assigned_task=task, resolved=False) | ||||
|  | ||||
|     @classmethod | ||||
|     def handle_alert_failure(cls, instance: Union[Agent, AutomatedTask, Check]) -> None: | ||||
|         from agents.models import Agent | ||||
|         from autotasks.models import AutomatedTask | ||||
|         from checks.models import Check | ||||
|  | ||||
|         # set variables | ||||
|         dashboard_severities = None | ||||
|         email_severities = None | ||||
|         text_severities = None | ||||
|         always_dashboard = None | ||||
|         always_email = None | ||||
|         always_text = None | ||||
|         alert_interval = None | ||||
|         email_task = None | ||||
|         text_task = None | ||||
|  | ||||
|         # check what the instance passed is | ||||
|         if isinstance(instance, Agent): | ||||
|             from agents.tasks import agent_outage_email_task, agent_outage_sms_task | ||||
|  | ||||
|             email_task = agent_outage_email_task | ||||
|             text_task = agent_outage_sms_task | ||||
|  | ||||
|             email_alert = instance.overdue_email_alert | ||||
|             text_alert = instance.overdue_text_alert | ||||
|             dashboard_alert = instance.overdue_dashboard_alert | ||||
|             alert_template = instance.alert_template | ||||
|             maintenance_mode = instance.maintenance_mode | ||||
|             alert_severity = "error" | ||||
|             agent = instance | ||||
|  | ||||
|             # set alert_template settings | ||||
|             if alert_template: | ||||
|                 dashboard_severities = ["error"] | ||||
|                 email_severities = ["error"] | ||||
|                 text_severities = ["error"] | ||||
|                 always_dashboard = alert_template.agent_always_alert | ||||
|                 always_email = alert_template.agent_always_email | ||||
|                 always_text = alert_template.agent_always_text | ||||
|                 alert_interval = alert_template.agent_periodic_alert_days | ||||
|                 run_script_action = alert_template.agent_script_actions | ||||
|  | ||||
|             if instance.should_create_alert(alert_template): | ||||
|                 alert = cls.create_or_return_availability_alert(instance) | ||||
|             else: | ||||
|                 # check if there is an alert that exists | ||||
|                 if cls.objects.filter(agent=instance, resolved=False).exists(): | ||||
|                     alert = cls.objects.get(agent=instance, resolved=False) | ||||
|                 else: | ||||
|                     alert = None | ||||
|  | ||||
|         elif isinstance(instance, Check): | ||||
|             from checks.tasks import ( | ||||
|                 handle_check_email_alert_task, | ||||
|                 handle_check_sms_alert_task, | ||||
|             ) | ||||
|  | ||||
|             email_task = handle_check_email_alert_task | ||||
|             text_task = handle_check_sms_alert_task | ||||
|  | ||||
|             email_alert = instance.email_alert | ||||
|             text_alert = instance.text_alert | ||||
|             dashboard_alert = instance.dashboard_alert | ||||
|             alert_template = instance.agent.alert_template | ||||
|             maintenance_mode = instance.agent.maintenance_mode | ||||
|             alert_severity = instance.alert_severity | ||||
|             agent = instance.agent | ||||
|  | ||||
|             # set alert_template settings | ||||
|             if alert_template: | ||||
|                 dashboard_severities = alert_template.check_dashboard_alert_severity | ||||
|                 email_severities = alert_template.check_email_alert_severity | ||||
|                 text_severities = alert_template.check_text_alert_severity | ||||
|                 always_dashboard = alert_template.check_always_alert | ||||
|                 always_email = alert_template.check_always_email | ||||
|                 always_text = alert_template.check_always_text | ||||
|                 alert_interval = alert_template.check_periodic_alert_days | ||||
|                 run_script_action = alert_template.check_script_actions | ||||
|  | ||||
|             if instance.should_create_alert(alert_template): | ||||
|                 alert = cls.create_or_return_check_alert(instance) | ||||
|             else: | ||||
|                 # check if there is an alert that exists | ||||
|                 if cls.objects.filter(assigned_check=instance, resolved=False).exists(): | ||||
|                     alert = cls.objects.get(assigned_check=instance, resolved=False) | ||||
|                 else: | ||||
|                     alert = None | ||||
|  | ||||
|         elif isinstance(instance, AutomatedTask): | ||||
|             from autotasks.tasks import handle_task_email_alert, handle_task_sms_alert | ||||
|  | ||||
|             email_task = handle_task_email_alert | ||||
|             text_task = handle_task_sms_alert | ||||
|  | ||||
|             email_alert = instance.email_alert | ||||
|             text_alert = instance.text_alert | ||||
|             dashboard_alert = instance.dashboard_alert | ||||
|             alert_template = instance.agent.alert_template | ||||
|             maintenance_mode = instance.agent.maintenance_mode | ||||
|             alert_severity = instance.alert_severity | ||||
|             agent = instance.agent | ||||
|  | ||||
|             # set alert_template settings | ||||
|             if alert_template: | ||||
|                 dashboard_severities = alert_template.task_dashboard_alert_severity | ||||
|                 email_severities = alert_template.task_email_alert_severity | ||||
|                 text_severities = alert_template.task_text_alert_severity | ||||
|                 always_dashboard = alert_template.task_always_alert | ||||
|                 always_email = alert_template.task_always_email | ||||
|                 always_text = alert_template.task_always_text | ||||
|                 alert_interval = alert_template.task_periodic_alert_days | ||||
|                 run_script_action = alert_template.task_script_actions | ||||
|  | ||||
|             if instance.should_create_alert(alert_template): | ||||
|                 alert = cls.create_or_return_task_alert(instance) | ||||
|             else: | ||||
|                 # check if there is an alert that exists | ||||
|                 if cls.objects.filter(assigned_task=instance, resolved=False).exists(): | ||||
|                     alert = cls.objects.get(assigned_task=instance, resolved=False) | ||||
|                 else: | ||||
|                     alert = None | ||||
|         else: | ||||
|             return | ||||
|  | ||||
|         # return if agent is in maintenance mode | ||||
|         if maintenance_mode or not alert: | ||||
|             return | ||||
|  | ||||
|         # check if alert severity changed on check and update the alert | ||||
|         if alert_severity != alert.severity: | ||||
|             alert.severity = alert_severity | ||||
|             alert.save(update_fields=["severity"]) | ||||
|  | ||||
|         # create alert in dashboard if enabled | ||||
|         if dashboard_alert or always_dashboard: | ||||
|  | ||||
|             # check if alert template is set and specific severities are configured | ||||
|             if alert_template and alert.severity not in dashboard_severities:  # type: ignore | ||||
|                 pass | ||||
|             else: | ||||
|                 alert.hidden = False | ||||
|                 alert.save() | ||||
|  | ||||
|         # send email if enabled | ||||
|         if email_alert or always_email: | ||||
|  | ||||
|             # check if alert template is set and specific severities are configured | ||||
|             if alert_template and alert.severity not in email_severities:  # type: ignore | ||||
|                 pass | ||||
|             else: | ||||
|                 email_task.delay( | ||||
|                     pk=alert.pk, | ||||
|                     alert_interval=alert_interval, | ||||
|                 ) | ||||
|  | ||||
|         # send text if enabled | ||||
|         if text_alert or always_text: | ||||
|  | ||||
|             # check if alert template is set and specific severities are configured | ||||
|             if alert_template and alert.severity not in text_severities:  # type: ignore | ||||
|                 pass | ||||
|             else: | ||||
|                 text_task.delay(pk=alert.pk, alert_interval=alert_interval) | ||||
|  | ||||
|         # check if any scripts should be run | ||||
|         if alert_template and alert_template.action and run_script_action and not alert.action_run:  # type: ignore | ||||
|             r = agent.run_script( | ||||
|                 scriptpk=alert_template.action.pk, | ||||
|                 args=alert.parse_script_args(alert_template.action_args), | ||||
|                 timeout=alert_template.action_timeout, | ||||
|                 wait=True, | ||||
|                 full=True, | ||||
|                 run_on_any=True, | ||||
|             ) | ||||
|  | ||||
|             # command was successful | ||||
|             if type(r) == dict: | ||||
|                 alert.action_retcode = r["retcode"] | ||||
|                 alert.action_stdout = r["stdout"] | ||||
|                 alert.action_stderr = r["stderr"] | ||||
|                 alert.action_execution_time = "{:.4f}".format(r["execution_time"]) | ||||
|                 alert.action_run = djangotime.now() | ||||
|                 alert.save() | ||||
|             else: | ||||
|                 DebugLog.error( | ||||
|                     agent=agent, | ||||
|                     log_type="scripting", | ||||
|                     message=f"Failure action: {alert_template.action.name} failed to run on any agent for {agent.hostname}({agent.pk}) failure alert", | ||||
|                 ) | ||||
|  | ||||
|     @classmethod | ||||
|     def handle_alert_resolve(cls, instance: Union[Agent, AutomatedTask, Check]) -> None: | ||||
|         from agents.models import Agent | ||||
|         from autotasks.models import AutomatedTask | ||||
|         from checks.models import Check | ||||
|  | ||||
|         # set variables | ||||
|         email_on_resolved = False | ||||
|         text_on_resolved = False | ||||
|         resolved_email_task = None | ||||
|         resolved_text_task = None | ||||
|  | ||||
|         # check what the instance passed is | ||||
|         if isinstance(instance, Agent): | ||||
|             from agents.tasks import agent_recovery_email_task, agent_recovery_sms_task | ||||
|  | ||||
|             resolved_email_task = agent_recovery_email_task | ||||
|             resolved_text_task = agent_recovery_sms_task | ||||
|  | ||||
|             alert_template = instance.alert_template | ||||
|             alert = cls.objects.get(agent=instance, resolved=False) | ||||
|             maintenance_mode = instance.maintenance_mode | ||||
|             agent = instance | ||||
|  | ||||
|             if alert_template: | ||||
|                 email_on_resolved = alert_template.agent_email_on_resolved | ||||
|                 text_on_resolved = alert_template.agent_text_on_resolved | ||||
|                 run_script_action = alert_template.agent_script_actions | ||||
|  | ||||
|         elif isinstance(instance, Check): | ||||
|             from checks.tasks import ( | ||||
|                 handle_resolved_check_email_alert_task, | ||||
|                 handle_resolved_check_sms_alert_task, | ||||
|             ) | ||||
|  | ||||
|             resolved_email_task = handle_resolved_check_email_alert_task | ||||
|             resolved_text_task = handle_resolved_check_sms_alert_task | ||||
|  | ||||
|             alert_template = instance.agent.alert_template | ||||
|             alert = cls.objects.get(assigned_check=instance, resolved=False) | ||||
|             maintenance_mode = instance.agent.maintenance_mode | ||||
|             agent = instance.agent | ||||
|  | ||||
|             if alert_template: | ||||
|                 email_on_resolved = alert_template.check_email_on_resolved | ||||
|                 text_on_resolved = alert_template.check_text_on_resolved | ||||
|                 run_script_action = alert_template.check_script_actions | ||||
|  | ||||
|         elif isinstance(instance, AutomatedTask): | ||||
|             from autotasks.tasks import ( | ||||
|                 handle_resolved_task_email_alert, | ||||
|                 handle_resolved_task_sms_alert, | ||||
|             ) | ||||
|  | ||||
|             resolved_email_task = handle_resolved_task_email_alert | ||||
|             resolved_text_task = handle_resolved_task_sms_alert | ||||
|  | ||||
|             alert_template = instance.agent.alert_template | ||||
|             alert = cls.objects.get(assigned_task=instance, resolved=False) | ||||
|             maintenance_mode = instance.agent.maintenance_mode | ||||
|             agent = instance.agent | ||||
|  | ||||
|             if alert_template: | ||||
|                 email_on_resolved = alert_template.task_email_on_resolved | ||||
|                 text_on_resolved = alert_template.task_text_on_resolved | ||||
|                 run_script_action = alert_template.task_script_actions | ||||
|  | ||||
|         else: | ||||
|             return | ||||
|  | ||||
|         # return if agent is in maintenance mode | ||||
|         if maintenance_mode: | ||||
|             return | ||||
|  | ||||
|         alert.resolve() | ||||
|  | ||||
|         # check if a resolved email notification should be send | ||||
|         if email_on_resolved and not alert.resolved_email_sent: | ||||
|             resolved_email_task.delay(pk=alert.pk) | ||||
|  | ||||
|         # check if resolved text should be sent | ||||
|         if text_on_resolved and not alert.resolved_sms_sent: | ||||
|             resolved_text_task.delay(pk=alert.pk) | ||||
|  | ||||
|         # check if resolved script should be run | ||||
|         if ( | ||||
|             alert_template | ||||
|             and alert_template.resolved_action | ||||
|             and run_script_action  # type: ignore | ||||
|             and not alert.resolved_action_run | ||||
|         ): | ||||
|             r = agent.run_script( | ||||
|                 scriptpk=alert_template.resolved_action.pk, | ||||
|                 args=alert.parse_script_args(alert_template.resolved_action_args), | ||||
|                 timeout=alert_template.resolved_action_timeout, | ||||
|                 wait=True, | ||||
|                 full=True, | ||||
|                 run_on_any=True, | ||||
|             ) | ||||
|  | ||||
|             # command was successful | ||||
|             if type(r) == dict: | ||||
|                 alert.resolved_action_retcode = r["retcode"] | ||||
|                 alert.resolved_action_stdout = r["stdout"] | ||||
|                 alert.resolved_action_stderr = r["stderr"] | ||||
|                 alert.resolved_action_execution_time = "{:.4f}".format( | ||||
|                     r["execution_time"] | ||||
|                 ) | ||||
|                 alert.resolved_action_run = djangotime.now() | ||||
|                 alert.save() | ||||
|             else: | ||||
|                 DebugLog.error( | ||||
|                     agent=agent, | ||||
|                     log_type="scripting", | ||||
|                     message=f"Resolved action: {alert_template.action.name} failed to run on any agent for {agent.hostname}({agent.pk}) resolved alert", | ||||
|                 ) | ||||
|  | ||||
|     def parse_script_args(self, args: list[str]): | ||||
|  | ||||
|         if not args: | ||||
|             return [] | ||||
|  | ||||
|         temp_args = list() | ||||
|         # pattern to match for injection | ||||
|         pattern = re.compile(".*\\{\\{alert\\.(.*)\\}\\}.*") | ||||
|  | ||||
|         for arg in args: | ||||
|             match = pattern.match(arg) | ||||
|             if match: | ||||
|                 name = match.group(1) | ||||
|  | ||||
|                 # check if attr exists and isn't a function | ||||
|                 if hasattr(self, name) and not callable(getattr(self, name)): | ||||
|                     value = f"'{getattr(self, name)}'" | ||||
|                 else: | ||||
|                     continue | ||||
|  | ||||
|                 try: | ||||
|                     temp_args.append(re.sub("\\{\\{.*\\}\\}", value, arg))  # type: ignore | ||||
|                 except Exception as e: | ||||
|                     DebugLog.error(log_type="scripting", message=str(e)) | ||||
|                     continue | ||||
|  | ||||
|             else: | ||||
|                 temp_args.append(arg) | ||||
|  | ||||
|         return temp_args | ||||
|  | ||||
|  | ||||
| class AlertTemplate(BaseAuditModel): | ||||
|     name = models.CharField(max_length=100) | ||||
|     is_active = models.BooleanField(default=True) | ||||
|  | ||||
|     action = models.ForeignKey( | ||||
|         "scripts.Script", | ||||
|         related_name="alert_template", | ||||
|         blank=True, | ||||
|         null=True, | ||||
|         on_delete=models.SET_NULL, | ||||
|     ) | ||||
|     action_args = ArrayField( | ||||
|         models.CharField(max_length=255, null=True, blank=True), | ||||
|         null=True, | ||||
|         blank=True, | ||||
|         default=list, | ||||
|     ) | ||||
|     action_timeout = models.PositiveIntegerField(default=15) | ||||
|     resolved_action = models.ForeignKey( | ||||
|         "scripts.Script", | ||||
|         related_name="resolved_alert_template", | ||||
|         blank=True, | ||||
|         null=True, | ||||
|         on_delete=models.SET_NULL, | ||||
|     ) | ||||
|     resolved_action_args = ArrayField( | ||||
|         models.CharField(max_length=255, null=True, blank=True), | ||||
|         null=True, | ||||
|         blank=True, | ||||
|         default=list, | ||||
|     ) | ||||
|     resolved_action_timeout = models.PositiveIntegerField(default=15) | ||||
|  | ||||
|     # overrides the global recipients | ||||
|     email_recipients = ArrayField( | ||||
|         models.CharField(max_length=100, blank=True), | ||||
|         null=True, | ||||
|         blank=True, | ||||
|         default=list, | ||||
|     ) | ||||
|     text_recipients = ArrayField( | ||||
|         models.CharField(max_length=100, blank=True), | ||||
|         null=True, | ||||
|         blank=True, | ||||
|         default=list, | ||||
|     ) | ||||
|  | ||||
|     # overrides the from address | ||||
|     email_from = models.EmailField(blank=True, null=True) | ||||
|  | ||||
|     # agent alert settings | ||||
|     agent_email_on_resolved = BooleanField(null=True, blank=True, default=False) | ||||
|     agent_text_on_resolved = BooleanField(null=True, blank=True, default=False) | ||||
|     agent_always_email = BooleanField(null=True, blank=True, default=None) | ||||
|     agent_always_text = BooleanField(null=True, blank=True, default=None) | ||||
|     agent_always_alert = BooleanField(null=True, blank=True, default=None) | ||||
|     agent_periodic_alert_days = PositiveIntegerField(blank=True, null=True, default=0) | ||||
|     agent_script_actions = BooleanField(null=True, blank=True, default=True) | ||||
|  | ||||
|     # check alert settings | ||||
|     check_email_alert_severity = ArrayField( | ||||
|         models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES), | ||||
|         blank=True, | ||||
|         default=list, | ||||
|     ) | ||||
|     check_text_alert_severity = ArrayField( | ||||
|         models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES), | ||||
|         blank=True, | ||||
|         default=list, | ||||
|     ) | ||||
|     check_dashboard_alert_severity = ArrayField( | ||||
|         models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES), | ||||
|         blank=True, | ||||
|         default=list, | ||||
|     ) | ||||
|     check_email_on_resolved = BooleanField(null=True, blank=True, default=False) | ||||
|     check_text_on_resolved = BooleanField(null=True, blank=True, default=False) | ||||
|     check_always_email = BooleanField(null=True, blank=True, default=None) | ||||
|     check_always_text = BooleanField(null=True, blank=True, default=None) | ||||
|     check_always_alert = BooleanField(null=True, blank=True, default=None) | ||||
|     check_periodic_alert_days = PositiveIntegerField(blank=True, null=True, default=0) | ||||
|     check_script_actions = BooleanField(null=True, blank=True, default=True) | ||||
|  | ||||
|     # task alert settings | ||||
|     task_email_alert_severity = ArrayField( | ||||
|         models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES), | ||||
|         blank=True, | ||||
|         default=list, | ||||
|     ) | ||||
|     task_text_alert_severity = ArrayField( | ||||
|         models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES), | ||||
|         blank=True, | ||||
|         default=list, | ||||
|     ) | ||||
|     task_dashboard_alert_severity = ArrayField( | ||||
|         models.CharField(max_length=25, blank=True, choices=SEVERITY_CHOICES), | ||||
|         blank=True, | ||||
|         default=list, | ||||
|     ) | ||||
|     task_email_on_resolved = BooleanField(null=True, blank=True, default=False) | ||||
|     task_text_on_resolved = BooleanField(null=True, blank=True, default=False) | ||||
|     task_always_email = BooleanField(null=True, blank=True, default=None) | ||||
|     task_always_text = BooleanField(null=True, blank=True, default=None) | ||||
|     task_always_alert = BooleanField(null=True, blank=True, default=None) | ||||
|     task_periodic_alert_days = PositiveIntegerField(blank=True, null=True, default=0) | ||||
|     task_script_actions = BooleanField(null=True, blank=True, default=True) | ||||
|  | ||||
|     # exclusion settings | ||||
|     exclude_workstations = BooleanField(null=True, blank=True, default=False) | ||||
|     exclude_servers = BooleanField(null=True, blank=True, default=False) | ||||
|  | ||||
|     excluded_sites = models.ManyToManyField( | ||||
|         "clients.Site", related_name="alert_exclusions", blank=True | ||||
|     ) | ||||
|     excluded_clients = models.ManyToManyField( | ||||
|         "clients.Client", related_name="alert_exclusions", blank=True | ||||
|     ) | ||||
|     excluded_agents = models.ManyToManyField( | ||||
|         "agents.Agent", related_name="alert_exclusions", blank=True | ||||
|     ) | ||||
|  | ||||
|     def __str__(self): | ||||
|         return self.name | ||||
|  | ||||
|     @staticmethod | ||||
|     def serialize(alert_template): | ||||
|         # serializes the agent and returns json | ||||
|         from .serializers import AlertTemplateAuditSerializer | ||||
|  | ||||
|         return AlertTemplateAuditSerializer(alert_template).data | ||||
|  | ||||
|     @property | ||||
|     def has_agent_settings(self) -> bool: | ||||
|         return ( | ||||
|             self.agent_email_on_resolved | ||||
|             or self.agent_text_on_resolved | ||||
|             or self.agent_always_email | ||||
|             or self.agent_always_text | ||||
|             or self.agent_always_alert | ||||
|             or bool(self.agent_periodic_alert_days) | ||||
|         ) | ||||
|  | ||||
|     @property | ||||
|     def has_check_settings(self) -> bool: | ||||
|         return ( | ||||
|             bool(self.check_email_alert_severity) | ||||
|             or bool(self.check_text_alert_severity) | ||||
|             or bool(self.check_dashboard_alert_severity) | ||||
|             or self.check_email_on_resolved | ||||
|             or self.check_text_on_resolved | ||||
|             or self.check_always_email | ||||
|             or self.check_always_text | ||||
|             or self.check_always_alert | ||||
|             or bool(self.check_periodic_alert_days) | ||||
|         ) | ||||
|  | ||||
|     @property | ||||
|     def has_task_settings(self) -> bool: | ||||
|         return ( | ||||
|             bool(self.task_email_alert_severity) | ||||
|             or bool(self.task_text_alert_severity) | ||||
|             or bool(self.task_dashboard_alert_severity) | ||||
|             or self.task_email_on_resolved | ||||
|             or self.task_text_on_resolved | ||||
|             or self.task_always_email | ||||
|             or self.task_always_text | ||||
|             or self.task_always_alert | ||||
|             or bool(self.task_periodic_alert_days) | ||||
|         ) | ||||
|  | ||||
|     @property | ||||
|     def has_core_settings(self) -> bool: | ||||
|         return bool(self.email_from) or self.email_recipients or self.text_recipients | ||||
|  | ||||
|     @property | ||||
|     def is_default_template(self) -> bool: | ||||
|         return self.default_alert_template.exists()  # type: ignore | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user