Compare commits
	
		
			387 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					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 | 
@@ -1,7 +1,6 @@
 | 
			
		||||
FROM python:3.8-slim
 | 
			
		||||
FROM python:3.9.2-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,14 +8,11 @@ ENV VIRTUAL_ENV ${WORKSPACE_DIR}/api/tacticalrmm/env
 | 
			
		||||
ENV PYTHONDONTWRITEBYTECODE=1
 | 
			
		||||
ENV PYTHONUNBUFFERED=1
 | 
			
		||||
 | 
			
		||||
EXPOSE 8000
 | 
			
		||||
EXPOSE 8000 8383
 | 
			
		||||
 | 
			
		||||
RUN groupadd -g 1000 tactical && \
 | 
			
		||||
    useradd -u 1000 -g 1000 tactical
 | 
			
		||||
 | 
			
		||||
# Copy Go Files
 | 
			
		||||
COPY --from=golang:1.16 /usr/local/go ${TACTICAL_GO_DIR}/go
 | 
			
		||||
 | 
			
		||||
# Copy Dev python reqs
 | 
			
		||||
COPY ./requirements.txt /
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ version: '3.4'
 | 
			
		||||
 | 
			
		||||
services:
 | 
			
		||||
  api-dev:
 | 
			
		||||
    container_name: trmm-api-dev
 | 
			
		||||
    image: api-dev
 | 
			
		||||
    restart: always
 | 
			
		||||
    build:
 | 
			
		||||
@@ -21,9 +22,10 @@ services:
 | 
			
		||||
          - tactical-backend
 | 
			
		||||
 | 
			
		||||
  app-dev:
 | 
			
		||||
    image: node:12-alpine
 | 
			
		||||
    container_name: trmm-app-dev
 | 
			
		||||
    image: node:14-alpine
 | 
			
		||||
    restart: always
 | 
			
		||||
    command: /bin/sh -c "npm install && npm run serve -- --host 0.0.0.0 --port ${APP_PORT}"
 | 
			
		||||
    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
 | 
			
		||||
@@ -36,6 +38,7 @@ services:
 | 
			
		||||
 | 
			
		||||
  # nats
 | 
			
		||||
  nats-dev:
 | 
			
		||||
    container_name: trmm-nats-dev
 | 
			
		||||
    image: ${IMAGE_REPO}tactical-nats:${VERSION}
 | 
			
		||||
    restart: always
 | 
			
		||||
    environment:
 | 
			
		||||
@@ -55,6 +58,7 @@ services:
 | 
			
		||||
 | 
			
		||||
  # meshcentral container
 | 
			
		||||
  meshcentral-dev:
 | 
			
		||||
    container_name: trmm-meshcentral-dev
 | 
			
		||||
    image: ${IMAGE_REPO}tactical-meshcentral:${VERSION}
 | 
			
		||||
    restart: always
 | 
			
		||||
    environment: 
 | 
			
		||||
@@ -77,6 +81,7 @@ services:
 | 
			
		||||
 | 
			
		||||
  # mongodb container for meshcentral
 | 
			
		||||
  mongodb-dev:
 | 
			
		||||
    container_name: trmm-mongodb-dev
 | 
			
		||||
    image: mongo:4.4
 | 
			
		||||
    restart: always
 | 
			
		||||
    environment:
 | 
			
		||||
@@ -92,6 +97,7 @@ services:
 | 
			
		||||
 | 
			
		||||
  # postgres database for api service
 | 
			
		||||
  postgres-dev:
 | 
			
		||||
    container_name: trmm-postgres-dev
 | 
			
		||||
    image: postgres:13-alpine
 | 
			
		||||
    restart: always
 | 
			
		||||
    environment:
 | 
			
		||||
@@ -107,6 +113,7 @@ services:
 | 
			
		||||
 | 
			
		||||
  # redis container for celery tasks
 | 
			
		||||
  redis-dev:
 | 
			
		||||
    container_name: trmm-redis-dev
 | 
			
		||||
    restart: always
 | 
			
		||||
    image: redis:6.0-alpine
 | 
			
		||||
    networks:
 | 
			
		||||
@@ -115,6 +122,7 @@ services:
 | 
			
		||||
          - tactical-redis
 | 
			
		||||
 | 
			
		||||
  init-dev:
 | 
			
		||||
    container_name: trmm-init-dev
 | 
			
		||||
    image: api-dev
 | 
			
		||||
    build:
 | 
			
		||||
      context: .
 | 
			
		||||
@@ -143,6 +151,7 @@ services:
 | 
			
		||||
 | 
			
		||||
  # container for celery worker service
 | 
			
		||||
  celery-dev:
 | 
			
		||||
    container_name: trmm-celery-dev
 | 
			
		||||
    image: api-dev
 | 
			
		||||
    build:
 | 
			
		||||
      context: .
 | 
			
		||||
@@ -160,6 +169,7 @@ services:
 | 
			
		||||
 | 
			
		||||
  # container for celery beat service
 | 
			
		||||
  celerybeat-dev:
 | 
			
		||||
    container_name: trmm-celerybeat-dev
 | 
			
		||||
    image: api-dev
 | 
			
		||||
    build:
 | 
			
		||||
      context: .
 | 
			
		||||
@@ -175,8 +185,29 @@ services:
 | 
			
		||||
      - postgres-dev
 | 
			
		||||
      - redis-dev
 | 
			
		||||
 | 
			
		||||
  nginx-dev:
 | 
			
		||||
  # container for websockets communication
 | 
			
		||||
  websockets-dev:
 | 
			
		||||
    container_name: trmm-websockets-dev
 | 
			
		||||
    image: api-dev
 | 
			
		||||
    build:
 | 
			
		||||
      context: .
 | 
			
		||||
      dockerfile: ./api.dockerfile
 | 
			
		||||
    command: ["tactical-websockets-dev"]
 | 
			
		||||
    restart: always
 | 
			
		||||
    networks:
 | 
			
		||||
      dev:
 | 
			
		||||
        aliases:
 | 
			
		||||
          - tactical-websockets
 | 
			
		||||
    volumes:
 | 
			
		||||
      - tactical-data-dev:/opt/tactical
 | 
			
		||||
      - ..:/workspace:cached
 | 
			
		||||
    depends_on:
 | 
			
		||||
      - postgres-dev
 | 
			
		||||
      - redis-dev
 | 
			
		||||
 | 
			
		||||
  # container for tactical reverse proxy
 | 
			
		||||
  nginx-dev:
 | 
			
		||||
    container_name: trmm-nginx-dev
 | 
			
		||||
    image: ${IMAGE_REPO}tactical-nginx:${VERSION}
 | 
			
		||||
    restart: always
 | 
			
		||||
    environment:
 | 
			
		||||
 
 | 
			
		||||
@@ -100,6 +100,7 @@ MESH_USERNAME = '${MESH_USER}'
 | 
			
		||||
MESH_SITE = 'https://${MESH_HOST}'
 | 
			
		||||
MESH_TOKEN_KEY = '${MESH_TOKEN}'
 | 
			
		||||
REDIS_HOST    = '${REDIS_HOST}'
 | 
			
		||||
ADMIN_ENABLED = True
 | 
			
		||||
EOF
 | 
			
		||||
)"
 | 
			
		||||
 | 
			
		||||
@@ -126,7 +127,7 @@ if [ "$1" = 'tactical-init-dev' ]; then
 | 
			
		||||
  test -f "${TACTICAL_READY_FILE}" && rm "${TACTICAL_READY_FILE}"
 | 
			
		||||
 | 
			
		||||
  # setup Python virtual env and install dependencies
 | 
			
		||||
  ! test -e "${VIRTUAL_ENV}" && python -m venv --copies ${VIRTUAL_ENV}
 | 
			
		||||
  ! test -e "${VIRTUAL_ENV}" && python -m venv ${VIRTUAL_ENV}
 | 
			
		||||
  "${VIRTUAL_ENV}"/bin/pip install --no-cache-dir -r /requirements.txt
 | 
			
		||||
 | 
			
		||||
  django_setup
 | 
			
		||||
@@ -135,10 +136,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}"
 | 
			
		||||
@@ -149,9 +151,6 @@ 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
 | 
			
		||||
  "${VIRTUAL_ENV}"/bin/python manage.py runserver 0.0.0.0:"${API_PORT}"
 | 
			
		||||
fi
 | 
			
		||||
@@ -166,3 +165,8 @@ if [ "$1" = 'tactical-celerybeat-dev' ]; then
 | 
			
		||||
  test -f "${WORKSPACE_DIR}/api/tacticalrmm/celerybeat.pid" && rm "${WORKSPACE_DIR}/api/tacticalrmm/celerybeat.pid"
 | 
			
		||||
  "${VIRTUAL_ENV}"/bin/celery -A tacticalrmm beat -l debug
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
if [ "$1" = 'tactical-websockets-dev' ]; then
 | 
			
		||||
  check_tactical_ready
 | 
			
		||||
  "${VIRTUAL_ENV}"/bin/daphne tacticalrmm.asgi:application --port 8383 -b 0.0.0.0
 | 
			
		||||
fi
 | 
			
		||||
 
 | 
			
		||||
@@ -1,40 +1,25 @@
 | 
			
		||||
# 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==5.0.5
 | 
			
		||||
asgiref==3.3.1
 | 
			
		||||
asyncio-nats-client==0.11.4
 | 
			
		||||
billiard==3.6.3.0
 | 
			
		||||
celery==5.0.5
 | 
			
		||||
certifi==2020.12.5
 | 
			
		||||
cffi==1.14.5
 | 
			
		||||
chardet==4.0.0
 | 
			
		||||
cryptography==3.4.4
 | 
			
		||||
decorator==4.4.2
 | 
			
		||||
Django==3.1.6
 | 
			
		||||
django-cors-headers==3.7.0
 | 
			
		||||
django-rest-knox==4.1.0
 | 
			
		||||
djangorestframework==3.12.2
 | 
			
		||||
future==0.18.2
 | 
			
		||||
kombu==5.0.2
 | 
			
		||||
loguru==0.5.3
 | 
			
		||||
msgpack==1.0.2
 | 
			
		||||
packaging==20.8
 | 
			
		||||
psycopg2-binary==2.8.6
 | 
			
		||||
pycparser==2.20
 | 
			
		||||
pycryptodome==3.10.1
 | 
			
		||||
pyotp==2.6.0
 | 
			
		||||
pyparsing==2.4.7
 | 
			
		||||
pytz==2021.1
 | 
			
		||||
qrcode==6.1
 | 
			
		||||
redis==3.5.3
 | 
			
		||||
requests==2.25.1
 | 
			
		||||
six==1.15.0
 | 
			
		||||
sqlparse==0.4.1
 | 
			
		||||
twilio==6.52.0
 | 
			
		||||
urllib3==1.26.3
 | 
			
		||||
validators==0.18.2
 | 
			
		||||
vine==5.0.0
 | 
			
		||||
websockets==8.1
 | 
			
		||||
zipp==3.4.0
 | 
			
		||||
asyncio-nats-client
 | 
			
		||||
celery
 | 
			
		||||
channels
 | 
			
		||||
Django
 | 
			
		||||
django-cors-headers
 | 
			
		||||
django-rest-knox
 | 
			
		||||
djangorestframework
 | 
			
		||||
loguru
 | 
			
		||||
msgpack
 | 
			
		||||
psycopg2-binary
 | 
			
		||||
pycparser
 | 
			
		||||
pycryptodome
 | 
			
		||||
pyotp
 | 
			
		||||
pyparsing
 | 
			
		||||
pytz
 | 
			
		||||
qrcode
 | 
			
		||||
redis
 | 
			
		||||
twilio
 | 
			
		||||
packaging
 | 
			
		||||
validators
 | 
			
		||||
websockets
 | 
			
		||||
black
 | 
			
		||||
Werkzeug
 | 
			
		||||
django-extensions
 | 
			
		||||
@@ -44,3 +29,7 @@ model_bakery
 | 
			
		||||
mkdocs
 | 
			
		||||
mkdocs-material
 | 
			
		||||
pymdown-extensions
 | 
			
		||||
Pygments
 | 
			
		||||
mypy
 | 
			
		||||
pysnooper
 | 
			
		||||
isort
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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.
 | 
			
		||||
							
								
								
									
										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
 | 
			
		||||
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -45,3 +45,5 @@ htmlcov/
 | 
			
		||||
docker-compose.dev.yml
 | 
			
		||||
docs/.vuepress/dist
 | 
			
		||||
nats-rmm.conf
 | 
			
		||||
.mypy_cache
 | 
			
		||||
docs/site/
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										100
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										100
									
								
								README.md
									
									
									
									
									
								
							@@ -8,13 +8,15 @@
 | 
			
		||||
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 and integrates with [MeshCentral](https://github.com/Ylianst/MeshCentral)
 | 
			
		||||
 | 
			
		||||
# [LIVE DEMO](https://rmm.xlawgaming.com/)
 | 
			
		||||
# [LIVE DEMO](https://rmm.tacticalrmm.io/)
 | 
			
		||||
Demo database resets every hour. Alot of features are disabled for obvious reasons due to the nature of this app.
 | 
			
		||||
 | 
			
		||||
*Tactical RMM is currently in alpha and subject to breaking changes. Use in production at your own risk.*
 | 
			
		||||
 | 
			
		||||
### [Discord Chat](https://discord.gg/upGTkWp)
 | 
			
		||||
 | 
			
		||||
### [Documentation](https://wh1te909.github.io/tacticalrmm/)
 | 
			
		||||
 | 
			
		||||
## Features
 | 
			
		||||
 | 
			
		||||
- Teamviewer-like remote desktop control
 | 
			
		||||
@@ -33,98 +35,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 2GB 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 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/)
 | 
			
		||||
@@ -7,7 +7,7 @@ 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)
 | 
			
		||||
@@ -26,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!"))
 | 
			
		||||
@@ -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),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -15,6 +15,11 @@ 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)
 | 
			
		||||
@@ -27,6 +32,12 @@ class User(AbstractUser, BaseAuditModel):
 | 
			
		||||
    default_agent_tbl_tab = models.CharField(
 | 
			
		||||
        max_length=50, choices=AGENT_TBL_TAB_CHOICES, default="server"
 | 
			
		||||
    )
 | 
			
		||||
    agents_per_page = models.PositiveIntegerField(default=50)  # not currently used
 | 
			
		||||
    client_tree_sort = models.CharField(
 | 
			
		||||
        max_length=50, choices=CLIENT_TREE_SORT_CHOICES, default="alphafail"
 | 
			
		||||
    )
 | 
			
		||||
    client_tree_splitter = models.PositiveIntegerField(default=11)
 | 
			
		||||
    loading_bar_color = models.CharField(max_length=255, default="red")
 | 
			
		||||
 | 
			
		||||
    agent = models.OneToOneField(
 | 
			
		||||
        "agents.Agent",
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,20 @@ from rest_framework.serializers import ModelSerializer, SerializerMethodField
 | 
			
		||||
from .models import User
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserUISerializer(ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = User
 | 
			
		||||
        fields = [
 | 
			
		||||
            "dark_mode",
 | 
			
		||||
            "show_community_scripts",
 | 
			
		||||
            "agent_dblclick_action",
 | 
			
		||||
            "default_agent_tbl_tab",
 | 
			
		||||
            "client_tree_sort",
 | 
			
		||||
            "client_tree_splitter",
 | 
			
		||||
            "loading_bar_color",
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserSerializer(ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = User
 | 
			
		||||
 
 | 
			
		||||
@@ -271,18 +271,15 @@ 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 = {
 | 
			
		||||
            "userui": True,
 | 
			
		||||
            "dark_mode": True,
 | 
			
		||||
            "show_community_scripts": True,
 | 
			
		||||
            "agent_dblclick_action": "editagent",
 | 
			
		||||
            "default_agent_tbl_tab": "mixed",
 | 
			
		||||
            "client_tree_sort": "alpha",
 | 
			
		||||
            "client_tree_splitter": 14,
 | 
			
		||||
            "loading_bar_color": "green",
 | 
			
		||||
        }
 | 
			
		||||
        r = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 
 | 
			
		||||
@@ -10,12 +10,19 @@ from rest_framework.permissions import AllowAny
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from logs.models import AuditLog
 | 
			
		||||
from tacticalrmm.utils import notify_error
 | 
			
		||||
 | 
			
		||||
from .models import User
 | 
			
		||||
from .serializers import TOTPSetupSerializer, UserSerializer
 | 
			
		||||
from .serializers import TOTPSetupSerializer, UserSerializer, UserUISerializer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _is_root_user(request, user) -> bool:
 | 
			
		||||
    return (
 | 
			
		||||
        hasattr(settings, "ROOT_USER")
 | 
			
		||||
        and request.user != user
 | 
			
		||||
        and user.username == settings.ROOT_USER
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CheckCreds(KnoxLoginView):
 | 
			
		||||
@@ -79,7 +86,7 @@ class GetAddUsers(APIView):
 | 
			
		||||
    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"],
 | 
			
		||||
@@ -106,11 +113,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)
 | 
			
		||||
@@ -121,11 +124,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()
 | 
			
		||||
@@ -138,11 +137,7 @@ class UserActions(APIView):
 | 
			
		||||
    # 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"])
 | 
			
		||||
@@ -153,11 +148,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 = ""
 | 
			
		||||
@@ -185,19 +176,9 @@ 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")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,8 @@
 | 
			
		||||
from django.contrib import admin
 | 
			
		||||
 | 
			
		||||
from .models import Agent, Note, RecoveryAction
 | 
			
		||||
from .models import Agent, AgentCustomField, Note, RecoveryAction
 | 
			
		||||
 | 
			
		||||
admin.site.register(Agent)
 | 
			
		||||
admin.site.register(RecoveryAction)
 | 
			
		||||
admin.site.register(Note)
 | 
			
		||||
admin.site.register(AgentCustomField)
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ from itertools import cycle
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from model_bakery.recipe import Recipe, foreign_key
 | 
			
		||||
from model_bakery.recipe import Recipe, foreign_key, seq
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def generate_agent_id(hostname):
 | 
			
		||||
@@ -30,8 +30,7 @@ agent = Recipe(
 | 
			
		||||
    hostname="DESKTOP-TEST123",
 | 
			
		||||
    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("asdkj3h4234-1234hg3h4g34-234jjh34|DESKTOP-TEST123"),
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
server_agent = agent.extend(
 | 
			
		||||
@@ -44,8 +43,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,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}")
 | 
			
		||||
            )
 | 
			
		||||
@@ -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',
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -4,7 +4,7 @@ import re
 | 
			
		||||
import time
 | 
			
		||||
from collections import Counter
 | 
			
		||||
from distutils.version import LooseVersion
 | 
			
		||||
from typing import Any, List, Union
 | 
			
		||||
from typing import Any
 | 
			
		||||
 | 
			
		||||
import msgpack
 | 
			
		||||
import validators
 | 
			
		||||
@@ -13,14 +13,13 @@ from Crypto.Hash import SHA3_384
 | 
			
		||||
from Crypto.Random import get_random_bytes
 | 
			
		||||
from Crypto.Util.Padding import pad
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.contrib.postgres.fields import ArrayField
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from loguru import logger
 | 
			
		||||
from nats.aio.client import Client as NATS
 | 
			
		||||
from nats.aio.errors import ErrTimeout
 | 
			
		||||
from packaging import version as pyver
 | 
			
		||||
 | 
			
		||||
from alerts.models import AlertTemplate
 | 
			
		||||
from core.models import TZ_CHOICES, CoreSettings
 | 
			
		||||
from logs.models import BaseAuditModel
 | 
			
		||||
 | 
			
		||||
@@ -64,6 +63,13 @@ class Agent(BaseAuditModel):
 | 
			
		||||
        max_length=255, choices=TZ_CHOICES, null=True, blank=True
 | 
			
		||||
    )
 | 
			
		||||
    maintenance_mode = models.BooleanField(default=False)
 | 
			
		||||
    alert_template = models.ForeignKey(
 | 
			
		||||
        "alerts.AlertTemplate",
 | 
			
		||||
        related_name="agents",
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        on_delete=models.SET_NULL,
 | 
			
		||||
    )
 | 
			
		||||
    site = models.ForeignKey(
 | 
			
		||||
        "clients.Site",
 | 
			
		||||
        related_name="agents",
 | 
			
		||||
@@ -85,7 +91,7 @@ class Agent(BaseAuditModel):
 | 
			
		||||
        old_agent = type(self).objects.get(pk=self.pk) if self.pk else None
 | 
			
		||||
        super(BaseAuditModel, self).save(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
        # check if new agent has been create
 | 
			
		||||
        # check if new agent has been created
 | 
			
		||||
        # or check if policy have changed on agent
 | 
			
		||||
        # or if site has changed on agent and if so generate-policies
 | 
			
		||||
        if (
 | 
			
		||||
@@ -104,14 +110,6 @@ class Agent(BaseAuditModel):
 | 
			
		||||
    def client(self):
 | 
			
		||||
        return self.site.client
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def has_nats(self):
 | 
			
		||||
        return pyver.parse(self.version) >= pyver.parse("1.1.0")
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def has_gotasks(self):
 | 
			
		||||
        return pyver.parse(self.version) >= pyver.parse("1.1.1")
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def timezone(self):
 | 
			
		||||
        # return the default timezone unless the timezone is explicity set per agent
 | 
			
		||||
@@ -164,14 +162,14 @@ class Agent(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def has_patches_pending(self):
 | 
			
		||||
        return self.winupdates.filter(action="approve").filter(installed=False).exists()
 | 
			
		||||
        return self.winupdates.filter(action="approve").filter(installed=False).exists()  # type: ignore
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def checks(self):
 | 
			
		||||
        total, passing, failing = 0, 0, 0
 | 
			
		||||
 | 
			
		||||
        if self.agentchecks.exists():
 | 
			
		||||
            for i in self.agentchecks.all():
 | 
			
		||||
        if self.agentchecks.exists():  # type: ignore
 | 
			
		||||
            for i in self.agentchecks.all():  # type: ignore
 | 
			
		||||
                total += 1
 | 
			
		||||
                if i.status == "passing":
 | 
			
		||||
                    passing += 1
 | 
			
		||||
@@ -197,6 +195,27 @@ class Agent(BaseAuditModel):
 | 
			
		||||
        except:
 | 
			
		||||
            return ["unknown cpu model"]
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def graphics(self):
 | 
			
		||||
        ret, mrda = [], []
 | 
			
		||||
        try:
 | 
			
		||||
            graphics = self.wmi_detail["graphics"]
 | 
			
		||||
            for i in graphics:
 | 
			
		||||
                caption = [x["Caption"] for x in i if "Caption" in x][0]
 | 
			
		||||
                if "microsoft remote display adapter" in caption.lower():
 | 
			
		||||
                    mrda.append("yes")
 | 
			
		||||
                    continue
 | 
			
		||||
 | 
			
		||||
                ret.append([x["Caption"] for x in i if "Caption" in x][0])
 | 
			
		||||
 | 
			
		||||
            # only return this if no other graphics cards
 | 
			
		||||
            if not ret and mrda:
 | 
			
		||||
                return "Microsoft Remote Display Adapter"
 | 
			
		||||
 | 
			
		||||
            return ", ".join(ret)
 | 
			
		||||
        except:
 | 
			
		||||
            return "Graphics info requires agent v1.4.14"
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def local_ips(self):
 | 
			
		||||
        ret = []
 | 
			
		||||
@@ -241,6 +260,7 @@ class Agent(BaseAuditModel):
 | 
			
		||||
            pass
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            comp_sys_prod = self.wmi_detail["comp_sys_prod"][0]
 | 
			
		||||
            return [x["Version"] for x in comp_sys_prod if "Version" in x][0]
 | 
			
		||||
        except:
 | 
			
		||||
            pass
 | 
			
		||||
@@ -270,10 +290,24 @@ class Agent(BaseAuditModel):
 | 
			
		||||
        except:
 | 
			
		||||
            return ["unknown disk"]
 | 
			
		||||
 | 
			
		||||
    def check_run_interval(self) -> int:
 | 
			
		||||
        interval = self.check_interval
 | 
			
		||||
        # determine if any agent checks have a custom interval and set the lowest interval
 | 
			
		||||
        for check in self.agentchecks.filter(overriden_by_policy=False):  # type: ignore
 | 
			
		||||
            if check.run_interval and check.run_interval < interval:
 | 
			
		||||
 | 
			
		||||
                # don't allow check runs less than 15s
 | 
			
		||||
                if check.run_interval < 15:
 | 
			
		||||
                    interval = 15
 | 
			
		||||
                else:
 | 
			
		||||
                    interval = check.run_interval
 | 
			
		||||
 | 
			
		||||
        return interval
 | 
			
		||||
 | 
			
		||||
    def run_script(
 | 
			
		||||
        self,
 | 
			
		||||
        scriptpk: int,
 | 
			
		||||
        args: List[str] = [],
 | 
			
		||||
        args: list[str] = [],
 | 
			
		||||
        timeout: int = 120,
 | 
			
		||||
        full: bool = False,
 | 
			
		||||
        wait: bool = False,
 | 
			
		||||
@@ -283,10 +317,13 @@ class Agent(BaseAuditModel):
 | 
			
		||||
        from scripts.models import Script
 | 
			
		||||
 | 
			
		||||
        script = Script.objects.get(pk=scriptpk)
 | 
			
		||||
 | 
			
		||||
        parsed_args = script.parse_script_args(self, script.shell, args)
 | 
			
		||||
 | 
			
		||||
        data = {
 | 
			
		||||
            "func": "runscriptfull" if full else "runscript",
 | 
			
		||||
            "timeout": timeout,
 | 
			
		||||
            "script_args": args,
 | 
			
		||||
            "script_args": parsed_args,
 | 
			
		||||
            "payload": {
 | 
			
		||||
                "code": script.code,
 | 
			
		||||
                "shell": script.shell,
 | 
			
		||||
@@ -295,10 +332,10 @@ class Agent(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
        running_agent = self
 | 
			
		||||
        if run_on_any:
 | 
			
		||||
            nats_ping = {"func": "ping", "timeout": 1}
 | 
			
		||||
            nats_ping = {"func": "ping"}
 | 
			
		||||
 | 
			
		||||
            # try on self first
 | 
			
		||||
            r = asyncio.run(self.nats_cmd(nats_ping))
 | 
			
		||||
            r = asyncio.run(self.nats_cmd(nats_ping, timeout=1))
 | 
			
		||||
 | 
			
		||||
            if r == "pong":
 | 
			
		||||
                running_agent = self
 | 
			
		||||
@@ -306,13 +343,13 @@ class Agent(BaseAuditModel):
 | 
			
		||||
                online = [
 | 
			
		||||
                    agent
 | 
			
		||||
                    for agent in Agent.objects.only(
 | 
			
		||||
                        "pk", "last_seen", "overdue_time", "offline_time"
 | 
			
		||||
                        "pk", "agent_id", "last_seen", "overdue_time", "offline_time"
 | 
			
		||||
                    )
 | 
			
		||||
                    if agent.status == "online"
 | 
			
		||||
                ]
 | 
			
		||||
 | 
			
		||||
                for agent in online:
 | 
			
		||||
                    r = asyncio.run(agent.nats_cmd(nats_ping))
 | 
			
		||||
                    r = asyncio.run(agent.nats_cmd(nats_ping, timeout=1))
 | 
			
		||||
                    if r == "pong":
 | 
			
		||||
                        running_agent = agent
 | 
			
		||||
                        break
 | 
			
		||||
@@ -333,27 +370,27 @@ class Agent(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
        updates = list()
 | 
			
		||||
        if patch_policy.critical == "approve":
 | 
			
		||||
            updates += self.winupdates.filter(
 | 
			
		||||
            updates += self.winupdates.filter(  # type: ignore
 | 
			
		||||
                severity="Critical", installed=False
 | 
			
		||||
            ).exclude(action="approve")
 | 
			
		||||
 | 
			
		||||
        if patch_policy.important == "approve":
 | 
			
		||||
            updates += self.winupdates.filter(
 | 
			
		||||
            updates += self.winupdates.filter(  # type: ignore
 | 
			
		||||
                severity="Important", installed=False
 | 
			
		||||
            ).exclude(action="approve")
 | 
			
		||||
 | 
			
		||||
        if patch_policy.moderate == "approve":
 | 
			
		||||
            updates += self.winupdates.filter(
 | 
			
		||||
            updates += self.winupdates.filter(  # type: ignore
 | 
			
		||||
                severity="Moderate", installed=False
 | 
			
		||||
            ).exclude(action="approve")
 | 
			
		||||
 | 
			
		||||
        if patch_policy.low == "approve":
 | 
			
		||||
            updates += self.winupdates.filter(severity="Low", installed=False).exclude(
 | 
			
		||||
            updates += self.winupdates.filter(severity="Low", installed=False).exclude(  # type: ignore
 | 
			
		||||
                action="approve"
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        if patch_policy.other == "approve":
 | 
			
		||||
            updates += self.winupdates.filter(severity="", installed=False).exclude(
 | 
			
		||||
            updates += self.winupdates.filter(severity="", installed=False).exclude(  # type: ignore
 | 
			
		||||
                action="approve"
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
@@ -368,7 +405,7 @@ class Agent(BaseAuditModel):
 | 
			
		||||
        site = self.site
 | 
			
		||||
        core_settings = CoreSettings.objects.first()
 | 
			
		||||
        patch_policy = None
 | 
			
		||||
        agent_policy = self.winupdatepolicy.get()
 | 
			
		||||
        agent_policy = self.winupdatepolicy.get()  # type: ignore
 | 
			
		||||
 | 
			
		||||
        if self.monitoring_type == "server":
 | 
			
		||||
            # check agent policy first which should override client or site policy
 | 
			
		||||
@@ -453,16 +490,16 @@ class Agent(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
        return patch_policy
 | 
			
		||||
 | 
			
		||||
    def get_approved_update_guids(self) -> List[str]:
 | 
			
		||||
    def get_approved_update_guids(self) -> list[str]:
 | 
			
		||||
        return list(
 | 
			
		||||
            self.winupdates.filter(action="approve", installed=False).values_list(
 | 
			
		||||
            self.winupdates.filter(action="approve", installed=False).values_list(  # type: ignore
 | 
			
		||||
                "guid", flat=True
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    # returns alert template assigned in the following order: policy, site, client, global
 | 
			
		||||
    # will return None if nothing is found
 | 
			
		||||
    def get_alert_template(self) -> Union[AlertTemplate, None]:
 | 
			
		||||
    # sets alert template assigned in the following order: policy, site, client, global
 | 
			
		||||
    # sets None if nothing is found
 | 
			
		||||
    def set_alert_template(self):
 | 
			
		||||
 | 
			
		||||
        site = self.site
 | 
			
		||||
        client = self.client
 | 
			
		||||
@@ -562,16 +599,23 @@ class Agent(BaseAuditModel):
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            else:
 | 
			
		||||
                # save alert_template to agent cache field
 | 
			
		||||
                self.alert_template = template
 | 
			
		||||
                self.save()
 | 
			
		||||
 | 
			
		||||
                return template
 | 
			
		||||
 | 
			
		||||
        # no alert templates found or agent has been excluded
 | 
			
		||||
        self.alert_template = None
 | 
			
		||||
        self.save()
 | 
			
		||||
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    def generate_checks_from_policies(self):
 | 
			
		||||
        from automation.models import Policy
 | 
			
		||||
 | 
			
		||||
        # Clear agent checks that have overriden_by_policy set
 | 
			
		||||
        self.agentchecks.update(overriden_by_policy=False)
 | 
			
		||||
        self.agentchecks.update(overriden_by_policy=False)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # Generate checks based on policies
 | 
			
		||||
        Policy.generate_policy_checks(self)
 | 
			
		||||
@@ -606,7 +650,7 @@ class Agent(BaseAuditModel):
 | 
			
		||||
        except Exception:
 | 
			
		||||
            return "err"
 | 
			
		||||
 | 
			
		||||
    async def nats_cmd(self, data, timeout=30, wait=True):
 | 
			
		||||
    async def nats_cmd(self, data: dict, timeout: int = 30, wait: bool = True):
 | 
			
		||||
        nc = NATS()
 | 
			
		||||
        options = {
 | 
			
		||||
            "servers": f"tls://{settings.ALLOWED_HOSTS[0]}:4222",
 | 
			
		||||
@@ -628,7 +672,11 @@ class Agent(BaseAuditModel):
 | 
			
		||||
            except ErrTimeout:
 | 
			
		||||
                ret = "timeout"
 | 
			
		||||
            else:
 | 
			
		||||
                ret = msgpack.loads(msg.data)
 | 
			
		||||
                try:
 | 
			
		||||
                    ret = msgpack.loads(msg.data)  # type: ignore
 | 
			
		||||
                except Exception as e:
 | 
			
		||||
                    logger.error(e)
 | 
			
		||||
                    ret = str(e)
 | 
			
		||||
 | 
			
		||||
            await nc.close()
 | 
			
		||||
            return ret
 | 
			
		||||
@@ -650,12 +698,12 @@ class Agent(BaseAuditModel):
 | 
			
		||||
    def delete_superseded_updates(self):
 | 
			
		||||
        try:
 | 
			
		||||
            pks = []  # list of pks to delete
 | 
			
		||||
            kbs = list(self.winupdates.values_list("kb", flat=True))
 | 
			
		||||
            kbs = list(self.winupdates.values_list("kb", flat=True))  # type: ignore
 | 
			
		||||
            d = Counter(kbs)
 | 
			
		||||
            dupes = [k for k, v in d.items() if v > 1]
 | 
			
		||||
 | 
			
		||||
            for dupe in dupes:
 | 
			
		||||
                titles = self.winupdates.filter(kb=dupe).values_list("title", flat=True)
 | 
			
		||||
                titles = self.winupdates.filter(kb=dupe).values_list("title", flat=True)  # type: ignore
 | 
			
		||||
                # extract the version from the title and sort from oldest to newest
 | 
			
		||||
                # skip if no version info is available therefore nothing to parse
 | 
			
		||||
                try:
 | 
			
		||||
@@ -668,17 +716,17 @@ class Agent(BaseAuditModel):
 | 
			
		||||
                    continue
 | 
			
		||||
                # append all but the latest version to our list of pks to delete
 | 
			
		||||
                for ver in sorted_vers[:-1]:
 | 
			
		||||
                    q = self.winupdates.filter(kb=dupe).filter(title__contains=ver)
 | 
			
		||||
                    q = self.winupdates.filter(kb=dupe).filter(title__contains=ver)  # type: ignore
 | 
			
		||||
                    pks.append(q.first().pk)
 | 
			
		||||
 | 
			
		||||
            pks = list(set(pks))
 | 
			
		||||
            self.winupdates.filter(pk__in=pks).delete()
 | 
			
		||||
            self.winupdates.filter(pk__in=pks).delete()  # type: ignore
 | 
			
		||||
        except:
 | 
			
		||||
            pass
 | 
			
		||||
 | 
			
		||||
    # define how the agent should handle pending actions
 | 
			
		||||
    def handle_pending_actions(self):
 | 
			
		||||
        pending_actions = self.pendingactions.filter(status="pending")
 | 
			
		||||
        pending_actions = self.pendingactions.filter(status="pending")  # type: ignore
 | 
			
		||||
 | 
			
		||||
        for action in pending_actions:
 | 
			
		||||
            if action.action_type == "taskaction":
 | 
			
		||||
@@ -702,160 +750,29 @@ class Agent(BaseAuditModel):
 | 
			
		||||
    # for clearing duplicate pending actions on agent
 | 
			
		||||
    def remove_matching_pending_task_actions(self, task_id):
 | 
			
		||||
        # remove any other pending actions on agent with same task_id
 | 
			
		||||
        for action in self.pendingactions.exclude(status="completed"):
 | 
			
		||||
        for action in self.pendingactions.filter(action_type="taskaction").exclude(status="completed"):  # type: ignore
 | 
			
		||||
            if action.details["task_id"] == task_id:
 | 
			
		||||
                action.delete()
 | 
			
		||||
 | 
			
		||||
    def handle_alert(self, checkin: bool = False) -> None:
 | 
			
		||||
        from agents.tasks import (
 | 
			
		||||
            agent_outage_email_task,
 | 
			
		||||
            agent_outage_sms_task,
 | 
			
		||||
            agent_recovery_email_task,
 | 
			
		||||
            agent_recovery_sms_task,
 | 
			
		||||
        )
 | 
			
		||||
        from alerts.models import Alert
 | 
			
		||||
 | 
			
		||||
        # return if agent is in maintenace mode
 | 
			
		||||
        if self.maintenance_mode:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        alert_template = self.get_alert_template()
 | 
			
		||||
 | 
			
		||||
        # called when agent is back online
 | 
			
		||||
        if checkin:
 | 
			
		||||
            if Alert.objects.filter(agent=self, resolved=False).exists():
 | 
			
		||||
 | 
			
		||||
                # resolve alert if exists
 | 
			
		||||
                alert = Alert.objects.get(agent=self, resolved=False)
 | 
			
		||||
                alert.resolve()
 | 
			
		||||
 | 
			
		||||
                # check if a resolved notification should be emailed
 | 
			
		||||
                if (
 | 
			
		||||
                    not alert.resolved_email_sent
 | 
			
		||||
                    and alert_template
 | 
			
		||||
                    and alert_template.agent_email_on_resolved
 | 
			
		||||
    def should_create_alert(self, alert_template=None):
 | 
			
		||||
        return (
 | 
			
		||||
            self.overdue_dashboard_alert
 | 
			
		||||
            or self.overdue_email_alert
 | 
			
		||||
                ):
 | 
			
		||||
                    agent_recovery_email_task.delay(pk=alert.pk)
 | 
			
		||||
 | 
			
		||||
                # check if a resolved notification should be texted
 | 
			
		||||
                if (
 | 
			
		||||
                    not alert.resolved_sms_sent
 | 
			
		||||
                    and alert_template
 | 
			
		||||
                    and alert_template.agent_text_on_resolved
 | 
			
		||||
            or self.overdue_text_alert
 | 
			
		||||
                ):
 | 
			
		||||
                    agent_recovery_sms_task.delay(pk=alert.pk)
 | 
			
		||||
 | 
			
		||||
                # check if any scripts should be run
 | 
			
		||||
                if (
 | 
			
		||||
                    not alert.resolved_action_run
 | 
			
		||||
                    and alert_template
 | 
			
		||||
                    and alert_template.resolved_action
 | 
			
		||||
                ):
 | 
			
		||||
                    r = self.run_script(
 | 
			
		||||
                        scriptpk=alert_template.resolved_action.pk,
 | 
			
		||||
                        args=alert_template.resolved_action_args,
 | 
			
		||||
                        timeout=alert_template.resolved_action_timeout,
 | 
			
		||||
                        wait=True,
 | 
			
		||||
                        full=True,
 | 
			
		||||
                        run_on_any=True,
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
                    # command was successful
 | 
			
		||||
                    if type(r) == dict:
 | 
			
		||||
                        alert.resolved_action_retcode = r["retcode"]
 | 
			
		||||
                        alert.resolved_action_stdout = r["stdout"]
 | 
			
		||||
                        alert.resolved_action_stderr = r["stderr"]
 | 
			
		||||
                        alert.resolved_action_execution_time = "{:.4f}".format(
 | 
			
		||||
                            r["execution_time"]
 | 
			
		||||
                        )
 | 
			
		||||
                        alert.resolved_action_run = djangotime.now()
 | 
			
		||||
                        alert.save()
 | 
			
		||||
                    else:
 | 
			
		||||
                        logger.error(
 | 
			
		||||
                            f"Resolved action: {alert_template.resolved_action} failed to run on any agent for {self.hostname} resolved outage"
 | 
			
		||||
                        )
 | 
			
		||||
 | 
			
		||||
        # called when agent is offline
 | 
			
		||||
        else:
 | 
			
		||||
            # check if alert hasn't been created yet so create it
 | 
			
		||||
            if not Alert.objects.filter(agent=self, resolved=False).exists():
 | 
			
		||||
 | 
			
		||||
                alert = Alert.create_availability_alert(self)
 | 
			
		||||
 | 
			
		||||
                # add a null check history to allow gaps in graph
 | 
			
		||||
                for check in self.agentchecks.all():
 | 
			
		||||
                    check.add_check_history(None)
 | 
			
		||||
            else:
 | 
			
		||||
                alert = Alert.objects.get(agent=self, resolved=False)
 | 
			
		||||
 | 
			
		||||
            # create dashboard alert if enabled
 | 
			
		||||
            if (
 | 
			
		||||
            or (
 | 
			
		||||
                alert_template
 | 
			
		||||
                and alert_template.agent_always_alert
 | 
			
		||||
                or self.overdue_dashboard_alert
 | 
			
		||||
            ):
 | 
			
		||||
                alert.hidden = False
 | 
			
		||||
                alert.save()
 | 
			
		||||
 | 
			
		||||
            # send email alert if enabled
 | 
			
		||||
            if (
 | 
			
		||||
                not alert.email_sent
 | 
			
		||||
                and alert_template
 | 
			
		||||
                and alert_template.agent_always_email
 | 
			
		||||
                or self.overdue_email_alert
 | 
			
		||||
            ):
 | 
			
		||||
                agent_outage_email_task.delay(
 | 
			
		||||
                    pk=alert.pk,
 | 
			
		||||
                    alert_interval=alert_template.check_periodic_alert_days
 | 
			
		||||
                    if alert_template
 | 
			
		||||
                    else None,
 | 
			
		||||
                and (
 | 
			
		||||
                    alert_template.agent_always_alert
 | 
			
		||||
                    or alert_template.agent_always_email
 | 
			
		||||
                    or alert_template.agent_always_text
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            # send text message if enabled
 | 
			
		||||
            if (
 | 
			
		||||
                not alert.sms_sent
 | 
			
		||||
                and alert_template
 | 
			
		||||
                and alert_template.agent_always_text
 | 
			
		||||
                or self.overdue_text_alert
 | 
			
		||||
            ):
 | 
			
		||||
                agent_outage_sms_task.delay(
 | 
			
		||||
                    pk=alert.pk,
 | 
			
		||||
                    alert_interval=alert_template.check_periodic_alert_days
 | 
			
		||||
                    if alert_template
 | 
			
		||||
                    else None,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            # check if any scripts should be run
 | 
			
		||||
            if not alert.action_run and alert_template and alert_template.action:
 | 
			
		||||
                r = self.run_script(
 | 
			
		||||
                    scriptpk=alert_template.action.pk,
 | 
			
		||||
                    args=alert_template.action_args,
 | 
			
		||||
                    timeout=alert_template.action_timeout,
 | 
			
		||||
                    wait=True,
 | 
			
		||||
                    full=True,
 | 
			
		||||
                    run_on_any=True,
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
                # command was successful
 | 
			
		||||
                if isinstance(r, dict):
 | 
			
		||||
                    alert.action_retcode = r["retcode"]
 | 
			
		||||
                    alert.action_stdout = r["stdout"]
 | 
			
		||||
                    alert.action_stderr = r["stderr"]
 | 
			
		||||
                    alert.action_execution_time = "{:.4f}".format(r["execution_time"])
 | 
			
		||||
                    alert.action_run = djangotime.now()
 | 
			
		||||
                    alert.save()
 | 
			
		||||
                else:
 | 
			
		||||
                    logger.error(
 | 
			
		||||
                        f"Failure action: {alert_template.action.name} failed to run on any agent for {self.hostname} outage"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def send_outage_email(self):
 | 
			
		||||
        from core.models import CoreSettings
 | 
			
		||||
 | 
			
		||||
        CORE = CoreSettings.objects.first()
 | 
			
		||||
        alert_template = self.get_alert_template()
 | 
			
		||||
        CORE.send_mail(
 | 
			
		||||
            f"{self.client.name}, {self.site.name}, {self.hostname} - data overdue",
 | 
			
		||||
            (
 | 
			
		||||
@@ -864,14 +781,13 @@ class Agent(BaseAuditModel):
 | 
			
		||||
                f"agent {self.hostname} "
 | 
			
		||||
                "within the expected time."
 | 
			
		||||
            ),
 | 
			
		||||
            alert_template=alert_template,
 | 
			
		||||
            alert_template=self.alert_template,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def send_recovery_email(self):
 | 
			
		||||
        from core.models import CoreSettings
 | 
			
		||||
 | 
			
		||||
        CORE = CoreSettings.objects.first()
 | 
			
		||||
        alert_template = self.get_alert_template()
 | 
			
		||||
        CORE.send_mail(
 | 
			
		||||
            f"{self.client.name}, {self.site.name}, {self.hostname} - data received",
 | 
			
		||||
            (
 | 
			
		||||
@@ -880,27 +796,25 @@ class Agent(BaseAuditModel):
 | 
			
		||||
                f"agent {self.hostname} "
 | 
			
		||||
                "after an interruption in data transmission."
 | 
			
		||||
            ),
 | 
			
		||||
            alert_template=alert_template,
 | 
			
		||||
            alert_template=self.alert_template,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def send_outage_sms(self):
 | 
			
		||||
        from core.models import CoreSettings
 | 
			
		||||
 | 
			
		||||
        alert_template = self.get_alert_template()
 | 
			
		||||
        CORE = CoreSettings.objects.first()
 | 
			
		||||
        CORE.send_sms(
 | 
			
		||||
            f"{self.client.name}, {self.site.name}, {self.hostname} - data overdue",
 | 
			
		||||
            alert_template=alert_template,
 | 
			
		||||
            alert_template=self.alert_template,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def send_recovery_sms(self):
 | 
			
		||||
        from core.models import CoreSettings
 | 
			
		||||
 | 
			
		||||
        CORE = CoreSettings.objects.first()
 | 
			
		||||
        alert_template = self.get_alert_template()
 | 
			
		||||
        CORE.send_sms(
 | 
			
		||||
            f"{self.client.name}, {self.site.name}, {self.hostname} - data received",
 | 
			
		||||
            alert_template=alert_template,
 | 
			
		||||
            alert_template=self.alert_template,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -926,12 +840,6 @@ class RecoveryAction(models.Model):
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return f"{self.agent.hostname} - {self.mode}"
 | 
			
		||||
 | 
			
		||||
    def send(self):
 | 
			
		||||
        ret = {"recovery": self.mode}
 | 
			
		||||
        if self.mode == "command":
 | 
			
		||||
            ret["cmd"] = self.command
 | 
			
		||||
        return ret
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Note(models.Model):
 | 
			
		||||
    agent = models.ForeignKey(
 | 
			
		||||
@@ -951,3 +859,38 @@ class Note(models.Model):
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.agent.hostname
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AgentCustomField(models.Model):
 | 
			
		||||
    agent = models.ForeignKey(
 | 
			
		||||
        Agent,
 | 
			
		||||
        related_name="custom_fields",
 | 
			
		||||
        on_delete=models.CASCADE,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    field = models.ForeignKey(
 | 
			
		||||
        "core.CustomField",
 | 
			
		||||
        related_name="agent_fields",
 | 
			
		||||
        on_delete=models.CASCADE,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    string_value = models.TextField(null=True, blank=True)
 | 
			
		||||
    bool_value = models.BooleanField(blank=True, default=False)
 | 
			
		||||
    multiple_value = ArrayField(
 | 
			
		||||
        models.TextField(null=True, blank=True),
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        default=list,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.field
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def value(self):
 | 
			
		||||
        if self.field.type == "multiple":
 | 
			
		||||
            return self.multiple_value
 | 
			
		||||
        elif self.field.type == "checkbox":
 | 
			
		||||
            return self.bool_value
 | 
			
		||||
        else:
 | 
			
		||||
            return self.string_value
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ from rest_framework import serializers
 | 
			
		||||
from clients.serializers import ClientSerializer
 | 
			
		||||
from winupdate.serializers import WinUpdatePolicySerializer
 | 
			
		||||
 | 
			
		||||
from .models import Agent, Note
 | 
			
		||||
from .models import Agent, AgentCustomField, Note
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AgentSerializer(serializers.ModelSerializer):
 | 
			
		||||
@@ -16,6 +16,7 @@ class AgentSerializer(serializers.ModelSerializer):
 | 
			
		||||
    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()
 | 
			
		||||
@@ -57,16 +58,15 @@ class AgentTableSerializer(serializers.ModelSerializer):
 | 
			
		||||
    alert_template = serializers.SerializerMethodField()
 | 
			
		||||
 | 
			
		||||
    def get_alert_template(self, obj):
 | 
			
		||||
        alert_template = obj.get_alert_template()
 | 
			
		||||
 | 
			
		||||
        if not alert_template:
 | 
			
		||||
        if not obj.alert_template:
 | 
			
		||||
            return None
 | 
			
		||||
        else:
 | 
			
		||||
            return {
 | 
			
		||||
                "name": alert_template.name,
 | 
			
		||||
                "always_email": alert_template.agent_always_email,
 | 
			
		||||
                "always_text": alert_template.agent_always_text,
 | 
			
		||||
                "always_alert": alert_template.agent_always_alert,
 | 
			
		||||
                "name": obj.alert_template.name,
 | 
			
		||||
                "always_email": obj.alert_template.agent_always_email,
 | 
			
		||||
                "always_text": obj.alert_template.agent_always_text,
 | 
			
		||||
                "always_alert": obj.alert_template.agent_always_alert,
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
    def get_pending_actions(self, obj):
 | 
			
		||||
@@ -120,10 +120,30 @@ class AgentTableSerializer(serializers.ModelSerializer):
 | 
			
		||||
        depth = 2
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AgentCustomFieldSerializer(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = AgentCustomField
 | 
			
		||||
        fields = (
 | 
			
		||||
            "id",
 | 
			
		||||
            "field",
 | 
			
		||||
            "agent",
 | 
			
		||||
            "value",
 | 
			
		||||
            "string_value",
 | 
			
		||||
            "bool_value",
 | 
			
		||||
            "multiple_value",
 | 
			
		||||
        )
 | 
			
		||||
        extra_kwargs = {
 | 
			
		||||
            "string_value": {"write_only": True},
 | 
			
		||||
            "bool_value": {"write_only": True},
 | 
			
		||||
            "multiple_value": {"write_only": True},
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AgentEditSerializer(serializers.ModelSerializer):
 | 
			
		||||
    winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True)
 | 
			
		||||
    all_timezones = serializers.SerializerMethodField()
 | 
			
		||||
    client = ClientSerializer(read_only=True)
 | 
			
		||||
    custom_fields = AgentCustomFieldSerializer(many=True, read_only=True)
 | 
			
		||||
 | 
			
		||||
    def get_all_timezones(self, obj):
 | 
			
		||||
        return pytz.all_timezones
 | 
			
		||||
@@ -147,6 +167,7 @@ class AgentEditSerializer(serializers.ModelSerializer):
 | 
			
		||||
            "all_timezones",
 | 
			
		||||
            "winupdatepolicy",
 | 
			
		||||
            "policy",
 | 
			
		||||
            "custom_fields",
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@ import asyncio
 | 
			
		||||
import datetime as dt
 | 
			
		||||
import random
 | 
			
		||||
from time import sleep
 | 
			
		||||
from typing import List, Union
 | 
			
		||||
from typing import Union
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
@@ -14,6 +14,7 @@ from core.models import CoreSettings
 | 
			
		||||
from logs.models import PendingAction
 | 
			
		||||
from scripts.models import Script
 | 
			
		||||
from tacticalrmm.celery import app
 | 
			
		||||
from tacticalrmm.utils import run_nats_api_cmd
 | 
			
		||||
 | 
			
		||||
logger.configure(**settings.LOG_CONFIG)
 | 
			
		||||
 | 
			
		||||
@@ -77,7 +78,7 @@ def agent_update(pk: int) -> str:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def send_agent_update_task(pks: List[int]) -> None:
 | 
			
		||||
def send_agent_update_task(pks: list[int]) -> None:
 | 
			
		||||
    chunks = (pks[i : i + 30] for i in range(0, len(pks), 30))
 | 
			
		||||
    for chunk in chunks:
 | 
			
		||||
        for pk in chunk:
 | 
			
		||||
@@ -93,7 +94,7 @@ def auto_self_agent_update_task() -> None:
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    q = Agent.objects.only("pk", "version")
 | 
			
		||||
    pks: List[int] = [
 | 
			
		||||
    pks: list[int] = [
 | 
			
		||||
        i.pk
 | 
			
		||||
        for i in q
 | 
			
		||||
        if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
 | 
			
		||||
@@ -183,6 +184,8 @@ def agent_recovery_sms_task(pk: int) -> str:
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def agent_outages_task() -> None:
 | 
			
		||||
    from alerts.models import Alert
 | 
			
		||||
 | 
			
		||||
    agents = Agent.objects.only(
 | 
			
		||||
        "pk",
 | 
			
		||||
        "last_seen",
 | 
			
		||||
@@ -195,30 +198,22 @@ def agent_outages_task() -> None:
 | 
			
		||||
 | 
			
		||||
    for agent in agents:
 | 
			
		||||
        if agent.status == "overdue":
 | 
			
		||||
            agent.handle_alert()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@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))
 | 
			
		||||
            Alert.handle_alert_failure(agent)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def run_script_email_results_task(
 | 
			
		||||
    agentpk: int, scriptpk: int, nats_timeout: int, emails: List[str]
 | 
			
		||||
    agentpk: int,
 | 
			
		||||
    scriptpk: int,
 | 
			
		||||
    nats_timeout: int,
 | 
			
		||||
    emails: list[str],
 | 
			
		||||
    args: list[str] = [],
 | 
			
		||||
):
 | 
			
		||||
    agent = Agent.objects.get(pk=agentpk)
 | 
			
		||||
    script = Script.objects.get(pk=scriptpk)
 | 
			
		||||
    r = agent.run_script(scriptpk=script.pk, full=True, timeout=nats_timeout, wait=True)
 | 
			
		||||
    r = agent.run_script(
 | 
			
		||||
        scriptpk=script.pk, args=args, full=True, timeout=nats_timeout, wait=True
 | 
			
		||||
    )
 | 
			
		||||
    if r == "timeout":
 | 
			
		||||
        logger.error(f"{agent.hostname} timed out running script.")
 | 
			
		||||
        return
 | 
			
		||||
@@ -258,3 +253,21 @@ def run_script_email_results_task(
 | 
			
		||||
                server.quit()
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        logger.error(e)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def monitor_agents_task() -> None:
 | 
			
		||||
    agents = Agent.objects.only(
 | 
			
		||||
        "pk", "agent_id", "last_seen", "overdue_time", "offline_time"
 | 
			
		||||
    )
 | 
			
		||||
    ids = [i.agent_id for i in agents if i.status != "online"]
 | 
			
		||||
    run_nats_api_cmd("monitor", ids)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def get_wmi_task() -> None:
 | 
			
		||||
    agents = Agent.objects.only(
 | 
			
		||||
        "pk", "agent_id", "last_seen", "overdue_time", "offline_time"
 | 
			
		||||
    )
 | 
			
		||||
    ids = [i.agent_id for i in agents if i.status == "online"]
 | 
			
		||||
    run_nats_api_cmd("wmi", ids)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
import json
 | 
			
		||||
import os
 | 
			
		||||
from itertools import cycle
 | 
			
		||||
from typing import List
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
@@ -13,11 +12,68 @@ from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
from winupdate.models import WinUpdatePolicy
 | 
			
		||||
from winupdate.serializers import WinUpdatePolicySerializer
 | 
			
		||||
 | 
			
		||||
from .models import Agent
 | 
			
		||||
from .models import Agent, AgentCustomField
 | 
			
		||||
from .serializers import AgentSerializer
 | 
			
		||||
from .tasks import auto_self_agent_update_task
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestAgentsList(TacticalTestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        self.setup_coresettings()
 | 
			
		||||
 | 
			
		||||
    def test_agents_list(self):
 | 
			
		||||
        url = "/agents/listagents/"
 | 
			
		||||
 | 
			
		||||
        # 36 total agents
 | 
			
		||||
        company1 = baker.make("clients.Client")
 | 
			
		||||
        company2 = baker.make("clients.Client")
 | 
			
		||||
        site1 = baker.make("clients.Site", client=company1)
 | 
			
		||||
        site2 = baker.make("clients.Site", client=company1)
 | 
			
		||||
        site3 = baker.make("clients.Site", client=company2)
 | 
			
		||||
 | 
			
		||||
        baker.make_recipe(
 | 
			
		||||
            "agents.online_agent", site=site1, monitoring_type="server", _quantity=15
 | 
			
		||||
        )
 | 
			
		||||
        baker.make_recipe(
 | 
			
		||||
            "agents.online_agent",
 | 
			
		||||
            site=site2,
 | 
			
		||||
            monitoring_type="workstation",
 | 
			
		||||
            _quantity=10,
 | 
			
		||||
        )
 | 
			
		||||
        baker.make_recipe(
 | 
			
		||||
            "agents.online_agent",
 | 
			
		||||
            site=site3,
 | 
			
		||||
            monitoring_type="server",
 | 
			
		||||
            _quantity=4,
 | 
			
		||||
        )
 | 
			
		||||
        baker.make_recipe(
 | 
			
		||||
            "agents.online_agent",
 | 
			
		||||
            site=site3,
 | 
			
		||||
            monitoring_type="workstation",
 | 
			
		||||
            _quantity=7,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # test all agents
 | 
			
		||||
        r = self.client.patch(url, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(len(r.data), 36)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # test client1
 | 
			
		||||
        data = {"clientPK": company1.pk}  # type: ignore
 | 
			
		||||
        r = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(len(r.data), 25)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # test site3
 | 
			
		||||
        data = {"sitePK": site3.pk}  # type: ignore
 | 
			
		||||
        r = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(len(r.data), 11)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("patch", url)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestAgentViews(TacticalTestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
@@ -78,12 +134,12 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
            _quantity=15,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        pks: List[int] = list(
 | 
			
		||||
        pks: list[int] = list(
 | 
			
		||||
            Agent.objects.only("pk", "version").values_list("pk", flat=True)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        data = {"pks": pks}
 | 
			
		||||
        expected: List[int] = [
 | 
			
		||||
        expected: list[int] = [
 | 
			
		||||
            i.pk
 | 
			
		||||
            for i in Agent.objects.only("pk", "version")
 | 
			
		||||
            if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
 | 
			
		||||
@@ -142,11 +198,6 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
    @patch("agents.models.Agent.nats_cmd")
 | 
			
		||||
    def test_get_processes(self, mock_ret):
 | 
			
		||||
        agent_old = baker.make_recipe("agents.online_agent", version="1.1.12")
 | 
			
		||||
        url_old = f"/agents/{agent_old.pk}/getprocs/"
 | 
			
		||||
        r = self.client.get(url_old)
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
 | 
			
		||||
        agent = baker.make_recipe("agents.online_agent", version="1.2.0")
 | 
			
		||||
        url = f"/agents/{agent.pk}/getprocs/"
 | 
			
		||||
 | 
			
		||||
@@ -257,7 +308,7 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
        mock_ret.return_value = "nt authority\system"
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertIsInstance(r.data, str)
 | 
			
		||||
        self.assertIsInstance(r.data, str)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        mock_ret.return_value = "timeout"
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
@@ -277,15 +328,16 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
        nats_cmd.return_value = "ok"
 | 
			
		||||
        r = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(r.data["time"], "August 29, 2025 at 06:41 PM")
 | 
			
		||||
        self.assertEqual(r.data["agent"], self.agent.hostname)
 | 
			
		||||
        self.assertEqual(r.data["time"], "August 29, 2025 at 06:41 PM")  # type: ignore
 | 
			
		||||
        self.assertEqual(r.data["agent"], self.agent.hostname)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        nats_data = {
 | 
			
		||||
            "func": "schedtask",
 | 
			
		||||
            "schedtaskpayload": {
 | 
			
		||||
                "type": "schedreboot",
 | 
			
		||||
                "deleteafter": True,
 | 
			
		||||
                "trigger": "once",
 | 
			
		||||
                "name": r.data["task_name"],
 | 
			
		||||
                "name": r.data["task_name"],  # type: ignore
 | 
			
		||||
                "year": 2025,
 | 
			
		||||
                "month": "August",
 | 
			
		||||
                "day": 29,
 | 
			
		||||
@@ -306,53 +358,43 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
        r = self.client.patch(url, data_invalid, format="json")
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
        self.assertEqual(r.data, "Invalid date")
 | 
			
		||||
        self.assertEqual(r.data, "Invalid date")  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("patch", url)
 | 
			
		||||
 | 
			
		||||
    @patch("os.path.exists")
 | 
			
		||||
    @patch("subprocess.run")
 | 
			
		||||
    def test_install_agent(self, mock_subprocess, mock_file_exists):
 | 
			
		||||
        url = f"/agents/installagent/"
 | 
			
		||||
    def test_install_agent(self, mock_file_exists):
 | 
			
		||||
        url = "/agents/installagent/"
 | 
			
		||||
 | 
			
		||||
        site = baker.make("clients.Site")
 | 
			
		||||
        data = {
 | 
			
		||||
            "client": site.client.id,
 | 
			
		||||
            "site": site.id,
 | 
			
		||||
            "client": site.client.id,  # type: ignore
 | 
			
		||||
            "site": site.id,  # type: ignore
 | 
			
		||||
            "arch": "64",
 | 
			
		||||
            "expires": 23,
 | 
			
		||||
            "installMethod": "exe",
 | 
			
		||||
            "installMethod": "manual",
 | 
			
		||||
            "api": "https://api.example.com",
 | 
			
		||||
            "agenttype": "server",
 | 
			
		||||
            "rdp": 1,
 | 
			
		||||
            "ping": 0,
 | 
			
		||||
            "power": 0,
 | 
			
		||||
            "fileName": "rmm-client-site-server.exe",
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        mock_file_exists.return_value = False
 | 
			
		||||
        mock_subprocess.return_value.returncode = 0
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 406)
 | 
			
		||||
 | 
			
		||||
        mock_file_exists.return_value = True
 | 
			
		||||
        mock_subprocess.return_value.returncode = 1
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 413)
 | 
			
		||||
 | 
			
		||||
        mock_file_exists.return_value = True
 | 
			
		||||
        mock_subprocess.return_value.returncode = 0
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        data["arch"] = "32"
 | 
			
		||||
        mock_subprocess.return_value.returncode = 0
 | 
			
		||||
        mock_file_exists.return_value = False
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 415)
 | 
			
		||||
 | 
			
		||||
        data["installMethod"] = "manual"
 | 
			
		||||
        data["arch"] = "64"
 | 
			
		||||
        mock_subprocess.return_value.returncode = 0
 | 
			
		||||
        mock_file_exists.return_value = True
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertIn("rdp", r.json()["cmd"])
 | 
			
		||||
@@ -363,52 +405,74 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
        self.assertIn("power", r.json()["cmd"])
 | 
			
		||||
        self.assertIn("ping", r.json()["cmd"])
 | 
			
		||||
 | 
			
		||||
        data["installMethod"] = "powershell"
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
    def test_recover(self):
 | 
			
		||||
    @patch("agents.models.Agent.nats_cmd")
 | 
			
		||||
    def test_recover(self, nats_cmd):
 | 
			
		||||
        from agents.models import RecoveryAction
 | 
			
		||||
 | 
			
		||||
        self.agent.version = "0.11.1"
 | 
			
		||||
        self.agent.save(update_fields=["version"])
 | 
			
		||||
        RecoveryAction.objects.all().delete()
 | 
			
		||||
        url = "/agents/recover/"
 | 
			
		||||
        data = {"pk": self.agent.pk, "cmd": None, "mode": "mesh"}
 | 
			
		||||
        agent = baker.make_recipe("agents.online_agent")
 | 
			
		||||
 | 
			
		||||
        # test mesh realtime
 | 
			
		||||
        data = {"pk": agent.pk, "cmd": None, "mode": "mesh"}
 | 
			
		||||
        nats_cmd.return_value = "ok"
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(RecoveryAction.objects.count(), 0)
 | 
			
		||||
        nats_cmd.assert_called_with(
 | 
			
		||||
            {"func": "recover", "payload": {"mode": "mesh"}}, timeout=10
 | 
			
		||||
        )
 | 
			
		||||
        nats_cmd.reset_mock()
 | 
			
		||||
 | 
			
		||||
        data["mode"] = "mesh"
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
        self.assertIn("pending", r.json())
 | 
			
		||||
 | 
			
		||||
        RecoveryAction.objects.all().delete()
 | 
			
		||||
        data["mode"] = "command"
 | 
			
		||||
        data["cmd"] = "ipconfig /flushdns"
 | 
			
		||||
        # test mesh with agent rpc not working
 | 
			
		||||
        data = {"pk": agent.pk, "cmd": None, "mode": "mesh"}
 | 
			
		||||
        nats_cmd.return_value = "timeout"
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        RecoveryAction.objects.all().delete()
 | 
			
		||||
        data["cmd"] = None
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(RecoveryAction.objects.count(), 1)
 | 
			
		||||
        mesh_recovery = RecoveryAction.objects.first()
 | 
			
		||||
        self.assertEqual(mesh_recovery.mode, "mesh")
 | 
			
		||||
        nats_cmd.reset_mock()
 | 
			
		||||
        RecoveryAction.objects.all().delete()
 | 
			
		||||
 | 
			
		||||
        self.agent.version = "0.9.4"
 | 
			
		||||
        self.agent.save(update_fields=["version"])
 | 
			
		||||
        data["mode"] = "mesh"
 | 
			
		||||
        # test tacagent realtime
 | 
			
		||||
        data = {"pk": agent.pk, "cmd": None, "mode": "tacagent"}
 | 
			
		||||
        nats_cmd.return_value = "ok"
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
        self.assertIn("0.9.5", r.json())
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
    def test_agents_list(self):
 | 
			
		||||
        url = "/agents/listagents/"
 | 
			
		||||
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(RecoveryAction.objects.count(), 0)
 | 
			
		||||
        nats_cmd.assert_called_with(
 | 
			
		||||
            {"func": "recover", "payload": {"mode": "tacagent"}}, timeout=10
 | 
			
		||||
        )
 | 
			
		||||
        nats_cmd.reset_mock()
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
        # test tacagent with rpc not working
 | 
			
		||||
        data = {"pk": agent.pk, "cmd": None, "mode": "tacagent"}
 | 
			
		||||
        nats_cmd.return_value = "timeout"
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
        self.assertEqual(RecoveryAction.objects.count(), 0)
 | 
			
		||||
        nats_cmd.reset_mock()
 | 
			
		||||
 | 
			
		||||
        # test shell cmd without command
 | 
			
		||||
        data = {"pk": agent.pk, "cmd": None, "mode": "command"}
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
        self.assertEqual(RecoveryAction.objects.count(), 0)
 | 
			
		||||
 | 
			
		||||
        # test shell cmd
 | 
			
		||||
        data = {"pk": agent.pk, "cmd": "shutdown /r /t 10 /f", "mode": "command"}
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(RecoveryAction.objects.count(), 1)
 | 
			
		||||
        cmd_recovery = RecoveryAction.objects.first()
 | 
			
		||||
        self.assertEqual(cmd_recovery.mode, "command")
 | 
			
		||||
        self.assertEqual(cmd_recovery.command, "shutdown /r /t 10 /f")
 | 
			
		||||
 | 
			
		||||
    def test_agents_agent_detail(self):
 | 
			
		||||
        url = f"/agents/{self.agent.pk}/agentdetail/"
 | 
			
		||||
@@ -426,7 +490,7 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        edit = {
 | 
			
		||||
            "id": self.agent.pk,
 | 
			
		||||
            "site": site.id,
 | 
			
		||||
            "site": site.id,  # type: ignore
 | 
			
		||||
            "monitoring_type": "workstation",
 | 
			
		||||
            "description": "asjdk234andasd",
 | 
			
		||||
            "offline_time": 4,
 | 
			
		||||
@@ -457,12 +521,41 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        agent = Agent.objects.get(pk=self.agent.pk)
 | 
			
		||||
        data = AgentSerializer(agent).data
 | 
			
		||||
        self.assertEqual(data["site"], site.id)
 | 
			
		||||
        self.assertEqual(data["site"], site.id)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        policy = WinUpdatePolicy.objects.get(agent=self.agent)
 | 
			
		||||
        data = WinUpdatePolicySerializer(policy).data
 | 
			
		||||
        self.assertEqual(data["run_time_days"], [2, 3, 6])
 | 
			
		||||
 | 
			
		||||
        # test adding custom fields
 | 
			
		||||
        field = baker.make("core.CustomField", model="agent", type="number")
 | 
			
		||||
        edit = {
 | 
			
		||||
            "id": self.agent.pk,
 | 
			
		||||
            "site": site.id,  # type: ignore
 | 
			
		||||
            "description": "asjdk234andasd",
 | 
			
		||||
            "custom_fields": [{"field": field.id, "string_value": "123"}],  # type: ignore
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        r = self.client.patch(url, edit, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertTrue(
 | 
			
		||||
            AgentCustomField.objects.filter(agent=self.agent, field=field).exists()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # test edit custom field
 | 
			
		||||
        edit = {
 | 
			
		||||
            "id": self.agent.pk,
 | 
			
		||||
            "site": site.id,  # type: ignore
 | 
			
		||||
            "description": "asjdk234andasd",
 | 
			
		||||
            "custom_fields": [{"field": field.id, "string_value": "456"}],  # type: ignore
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        r = self.client.patch(url, edit, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            AgentCustomField.objects.get(agent=agent, field=field).value,
 | 
			
		||||
            "456",
 | 
			
		||||
        )
 | 
			
		||||
        self.check_not_authenticated("patch", url)
 | 
			
		||||
 | 
			
		||||
    @patch("agents.models.Agent.get_login_token")
 | 
			
		||||
@@ -475,21 +568,21 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
        # TODO
 | 
			
		||||
        # decode the cookie
 | 
			
		||||
 | 
			
		||||
        self.assertIn("&viewmode=13", r.data["file"])
 | 
			
		||||
        self.assertIn("&viewmode=12", r.data["terminal"])
 | 
			
		||||
        self.assertIn("&viewmode=11", r.data["control"])
 | 
			
		||||
        self.assertIn("&viewmode=13", r.data["file"])  # type: ignore
 | 
			
		||||
        self.assertIn("&viewmode=12", r.data["terminal"])  # type: ignore
 | 
			
		||||
        self.assertIn("&viewmode=11", r.data["control"])  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.assertIn("&gotonode=", r.data["file"])
 | 
			
		||||
        self.assertIn("&gotonode=", r.data["terminal"])
 | 
			
		||||
        self.assertIn("&gotonode=", r.data["control"])
 | 
			
		||||
        self.assertIn("&gotonode=", r.data["file"])  # type: ignore
 | 
			
		||||
        self.assertIn("&gotonode=", r.data["terminal"])  # type: ignore
 | 
			
		||||
        self.assertIn("&gotonode=", r.data["control"])  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.assertIn("?login=", r.data["file"])
 | 
			
		||||
        self.assertIn("?login=", r.data["terminal"])
 | 
			
		||||
        self.assertIn("?login=", r.data["control"])
 | 
			
		||||
        self.assertIn("?login=", r.data["file"])  # type: ignore
 | 
			
		||||
        self.assertIn("?login=", r.data["terminal"])  # type: ignore
 | 
			
		||||
        self.assertIn("?login=", r.data["control"])  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(self.agent.hostname, r.data["hostname"])
 | 
			
		||||
        self.assertEqual(self.agent.client.name, r.data["client"])
 | 
			
		||||
        self.assertEqual(self.agent.site.name, r.data["site"])
 | 
			
		||||
        self.assertEqual(self.agent.hostname, r.data["hostname"])  # type: ignore
 | 
			
		||||
        self.assertEqual(self.agent.client.name, r.data["client"])  # type: ignore
 | 
			
		||||
        self.assertEqual(self.agent.site.name, r.data["site"])  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
@@ -499,32 +592,6 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    def test_by_client(self):
 | 
			
		||||
        url = f"/agents/byclient/{self.agent.client.id}/"
 | 
			
		||||
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertTrue(r.data)
 | 
			
		||||
 | 
			
		||||
        url = f"/agents/byclient/500/"
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertFalse(r.data)  # returns empty list
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    def test_by_site(self):
 | 
			
		||||
        url = f"/agents/bysite/{self.agent.site.id}/"
 | 
			
		||||
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertTrue(r.data)
 | 
			
		||||
 | 
			
		||||
        url = f"/agents/bysite/500/"
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.data, [])
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    def test_overdue_action(self):
 | 
			
		||||
        url = "/agents/overdueaction/"
 | 
			
		||||
 | 
			
		||||
@@ -533,14 +600,14 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        agent = Agent.objects.get(pk=self.agent.pk)
 | 
			
		||||
        self.assertTrue(agent.overdue_email_alert)
 | 
			
		||||
        self.assertEqual(self.agent.hostname, r.data)
 | 
			
		||||
        self.assertEqual(self.agent.hostname, r.data)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        payload = {"pk": self.agent.pk, "overdue_text_alert": False}
 | 
			
		||||
        r = self.client.post(url, payload, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        agent = Agent.objects.get(pk=self.agent.pk)
 | 
			
		||||
        self.assertFalse(agent.overdue_text_alert)
 | 
			
		||||
        self.assertEqual(self.agent.hostname, r.data)
 | 
			
		||||
        self.assertEqual(self.agent.hostname, r.data)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
@@ -684,7 +751,7 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
        nats_cmd.return_value = "ok"
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertIn(self.agent.hostname, r.data)
 | 
			
		||||
        self.assertIn(self.agent.hostname, r.data)  # type: ignore
 | 
			
		||||
        nats_cmd.assert_called_with(
 | 
			
		||||
            {"func": "recover", "payload": {"mode": "mesh"}}, timeout=45
 | 
			
		||||
        )
 | 
			
		||||
@@ -699,13 +766,84 @@ class TestAgentViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    @patch("agents.tasks.run_script_email_results_task.delay")
 | 
			
		||||
    @patch("agents.models.Agent.run_script")
 | 
			
		||||
    def test_run_script(self, run_script, email_task):
 | 
			
		||||
        run_script.return_value = "ok"
 | 
			
		||||
        url = "/agents/runscript/"
 | 
			
		||||
        script = baker.make_recipe("scripts.script")
 | 
			
		||||
 | 
			
		||||
        # test wait
 | 
			
		||||
        data = {
 | 
			
		||||
            "pk": self.agent.pk,
 | 
			
		||||
            "scriptPK": script.pk,
 | 
			
		||||
            "output": "wait",
 | 
			
		||||
            "args": [],
 | 
			
		||||
            "timeout": 15,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        run_script.assert_called_with(
 | 
			
		||||
            scriptpk=script.pk, args=[], timeout=18, wait=True
 | 
			
		||||
        )
 | 
			
		||||
        run_script.reset_mock()
 | 
			
		||||
 | 
			
		||||
        # test email default
 | 
			
		||||
        data = {
 | 
			
		||||
            "pk": self.agent.pk,
 | 
			
		||||
            "scriptPK": script.pk,
 | 
			
		||||
            "output": "email",
 | 
			
		||||
            "args": ["abc", "123"],
 | 
			
		||||
            "timeout": 15,
 | 
			
		||||
            "emailmode": "default",
 | 
			
		||||
            "emails": ["admin@example.com", "bob@example.com"],
 | 
			
		||||
        }
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        email_task.assert_called_with(
 | 
			
		||||
            agentpk=self.agent.pk,
 | 
			
		||||
            scriptpk=script.pk,
 | 
			
		||||
            nats_timeout=18,
 | 
			
		||||
            emails=[],
 | 
			
		||||
            args=["abc", "123"],
 | 
			
		||||
        )
 | 
			
		||||
        email_task.reset_mock()
 | 
			
		||||
 | 
			
		||||
        # test email overrides
 | 
			
		||||
        data["emailmode"] = "custom"
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        email_task.assert_called_with(
 | 
			
		||||
            agentpk=self.agent.pk,
 | 
			
		||||
            scriptpk=script.pk,
 | 
			
		||||
            nats_timeout=18,
 | 
			
		||||
            emails=["admin@example.com", "bob@example.com"],
 | 
			
		||||
            args=["abc", "123"],
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # test fire and forget
 | 
			
		||||
        data = {
 | 
			
		||||
            "pk": self.agent.pk,
 | 
			
		||||
            "scriptPK": script.pk,
 | 
			
		||||
            "output": "forget",
 | 
			
		||||
            "args": ["hello", "world"],
 | 
			
		||||
            "timeout": 22,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        run_script.assert_called_with(
 | 
			
		||||
            scriptpk=script.pk, args=["hello", "world"], timeout=25
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestAgentViewsNew(TacticalTestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        self.setup_coresettings()
 | 
			
		||||
 | 
			
		||||
    def test_agent_counts(self):
 | 
			
		||||
    """ def test_agent_counts(self):
 | 
			
		||||
        url = "/agents/agent_counts/"
 | 
			
		||||
 | 
			
		||||
        # create some data
 | 
			
		||||
@@ -730,9 +868,9 @@ class TestAgentViewsNew(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        r = self.client.post(url, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(r.data, data)
 | 
			
		||||
        self.assertEqual(r.data, data)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
        self.check_not_authenticated("post", url) """
 | 
			
		||||
 | 
			
		||||
    def test_agent_maintenance_mode(self):
 | 
			
		||||
        url = "/agents/maintenance/"
 | 
			
		||||
@@ -742,14 +880,14 @@ class TestAgentViewsNew(TacticalTestCase):
 | 
			
		||||
        agent = baker.make_recipe("agents.agent", site=site)
 | 
			
		||||
 | 
			
		||||
        # Test client toggle maintenance mode
 | 
			
		||||
        data = {"type": "Client", "id": site.client.id, "action": True}
 | 
			
		||||
        data = {"type": "Client", "id": site.client.id, "action": True}  # type: ignore
 | 
			
		||||
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertTrue(Agent.objects.get(pk=agent.pk).maintenance_mode)
 | 
			
		||||
 | 
			
		||||
        # Test site toggle maintenance mode
 | 
			
		||||
        data = {"type": "Site", "id": site.id, "action": False}
 | 
			
		||||
        data = {"type": "Site", "id": site.id, "action": False}  # type: ignore
 | 
			
		||||
 | 
			
		||||
        r = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 
 | 
			
		||||
@@ -6,8 +6,6 @@ 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),
 | 
			
		||||
@@ -29,7 +27,6 @@ urlpatterns = [
 | 
			
		||||
    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()),
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -3,15 +3,13 @@ import datetime as dt
 | 
			
		||||
import os
 | 
			
		||||
import random
 | 
			
		||||
import string
 | 
			
		||||
import subprocess
 | 
			
		||||
from typing import List
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.http import HttpResponse
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from loguru import logger
 | 
			
		||||
from packaging import version as pyver
 | 
			
		||||
from rest_framework import generics, status
 | 
			
		||||
from rest_framework import status
 | 
			
		||||
from rest_framework.decorators import api_view
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
@@ -24,8 +22,9 @@ from tacticalrmm.utils import get_default_timezone, notify_error, reload_nats
 | 
			
		||||
from winupdate.serializers import WinUpdatePolicySerializer
 | 
			
		||||
from winupdate.tasks import bulk_check_for_updates_task, bulk_install_updates_task
 | 
			
		||||
 | 
			
		||||
from .models import Agent, Note, RecoveryAction
 | 
			
		||||
from .models import Agent, AgentCustomField, Note, RecoveryAction
 | 
			
		||||
from .serializers import (
 | 
			
		||||
    AgentCustomFieldSerializer,
 | 
			
		||||
    AgentEditSerializer,
 | 
			
		||||
    AgentHostnameSerializer,
 | 
			
		||||
    AgentOverdueActionSerializer,
 | 
			
		||||
@@ -41,7 +40,7 @@ logger.configure(**settings.LOG_CONFIG)
 | 
			
		||||
 | 
			
		||||
@api_view()
 | 
			
		||||
def get_agent_versions(request):
 | 
			
		||||
    agents = Agent.objects.only("pk")
 | 
			
		||||
    agents = Agent.objects.prefetch_related("site").only("pk", "hostname")
 | 
			
		||||
    return Response(
 | 
			
		||||
        {
 | 
			
		||||
            "versions": [settings.LATEST_AGENT_VER],
 | 
			
		||||
@@ -53,7 +52,7 @@ def get_agent_versions(request):
 | 
			
		||||
@api_view(["POST"])
 | 
			
		||||
def update_agents(request):
 | 
			
		||||
    q = Agent.objects.filter(pk__in=request.data["pks"]).only("pk", "version")
 | 
			
		||||
    pks: List[int] = [
 | 
			
		||||
    pks: list[int] = [
 | 
			
		||||
        i.pk
 | 
			
		||||
        for i in q
 | 
			
		||||
        if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER)
 | 
			
		||||
@@ -66,7 +65,6 @@ def update_agents(request):
 | 
			
		||||
def ping(request, pk):
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=pk)
 | 
			
		||||
    status = "offline"
 | 
			
		||||
    if agent.has_nats:
 | 
			
		||||
    r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=5))
 | 
			
		||||
    if r == "pong":
 | 
			
		||||
        status = "online"
 | 
			
		||||
@@ -77,7 +75,6 @@ def ping(request, pk):
 | 
			
		||||
@api_view(["DELETE"])
 | 
			
		||||
def uninstall(request):
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=request.data["pk"])
 | 
			
		||||
    if agent.has_nats:
 | 
			
		||||
    asyncio.run(agent.nats_cmd({"func": "uninstall"}, wait=False))
 | 
			
		||||
 | 
			
		||||
    name = agent.hostname
 | 
			
		||||
@@ -86,7 +83,7 @@ def uninstall(request):
 | 
			
		||||
    return Response(f"{name} will now be uninstalled.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view(["PATCH"])
 | 
			
		||||
@api_view(["PATCH", "PUT"])
 | 
			
		||||
def edit_agent(request):
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=request.data["id"])
 | 
			
		||||
 | 
			
		||||
@@ -95,13 +92,36 @@ def edit_agent(request):
 | 
			
		||||
    a_serializer.save()
 | 
			
		||||
 | 
			
		||||
    if "winupdatepolicy" in request.data.keys():
 | 
			
		||||
        policy = agent.winupdatepolicy.get()
 | 
			
		||||
        policy = agent.winupdatepolicy.get()  # type: ignore
 | 
			
		||||
        p_serializer = WinUpdatePolicySerializer(
 | 
			
		||||
            instance=policy, data=request.data["winupdatepolicy"][0]
 | 
			
		||||
        )
 | 
			
		||||
        p_serializer.is_valid(raise_exception=True)
 | 
			
		||||
        p_serializer.save()
 | 
			
		||||
 | 
			
		||||
    if "custom_fields" in request.data.keys():
 | 
			
		||||
 | 
			
		||||
        for field in request.data["custom_fields"]:
 | 
			
		||||
 | 
			
		||||
            custom_field = field
 | 
			
		||||
            custom_field["agent"] = agent.id  # type: ignore
 | 
			
		||||
 | 
			
		||||
            if AgentCustomField.objects.filter(
 | 
			
		||||
                field=field["field"], agent=agent.id  # type: ignore
 | 
			
		||||
            ):
 | 
			
		||||
                value = AgentCustomField.objects.get(
 | 
			
		||||
                    field=field["field"], agent=agent.id  # type: ignore
 | 
			
		||||
                )
 | 
			
		||||
                serializer = AgentCustomFieldSerializer(
 | 
			
		||||
                    instance=value, data=custom_field
 | 
			
		||||
                )
 | 
			
		||||
                serializer.is_valid(raise_exception=True)
 | 
			
		||||
                serializer.save()
 | 
			
		||||
            else:
 | 
			
		||||
                serializer = AgentCustomFieldSerializer(data=custom_field)
 | 
			
		||||
                serializer.is_valid(raise_exception=True)
 | 
			
		||||
                serializer.save()
 | 
			
		||||
 | 
			
		||||
    return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -144,9 +164,6 @@ def agent_detail(request, pk):
 | 
			
		||||
@api_view()
 | 
			
		||||
def get_processes(request, pk):
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=pk)
 | 
			
		||||
    if pyver.parse(agent.version) < pyver.parse("1.2.0"):
 | 
			
		||||
        return notify_error("Requires agent version 1.2.0 or greater")
 | 
			
		||||
 | 
			
		||||
    r = asyncio.run(agent.nats_cmd(data={"func": "procs"}, timeout=5))
 | 
			
		||||
    if r == "timeout":
 | 
			
		||||
        return notify_error("Unable to contact the agent")
 | 
			
		||||
@@ -156,9 +173,6 @@ def get_processes(request, pk):
 | 
			
		||||
@api_view()
 | 
			
		||||
def kill_proc(request, pk, pid):
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=pk)
 | 
			
		||||
    if not agent.has_nats:
 | 
			
		||||
        return notify_error("Requires agent version 1.1.0 or greater")
 | 
			
		||||
 | 
			
		||||
    r = asyncio.run(
 | 
			
		||||
        agent.nats_cmd({"func": "killproc", "procpid": int(pid)}, timeout=15)
 | 
			
		||||
    )
 | 
			
		||||
@@ -174,8 +188,6 @@ def kill_proc(request, pk, pid):
 | 
			
		||||
@api_view()
 | 
			
		||||
def get_event_log(request, pk, logtype, days):
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=pk)
 | 
			
		||||
    if not agent.has_nats:
 | 
			
		||||
        return notify_error("Requires agent version 1.1.0 or greater")
 | 
			
		||||
    timeout = 180 if logtype == "Security" else 30
 | 
			
		||||
    data = {
 | 
			
		||||
        "func": "eventlog",
 | 
			
		||||
@@ -195,8 +207,6 @@ def get_event_log(request, pk, logtype, days):
 | 
			
		||||
@api_view(["POST"])
 | 
			
		||||
def send_raw_cmd(request):
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=request.data["pk"])
 | 
			
		||||
    if not agent.has_nats:
 | 
			
		||||
        return notify_error("Requires agent version 1.1.0 or greater")
 | 
			
		||||
    timeout = int(request.data["timeout"])
 | 
			
		||||
    data = {
 | 
			
		||||
        "func": "rawcmd",
 | 
			
		||||
@@ -221,15 +231,32 @@ def send_raw_cmd(request):
 | 
			
		||||
    return Response(r)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AgentsTableList(generics.ListAPIView):
 | 
			
		||||
class AgentsTableList(APIView):
 | 
			
		||||
    def patch(self, request):
 | 
			
		||||
        if "sitePK" in request.data.keys():
 | 
			
		||||
            queryset = (
 | 
			
		||||
        Agent.objects.select_related("site")
 | 
			
		||||
                Agent.objects.select_related("site", "policy", "alert_template")
 | 
			
		||||
                .prefetch_related("agentchecks")
 | 
			
		||||
        .only(
 | 
			
		||||
                .filter(site_id=request.data["sitePK"])
 | 
			
		||||
            )
 | 
			
		||||
        elif "clientPK" in request.data.keys():
 | 
			
		||||
            queryset = (
 | 
			
		||||
                Agent.objects.select_related("site", "policy", "alert_template")
 | 
			
		||||
                .prefetch_related("agentchecks")
 | 
			
		||||
                .filter(site__client_id=request.data["clientPK"])
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            queryset = Agent.objects.select_related(
 | 
			
		||||
                "site", "policy", "alert_template"
 | 
			
		||||
            ).prefetch_related("agentchecks")
 | 
			
		||||
 | 
			
		||||
        queryset = queryset.only(
 | 
			
		||||
            "pk",
 | 
			
		||||
            "hostname",
 | 
			
		||||
            "agent_id",
 | 
			
		||||
            "site",
 | 
			
		||||
            "policy",
 | 
			
		||||
            "alert_template",
 | 
			
		||||
            "monitoring_type",
 | 
			
		||||
            "description",
 | 
			
		||||
            "needs_reboot",
 | 
			
		||||
@@ -244,11 +271,6 @@ class AgentsTableList(generics.ListAPIView):
 | 
			
		||||
            "time_zone",
 | 
			
		||||
            "maintenance_mode",
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    serializer_class = AgentTableSerializer
 | 
			
		||||
 | 
			
		||||
    def list(self, request):
 | 
			
		||||
        queryset = self.get_queryset()
 | 
			
		||||
        ctx = {"default_tz": get_default_timezone()}
 | 
			
		||||
        serializer = AgentTableSerializer(queryset, many=True, context=ctx)
 | 
			
		||||
        return Response(serializer.data)
 | 
			
		||||
@@ -266,66 +288,6 @@ def agent_edit_details(request, pk):
 | 
			
		||||
    return Response(AgentEditSerializer(agent).data)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view()
 | 
			
		||||
def by_client(request, clientpk):
 | 
			
		||||
    agents = (
 | 
			
		||||
        Agent.objects.select_related("site")
 | 
			
		||||
        .filter(site__client_id=clientpk)
 | 
			
		||||
        .prefetch_related("agentchecks")
 | 
			
		||||
        .only(
 | 
			
		||||
            "pk",
 | 
			
		||||
            "hostname",
 | 
			
		||||
            "agent_id",
 | 
			
		||||
            "site",
 | 
			
		||||
            "monitoring_type",
 | 
			
		||||
            "description",
 | 
			
		||||
            "needs_reboot",
 | 
			
		||||
            "overdue_text_alert",
 | 
			
		||||
            "overdue_email_alert",
 | 
			
		||||
            "overdue_time",
 | 
			
		||||
            "offline_time",
 | 
			
		||||
            "last_seen",
 | 
			
		||||
            "boot_time",
 | 
			
		||||
            "logged_in_username",
 | 
			
		||||
            "last_logged_in_user",
 | 
			
		||||
            "time_zone",
 | 
			
		||||
            "maintenance_mode",
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    ctx = {"default_tz": get_default_timezone()}
 | 
			
		||||
    return Response(AgentTableSerializer(agents, many=True, context=ctx).data)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view()
 | 
			
		||||
def by_site(request, sitepk):
 | 
			
		||||
    agents = (
 | 
			
		||||
        Agent.objects.filter(site_id=sitepk)
 | 
			
		||||
        .select_related("site")
 | 
			
		||||
        .prefetch_related("agentchecks")
 | 
			
		||||
        .only(
 | 
			
		||||
            "pk",
 | 
			
		||||
            "hostname",
 | 
			
		||||
            "agent_id",
 | 
			
		||||
            "site",
 | 
			
		||||
            "monitoring_type",
 | 
			
		||||
            "description",
 | 
			
		||||
            "needs_reboot",
 | 
			
		||||
            "overdue_text_alert",
 | 
			
		||||
            "overdue_email_alert",
 | 
			
		||||
            "overdue_time",
 | 
			
		||||
            "offline_time",
 | 
			
		||||
            "last_seen",
 | 
			
		||||
            "boot_time",
 | 
			
		||||
            "logged_in_username",
 | 
			
		||||
            "last_logged_in_user",
 | 
			
		||||
            "time_zone",
 | 
			
		||||
            "maintenance_mode",
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    ctx = {"default_tz": get_default_timezone()}
 | 
			
		||||
    return Response(AgentTableSerializer(agents, many=True, context=ctx).data)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view(["POST"])
 | 
			
		||||
def overdue_action(request):
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=request.data["pk"])
 | 
			
		||||
@@ -341,9 +303,6 @@ class Reboot(APIView):
 | 
			
		||||
    # reboot now
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        agent = get_object_or_404(Agent, pk=request.data["pk"])
 | 
			
		||||
        if not agent.has_nats:
 | 
			
		||||
            return notify_error("Requires agent version 1.1.0 or greater")
 | 
			
		||||
 | 
			
		||||
        r = asyncio.run(agent.nats_cmd({"func": "rebootnow"}, timeout=10))
 | 
			
		||||
        if r != "ok":
 | 
			
		||||
            return notify_error("Unable to contact the agent")
 | 
			
		||||
@@ -353,8 +312,6 @@ class Reboot(APIView):
 | 
			
		||||
    # reboot later
 | 
			
		||||
    def patch(self, request):
 | 
			
		||||
        agent = get_object_or_404(Agent, pk=request.data["pk"])
 | 
			
		||||
        if not agent.has_gotasks:
 | 
			
		||||
            return notify_error("Requires agent version 1.1.1 or greater")
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            obj = dt.datetime.strptime(request.data["datetime"], "%Y-%m-%d %H:%M")
 | 
			
		||||
@@ -369,6 +326,7 @@ class Reboot(APIView):
 | 
			
		||||
            "func": "schedtask",
 | 
			
		||||
            "schedtaskpayload": {
 | 
			
		||||
                "type": "schedreboot",
 | 
			
		||||
                "deleteafter": True,
 | 
			
		||||
                "trigger": "once",
 | 
			
		||||
                "name": task_name,
 | 
			
		||||
                "year": int(dt.datetime.strftime(obj, "%Y")),
 | 
			
		||||
@@ -379,9 +337,6 @@ class Reboot(APIView):
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if pyver.parse(agent.version) >= pyver.parse("1.1.2"):
 | 
			
		||||
            nats_data["schedtaskpayload"]["deleteafter"] = True
 | 
			
		||||
 | 
			
		||||
        r = asyncio.run(agent.nats_cmd(nats_data, timeout=10))
 | 
			
		||||
        if r != "ok":
 | 
			
		||||
            return notify_error(r)
 | 
			
		||||
@@ -427,124 +382,20 @@ def install_agent(request):
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    if request.data["installMethod"] == "exe":
 | 
			
		||||
        go_bin = "/usr/local/rmmgo/go/bin/go"
 | 
			
		||||
        from tacticalrmm.utils import generate_winagent_exe
 | 
			
		||||
 | 
			
		||||
        if not os.path.exists(go_bin):
 | 
			
		||||
            return Response("nogolang", status=status.HTTP_409_CONFLICT)
 | 
			
		||||
 | 
			
		||||
        api = request.data["api"]
 | 
			
		||||
        atype = request.data["agenttype"]
 | 
			
		||||
        rdp = request.data["rdp"]
 | 
			
		||||
        ping = request.data["ping"]
 | 
			
		||||
        power = request.data["power"]
 | 
			
		||||
 | 
			
		||||
        file_name = "rmm-installer.exe"
 | 
			
		||||
        exe = os.path.join(settings.EXE_DIR, file_name)
 | 
			
		||||
 | 
			
		||||
        if os.path.exists(exe):
 | 
			
		||||
            try:
 | 
			
		||||
                os.remove(exe)
 | 
			
		||||
            except Exception as e:
 | 
			
		||||
                logger.error(str(e))
 | 
			
		||||
 | 
			
		||||
        goarch = "amd64" if arch == "64" else "386"
 | 
			
		||||
        cmd = [
 | 
			
		||||
            "env",
 | 
			
		||||
            "GOOS=windows",
 | 
			
		||||
            f"GOARCH={goarch}",
 | 
			
		||||
            go_bin,
 | 
			
		||||
            "build",
 | 
			
		||||
            f"-ldflags=\"-s -w -X 'main.Inno={inno}'",
 | 
			
		||||
            f"-X 'main.Api={api}'",
 | 
			
		||||
            f"-X 'main.Client={client_id}'",
 | 
			
		||||
            f"-X 'main.Site={site_id}'",
 | 
			
		||||
            f"-X 'main.Atype={atype}'",
 | 
			
		||||
            f"-X 'main.Rdp={rdp}'",
 | 
			
		||||
            f"-X 'main.Ping={ping}'",
 | 
			
		||||
            f"-X 'main.Power={power}'",
 | 
			
		||||
            f"-X 'main.DownloadUrl={download_url}'",
 | 
			
		||||
            f"-X 'main.Token={token}'\"",
 | 
			
		||||
            "-o",
 | 
			
		||||
            exe,
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        build_error = False
 | 
			
		||||
        gen_error = False
 | 
			
		||||
 | 
			
		||||
        gen = [
 | 
			
		||||
            "env",
 | 
			
		||||
            "GOOS=windows",
 | 
			
		||||
            f"GOARCH={goarch}",
 | 
			
		||||
            go_bin,
 | 
			
		||||
            "generate",
 | 
			
		||||
        ]
 | 
			
		||||
        try:
 | 
			
		||||
            r1 = subprocess.run(
 | 
			
		||||
                " ".join(gen),
 | 
			
		||||
                capture_output=True,
 | 
			
		||||
                shell=True,
 | 
			
		||||
                cwd=os.path.join(settings.BASE_DIR, "core/goinstaller"),
 | 
			
		||||
        return generate_winagent_exe(
 | 
			
		||||
            client=client_id,
 | 
			
		||||
            site=site_id,
 | 
			
		||||
            agent_type=request.data["agenttype"],
 | 
			
		||||
            rdp=request.data["rdp"],
 | 
			
		||||
            ping=request.data["ping"],
 | 
			
		||||
            power=request.data["power"],
 | 
			
		||||
            arch=arch,
 | 
			
		||||
            token=token,
 | 
			
		||||
            api=request.data["api"],
 | 
			
		||||
            file_name=request.data["fileName"],
 | 
			
		||||
        )
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            gen_error = True
 | 
			
		||||
            logger.error(str(e))
 | 
			
		||||
            return Response(
 | 
			
		||||
                "genfailed", status=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        if r1.returncode != 0:
 | 
			
		||||
            gen_error = True
 | 
			
		||||
            if r1.stdout:
 | 
			
		||||
                logger.error(r1.stdout.decode("utf-8", errors="ignore"))
 | 
			
		||||
 | 
			
		||||
            if r1.stderr:
 | 
			
		||||
                logger.error(r1.stderr.decode("utf-8", errors="ignore"))
 | 
			
		||||
 | 
			
		||||
            logger.error(f"Go build failed with return code {r1.returncode}")
 | 
			
		||||
 | 
			
		||||
        if gen_error:
 | 
			
		||||
            return Response(
 | 
			
		||||
                "genfailed", status=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            r = subprocess.run(
 | 
			
		||||
                " ".join(cmd),
 | 
			
		||||
                capture_output=True,
 | 
			
		||||
                shell=True,
 | 
			
		||||
                cwd=os.path.join(settings.BASE_DIR, "core/goinstaller"),
 | 
			
		||||
            )
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            build_error = True
 | 
			
		||||
            logger.error(str(e))
 | 
			
		||||
            return Response("buildfailed", status=status.HTTP_412_PRECONDITION_FAILED)
 | 
			
		||||
 | 
			
		||||
        if r.returncode != 0:
 | 
			
		||||
            build_error = True
 | 
			
		||||
            if r.stdout:
 | 
			
		||||
                logger.error(r.stdout.decode("utf-8", errors="ignore"))
 | 
			
		||||
 | 
			
		||||
            if r.stderr:
 | 
			
		||||
                logger.error(r.stderr.decode("utf-8", errors="ignore"))
 | 
			
		||||
 | 
			
		||||
            logger.error(f"Go build failed with return code {r.returncode}")
 | 
			
		||||
 | 
			
		||||
        if build_error:
 | 
			
		||||
            return Response("buildfailed", status=status.HTTP_412_PRECONDITION_FAILED)
 | 
			
		||||
 | 
			
		||||
        if settings.DEBUG:
 | 
			
		||||
            with open(exe, "rb") as f:
 | 
			
		||||
                response = HttpResponse(
 | 
			
		||||
                    f.read(),
 | 
			
		||||
                    content_type="application/vnd.microsoft.portable-executable",
 | 
			
		||||
                )
 | 
			
		||||
                response["Content-Disposition"] = f"inline; filename={file_name}"
 | 
			
		||||
                return response
 | 
			
		||||
        else:
 | 
			
		||||
            response = HttpResponse()
 | 
			
		||||
            response["Content-Disposition"] = f"attachment; filename={file_name}"
 | 
			
		||||
            response["X-Accel-Redirect"] = f"/private/exe/{file_name}"
 | 
			
		||||
            return response
 | 
			
		||||
 | 
			
		||||
    elif request.data["installMethod"] == "manual":
 | 
			
		||||
        cmd = [
 | 
			
		||||
@@ -638,22 +489,14 @@ def recover(request):
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=request.data["pk"])
 | 
			
		||||
    mode = request.data["mode"]
 | 
			
		||||
 | 
			
		||||
    if pyver.parse(agent.version) <= pyver.parse("0.9.5"):
 | 
			
		||||
        return notify_error("Only available in agent version greater than 0.9.5")
 | 
			
		||||
 | 
			
		||||
    if not agent.has_nats:
 | 
			
		||||
        if mode == "tacagent" or mode == "rpc":
 | 
			
		||||
            return notify_error("Requires agent version 1.1.0 or greater")
 | 
			
		||||
 | 
			
		||||
    # attempt a realtime recovery if supported, otherwise fall back to old recovery method
 | 
			
		||||
    if agent.has_nats:
 | 
			
		||||
    # attempt a realtime recovery, otherwise fall back to old recovery method
 | 
			
		||||
    if mode == "tacagent" or mode == "mesh":
 | 
			
		||||
        data = {"func": "recover", "payload": {"mode": mode}}
 | 
			
		||||
        r = asyncio.run(agent.nats_cmd(data, timeout=10))
 | 
			
		||||
        if r == "ok":
 | 
			
		||||
            return Response("Successfully completed recovery")
 | 
			
		||||
 | 
			
		||||
    if agent.recoveryactions.filter(last_run=None).exists():
 | 
			
		||||
    if agent.recoveryactions.filter(last_run=None).exists():  # type: ignore
 | 
			
		||||
        return notify_error(
 | 
			
		||||
            "A recovery action is currently pending. Please wait for the next agent check-in."
 | 
			
		||||
        )
 | 
			
		||||
@@ -681,10 +524,9 @@ def recover(request):
 | 
			
		||||
@api_view(["POST"])
 | 
			
		||||
def run_script(request):
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=request.data["pk"])
 | 
			
		||||
    if not agent.has_nats:
 | 
			
		||||
        return notify_error("Requires agent version 1.1.0 or greater")
 | 
			
		||||
    script = get_object_or_404(Script, pk=request.data["scriptPK"])
 | 
			
		||||
    output = request.data["output"]
 | 
			
		||||
    args = request.data["args"]
 | 
			
		||||
    req_timeout = int(request.data["timeout"]) + 3
 | 
			
		||||
 | 
			
		||||
    AuditLog.audit_script_run(
 | 
			
		||||
@@ -694,13 +536,12 @@ def run_script(request):
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    if output == "wait":
 | 
			
		||||
        r = agent.run_script(scriptpk=script.pk, timeout=req_timeout, wait=True)
 | 
			
		||||
        r = agent.run_script(
 | 
			
		||||
            scriptpk=script.pk, args=args, timeout=req_timeout, wait=True
 | 
			
		||||
        )
 | 
			
		||||
        return Response(r)
 | 
			
		||||
 | 
			
		||||
    elif output == "email":
 | 
			
		||||
        if not pyver.parse(agent.version) >= pyver.parse("1.1.12"):
 | 
			
		||||
            return notify_error("Requires agent version 1.1.12 or greater")
 | 
			
		||||
 | 
			
		||||
        emails = (
 | 
			
		||||
            [] if request.data["emailmode"] == "default" else request.data["emails"]
 | 
			
		||||
        )
 | 
			
		||||
@@ -709,9 +550,10 @@ def run_script(request):
 | 
			
		||||
            scriptpk=script.pk,
 | 
			
		||||
            nats_timeout=req_timeout,
 | 
			
		||||
            emails=emails,
 | 
			
		||||
            args=args,
 | 
			
		||||
        )
 | 
			
		||||
    else:
 | 
			
		||||
        agent.run_script(scriptpk=script.pk, timeout=req_timeout)
 | 
			
		||||
        agent.run_script(scriptpk=script.pk, args=args, timeout=req_timeout)
 | 
			
		||||
 | 
			
		||||
    return Response(f"{script.name} will now be run on {agent.hostname}")
 | 
			
		||||
 | 
			
		||||
@@ -719,9 +561,6 @@ def run_script(request):
 | 
			
		||||
@api_view()
 | 
			
		||||
def recover_mesh(request, pk):
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=pk)
 | 
			
		||||
    if not agent.has_nats:
 | 
			
		||||
        return notify_error("Requires agent version 1.1.0 or greater")
 | 
			
		||||
 | 
			
		||||
    data = {"func": "recover", "payload": {"mode": "mesh"}}
 | 
			
		||||
    r = asyncio.run(agent.nats_cmd(data, timeout=45))
 | 
			
		||||
    if r != "ok":
 | 
			
		||||
@@ -803,7 +642,7 @@ def bulk(request):
 | 
			
		||||
    elif request.data["monType"] == "workstations":
 | 
			
		||||
        q = q.filter(monitoring_type="workstation")
 | 
			
		||||
 | 
			
		||||
    agents: List[int] = [agent.pk for agent in q]
 | 
			
		||||
    agents: list[int] = [agent.pk for agent in q]
 | 
			
		||||
 | 
			
		||||
    AuditLog.audit_bulk_action(request.user, request.data["mode"], request.data)
 | 
			
		||||
 | 
			
		||||
@@ -832,49 +671,6 @@ def bulk(request):
 | 
			
		||||
    return notify_error("Something went wrong")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view(["POST"])
 | 
			
		||||
def agent_counts(request):
 | 
			
		||||
 | 
			
		||||
    server_offline_count = len(
 | 
			
		||||
        [
 | 
			
		||||
            agent
 | 
			
		||||
            for agent in Agent.objects.filter(monitoring_type="server").only(
 | 
			
		||||
                "pk",
 | 
			
		||||
                "last_seen",
 | 
			
		||||
                "overdue_time",
 | 
			
		||||
                "offline_time",
 | 
			
		||||
            )
 | 
			
		||||
            if not agent.status == "online"
 | 
			
		||||
        ]
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    workstation_offline_count = len(
 | 
			
		||||
        [
 | 
			
		||||
            agent
 | 
			
		||||
            for agent in Agent.objects.filter(monitoring_type="workstation").only(
 | 
			
		||||
                "pk",
 | 
			
		||||
                "last_seen",
 | 
			
		||||
                "overdue_time",
 | 
			
		||||
                "offline_time",
 | 
			
		||||
            )
 | 
			
		||||
            if not agent.status == "online"
 | 
			
		||||
        ]
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    return Response(
 | 
			
		||||
        {
 | 
			
		||||
            "total_server_count": Agent.objects.filter(
 | 
			
		||||
                monitoring_type="server"
 | 
			
		||||
            ).count(),
 | 
			
		||||
            "total_server_offline_count": server_offline_count,
 | 
			
		||||
            "total_workstation_count": Agent.objects.filter(
 | 
			
		||||
                monitoring_type="workstation"
 | 
			
		||||
            ).count(),
 | 
			
		||||
            "total_workstation_offline_count": workstation_offline_count,
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view(["POST"])
 | 
			
		||||
def agent_maintenance(request):
 | 
			
		||||
    if request.data["type"] == "Client":
 | 
			
		||||
@@ -901,9 +697,6 @@ def agent_maintenance(request):
 | 
			
		||||
class WMI(APIView):
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
        agent = get_object_or_404(Agent, pk=pk)
 | 
			
		||||
        if pyver.parse(agent.version) < pyver.parse("1.1.2"):
 | 
			
		||||
            return notify_error("Requires agent version 1.1.2 or greater")
 | 
			
		||||
 | 
			
		||||
        r = asyncio.run(agent.nats_cmd({"func": "sysinfo"}, timeout=20))
 | 
			
		||||
        if r != "ok":
 | 
			
		||||
            return notify_error("Unable to contact the agent")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,20 @@
 | 
			
		||||
from __future__ import annotations
 | 
			
		||||
 | 
			
		||||
from typing import TYPE_CHECKING, Union
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.contrib.postgres.fields import ArrayField
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.db.models.fields import BooleanField, PositiveIntegerField
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from loguru import logger
 | 
			
		||||
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
    from agents.models import Agent
 | 
			
		||||
    from autotasks.models import AutomatedTask
 | 
			
		||||
    from checks.models import Check
 | 
			
		||||
 | 
			
		||||
logger.configure(**settings.LOG_CONFIG)
 | 
			
		||||
 | 
			
		||||
SEVERITY_CHOICES = [
 | 
			
		||||
    ("info", "Informational"),
 | 
			
		||||
@@ -78,7 +91,7 @@ class Alert(models.Model):
 | 
			
		||||
        self.save()
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def create_availability_alert(cls, agent):
 | 
			
		||||
    def create_or_return_availability_alert(cls, agent):
 | 
			
		||||
        if not cls.objects.filter(agent=agent, resolved=False).exists():
 | 
			
		||||
            return cls.objects.create(
 | 
			
		||||
                agent=agent,
 | 
			
		||||
@@ -87,9 +100,11 @@ class Alert(models.Model):
 | 
			
		||||
                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_check_alert(cls, check):
 | 
			
		||||
    def create_or_return_check_alert(cls, check):
 | 
			
		||||
 | 
			
		||||
        if not cls.objects.filter(assigned_check=check, resolved=False).exists():
 | 
			
		||||
            return cls.objects.create(
 | 
			
		||||
@@ -99,9 +114,11 @@ class Alert(models.Model):
 | 
			
		||||
                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_task_alert(cls, task):
 | 
			
		||||
    def create_or_return_task_alert(cls, task):
 | 
			
		||||
 | 
			
		||||
        if not cls.objects.filter(assigned_task=task, resolved=False).exists():
 | 
			
		||||
            return cls.objects.create(
 | 
			
		||||
@@ -111,10 +128,305 @@ class Alert(models.Model):
 | 
			
		||||
                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 create_custom_alert(cls, custom):
 | 
			
		||||
    def handle_alert_failure(cls, instance: Union[Agent, AutomatedTask, Check]) -> None:
 | 
			
		||||
        from agents.models import Agent
 | 
			
		||||
        from autotasks.models import AutomatedTask
 | 
			
		||||
        from checks.models import Check
 | 
			
		||||
 | 
			
		||||
        # set variables
 | 
			
		||||
        dashboard_severities = None
 | 
			
		||||
        email_severities = None
 | 
			
		||||
        text_severities = None
 | 
			
		||||
        always_dashboard = None
 | 
			
		||||
        always_email = None
 | 
			
		||||
        always_text = None
 | 
			
		||||
        alert_interval = None
 | 
			
		||||
        email_task = None
 | 
			
		||||
        text_task = None
 | 
			
		||||
 | 
			
		||||
        # check what the instance passed is
 | 
			
		||||
        if isinstance(instance, Agent):
 | 
			
		||||
            from agents.tasks import agent_outage_email_task, agent_outage_sms_task
 | 
			
		||||
 | 
			
		||||
            email_task = agent_outage_email_task
 | 
			
		||||
            text_task = agent_outage_sms_task
 | 
			
		||||
 | 
			
		||||
            email_alert = instance.overdue_email_alert
 | 
			
		||||
            text_alert = instance.overdue_text_alert
 | 
			
		||||
            dashboard_alert = instance.overdue_dashboard_alert
 | 
			
		||||
            alert_template = instance.alert_template
 | 
			
		||||
            maintenance_mode = instance.maintenance_mode
 | 
			
		||||
            alert_severity = "error"
 | 
			
		||||
            agent = instance
 | 
			
		||||
 | 
			
		||||
            # set alert_template settings
 | 
			
		||||
            if alert_template:
 | 
			
		||||
                dashboard_severities = ["error"]
 | 
			
		||||
                email_severities = ["error"]
 | 
			
		||||
                text_severities = ["error"]
 | 
			
		||||
                always_dashboard = alert_template.agent_always_alert
 | 
			
		||||
                always_email = alert_template.agent_always_email
 | 
			
		||||
                always_text = alert_template.agent_always_text
 | 
			
		||||
                alert_interval = alert_template.agent_periodic_alert_days
 | 
			
		||||
 | 
			
		||||
            if instance.should_create_alert(alert_template):
 | 
			
		||||
                alert = cls.create_or_return_availability_alert(instance)
 | 
			
		||||
            else:
 | 
			
		||||
                # check if there is an alert that exists
 | 
			
		||||
                if cls.objects.filter(agent=instance, resolved=False).exists():
 | 
			
		||||
                    alert = cls.objects.get(agent=instance, resolved=False)
 | 
			
		||||
                else:
 | 
			
		||||
                    alert = None
 | 
			
		||||
 | 
			
		||||
        elif isinstance(instance, Check):
 | 
			
		||||
            from checks.tasks import (
 | 
			
		||||
                handle_check_email_alert_task,
 | 
			
		||||
                handle_check_sms_alert_task,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            email_task = handle_check_email_alert_task
 | 
			
		||||
            text_task = handle_check_sms_alert_task
 | 
			
		||||
 | 
			
		||||
            email_alert = instance.email_alert
 | 
			
		||||
            text_alert = instance.text_alert
 | 
			
		||||
            dashboard_alert = instance.dashboard_alert
 | 
			
		||||
            alert_template = instance.agent.alert_template
 | 
			
		||||
            maintenance_mode = instance.agent.maintenance_mode
 | 
			
		||||
            alert_severity = instance.alert_severity
 | 
			
		||||
            agent = instance.agent
 | 
			
		||||
 | 
			
		||||
            # set alert_template settings
 | 
			
		||||
            if alert_template:
 | 
			
		||||
                dashboard_severities = alert_template.check_dashboard_alert_severity
 | 
			
		||||
                email_severities = alert_template.check_email_alert_severity
 | 
			
		||||
                text_severities = alert_template.check_text_alert_severity
 | 
			
		||||
                always_dashboard = alert_template.check_always_alert
 | 
			
		||||
                always_email = alert_template.check_always_email
 | 
			
		||||
                always_text = alert_template.check_always_text
 | 
			
		||||
                alert_interval = alert_template.check_periodic_alert_days
 | 
			
		||||
 | 
			
		||||
            if instance.should_create_alert(alert_template):
 | 
			
		||||
                alert = cls.create_or_return_check_alert(instance)
 | 
			
		||||
            else:
 | 
			
		||||
                # check if there is an alert that exists
 | 
			
		||||
                if cls.objects.filter(assigned_check=instance, resolved=False).exists():
 | 
			
		||||
                    alert = cls.objects.get(assigned_check=instance, resolved=False)
 | 
			
		||||
                else:
 | 
			
		||||
                    alert = None
 | 
			
		||||
 | 
			
		||||
        elif isinstance(instance, AutomatedTask):
 | 
			
		||||
            from autotasks.tasks import handle_task_email_alert, handle_task_sms_alert
 | 
			
		||||
 | 
			
		||||
            email_task = handle_task_email_alert
 | 
			
		||||
            text_task = handle_task_sms_alert
 | 
			
		||||
 | 
			
		||||
            email_alert = instance.email_alert
 | 
			
		||||
            text_alert = instance.text_alert
 | 
			
		||||
            dashboard_alert = instance.dashboard_alert
 | 
			
		||||
            alert_template = instance.agent.alert_template
 | 
			
		||||
            maintenance_mode = instance.agent.maintenance_mode
 | 
			
		||||
            alert_severity = instance.alert_severity
 | 
			
		||||
            agent = instance.agent
 | 
			
		||||
 | 
			
		||||
            # set alert_template settings
 | 
			
		||||
            if alert_template:
 | 
			
		||||
                dashboard_severities = alert_template.task_dashboard_alert_severity
 | 
			
		||||
                email_severities = alert_template.task_email_alert_severity
 | 
			
		||||
                text_severities = alert_template.task_text_alert_severity
 | 
			
		||||
                always_dashboard = alert_template.task_always_alert
 | 
			
		||||
                always_email = alert_template.task_always_email
 | 
			
		||||
                always_text = alert_template.task_always_text
 | 
			
		||||
                alert_interval = alert_template.task_periodic_alert_days
 | 
			
		||||
 | 
			
		||||
            if instance.should_create_alert(alert_template):
 | 
			
		||||
                alert = cls.create_or_return_task_alert(instance)
 | 
			
		||||
            else:
 | 
			
		||||
                # check if there is an alert that exists
 | 
			
		||||
                if cls.objects.filter(assigned_task=instance, resolved=False).exists():
 | 
			
		||||
                    alert = cls.objects.get(assigned_task=instance, resolved=False)
 | 
			
		||||
                else:
 | 
			
		||||
                    alert = None
 | 
			
		||||
        else:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        # return if agent is in maintenance mode
 | 
			
		||||
        if maintenance_mode or not alert:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        # check if alert severity changed on check and update the alert
 | 
			
		||||
        if alert_severity != alert.severity:
 | 
			
		||||
            alert.severity = alert_severity
 | 
			
		||||
            alert.save(update_fields=["severity"])
 | 
			
		||||
 | 
			
		||||
        # create alert in dashboard if enabled
 | 
			
		||||
        if dashboard_alert or always_dashboard:
 | 
			
		||||
 | 
			
		||||
            # check if alert template is set and specific severities are configured
 | 
			
		||||
            if alert_template and alert.severity not in dashboard_severities:  # type: ignore
 | 
			
		||||
                pass
 | 
			
		||||
            else:
 | 
			
		||||
                alert.hidden = False
 | 
			
		||||
                alert.save()
 | 
			
		||||
 | 
			
		||||
        # send email if enabled
 | 
			
		||||
        if email_alert or always_email:
 | 
			
		||||
 | 
			
		||||
            # check if alert template is set and specific severities are configured
 | 
			
		||||
            if alert_template and alert.severity not in email_severities:  # type: ignore
 | 
			
		||||
                pass
 | 
			
		||||
            else:
 | 
			
		||||
                email_task.delay(
 | 
			
		||||
                    pk=alert.pk,
 | 
			
		||||
                    alert_interval=alert_interval,
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
        # send text if enabled
 | 
			
		||||
        if text_alert or always_text:
 | 
			
		||||
 | 
			
		||||
            # check if alert template is set and specific severities are configured
 | 
			
		||||
            if alert_template and alert.severity not in text_severities:  # type: ignore
 | 
			
		||||
                pass
 | 
			
		||||
            else:
 | 
			
		||||
                text_task.delay(pk=alert.pk, alert_interval=alert_interval)
 | 
			
		||||
 | 
			
		||||
        # check if any scripts should be run
 | 
			
		||||
        if alert_template and alert_template.action and not alert.action_run:
 | 
			
		||||
            r = agent.run_script(
 | 
			
		||||
                scriptpk=alert_template.action.pk,
 | 
			
		||||
                args=alert_template.action_args,
 | 
			
		||||
                timeout=alert_template.action_timeout,
 | 
			
		||||
                wait=True,
 | 
			
		||||
                full=True,
 | 
			
		||||
                run_on_any=True,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            # command was successful
 | 
			
		||||
            if type(r) == dict:
 | 
			
		||||
                alert.action_retcode = r["retcode"]
 | 
			
		||||
                alert.action_stdout = r["stdout"]
 | 
			
		||||
                alert.action_stderr = r["stderr"]
 | 
			
		||||
                alert.action_execution_time = "{:.4f}".format(r["execution_time"])
 | 
			
		||||
                alert.action_run = djangotime.now()
 | 
			
		||||
                alert.save()
 | 
			
		||||
            else:
 | 
			
		||||
                logger.error(
 | 
			
		||||
                    f"Failure action: {alert_template.action.name} failed to run on any agent for {agent.hostname} failure alert"
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def handle_alert_resolve(cls, instance: Union[Agent, AutomatedTask, Check]) -> None:
 | 
			
		||||
        from agents.models import Agent
 | 
			
		||||
        from autotasks.models import AutomatedTask
 | 
			
		||||
        from checks.models import Check
 | 
			
		||||
 | 
			
		||||
        # set variables
 | 
			
		||||
        email_on_resolved = False
 | 
			
		||||
        text_on_resolved = False
 | 
			
		||||
        resolved_email_task = None
 | 
			
		||||
        resolved_text_task = None
 | 
			
		||||
 | 
			
		||||
        # check what the instance passed is
 | 
			
		||||
        if isinstance(instance, Agent):
 | 
			
		||||
            from agents.tasks import agent_recovery_email_task, agent_recovery_sms_task
 | 
			
		||||
 | 
			
		||||
            resolved_email_task = agent_recovery_email_task
 | 
			
		||||
            resolved_text_task = agent_recovery_sms_task
 | 
			
		||||
 | 
			
		||||
            alert_template = instance.alert_template
 | 
			
		||||
            alert = cls.objects.get(agent=instance, resolved=False)
 | 
			
		||||
            maintenance_mode = instance.maintenance_mode
 | 
			
		||||
            agent = instance
 | 
			
		||||
 | 
			
		||||
            if alert_template:
 | 
			
		||||
                email_on_resolved = alert_template.agent_email_on_resolved
 | 
			
		||||
                text_on_resolved = alert_template.agent_text_on_resolved
 | 
			
		||||
 | 
			
		||||
        elif isinstance(instance, Check):
 | 
			
		||||
            from checks.tasks import (
 | 
			
		||||
                handle_resolved_check_email_alert_task,
 | 
			
		||||
                handle_resolved_check_sms_alert_task,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            resolved_email_task = handle_resolved_check_email_alert_task
 | 
			
		||||
            resolved_text_task = handle_resolved_check_sms_alert_task
 | 
			
		||||
 | 
			
		||||
            alert_template = instance.agent.alert_template
 | 
			
		||||
            alert = cls.objects.get(assigned_check=instance, resolved=False)
 | 
			
		||||
            maintenance_mode = instance.agent.maintenance_mode
 | 
			
		||||
            agent = instance.agent
 | 
			
		||||
 | 
			
		||||
            if alert_template:
 | 
			
		||||
                email_on_resolved = alert_template.check_email_on_resolved
 | 
			
		||||
                text_on_resolved = alert_template.check_text_on_resolved
 | 
			
		||||
 | 
			
		||||
        elif isinstance(instance, AutomatedTask):
 | 
			
		||||
            from autotasks.tasks import (
 | 
			
		||||
                handle_resolved_task_email_alert,
 | 
			
		||||
                handle_resolved_task_sms_alert,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            resolved_email_task = handle_resolved_task_email_alert
 | 
			
		||||
            resolved_text_task = handle_resolved_task_sms_alert
 | 
			
		||||
 | 
			
		||||
            alert_template = instance.agent.alert_template
 | 
			
		||||
            alert = cls.objects.get(assigned_task=instance, resolved=False)
 | 
			
		||||
            maintenance_mode = instance.agent.maintenance_mode
 | 
			
		||||
            agent = instance.agent
 | 
			
		||||
 | 
			
		||||
            if alert_template:
 | 
			
		||||
                email_on_resolved = alert_template.task_email_on_resolved
 | 
			
		||||
                text_on_resolved = alert_template.task_text_on_resolved
 | 
			
		||||
 | 
			
		||||
        else:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        # return if agent is in maintenance mode
 | 
			
		||||
        if maintenance_mode:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        alert.resolve()
 | 
			
		||||
 | 
			
		||||
        # check if a resolved email notification should be send
 | 
			
		||||
        if email_on_resolved and not alert.resolved_email_sent:
 | 
			
		||||
            resolved_email_task.delay(pk=alert.pk)
 | 
			
		||||
 | 
			
		||||
        # check if resolved text should be sent
 | 
			
		||||
        if text_on_resolved and not alert.resolved_sms_sent:
 | 
			
		||||
            resolved_text_task.delay(pk=alert.pk)
 | 
			
		||||
 | 
			
		||||
        # check if resolved script should be run
 | 
			
		||||
        if (
 | 
			
		||||
            alert_template
 | 
			
		||||
            and alert_template.resolved_action
 | 
			
		||||
            and not alert.resolved_action_run
 | 
			
		||||
        ):
 | 
			
		||||
            r = agent.run_script(
 | 
			
		||||
                scriptpk=alert_template.resolved_action.pk,
 | 
			
		||||
                args=alert_template.resolved_action_args,
 | 
			
		||||
                timeout=alert_template.resolved_action_timeout,
 | 
			
		||||
                wait=True,
 | 
			
		||||
                full=True,
 | 
			
		||||
                run_on_any=True,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            # command was successful
 | 
			
		||||
            if type(r) == dict:
 | 
			
		||||
                alert.resolved_action_retcode = r["retcode"]
 | 
			
		||||
                alert.resolved_action_stdout = r["stdout"]
 | 
			
		||||
                alert.resolved_action_stderr = r["stderr"]
 | 
			
		||||
                alert.resolved_action_execution_time = "{:.4f}".format(
 | 
			
		||||
                    r["execution_time"]
 | 
			
		||||
                )
 | 
			
		||||
                alert.resolved_action_run = djangotime.now()
 | 
			
		||||
                alert.save()
 | 
			
		||||
            else:
 | 
			
		||||
                logger.error(
 | 
			
		||||
                    f"Resolved action: {alert_template.action.name} failed to run on any agent for {agent.hostname} resolved alert"
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AlertTemplate(models.Model):
 | 
			
		||||
@@ -283,4 +595,4 @@ class AlertTemplate(models.Model):
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def is_default_template(self) -> bool:
 | 
			
		||||
        return self.default_alert_template.exists()
 | 
			
		||||
        return self.default_alert_template.exists()  # type: ignore
 | 
			
		||||
 
 | 
			
		||||
@@ -12,3 +12,13 @@ def unsnooze_alerts() -> str:
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    return "ok"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task
 | 
			
		||||
def cache_agents_alert_template():
 | 
			
		||||
    from agents.models import Agent
 | 
			
		||||
 | 
			
		||||
    for agent in Agent.objects.only("pk"):
 | 
			
		||||
        agent.set_alert_template()
 | 
			
		||||
 | 
			
		||||
    return "ok"
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -14,6 +14,7 @@ from .serializers import (
 | 
			
		||||
    AlertTemplateRelationSerializer,
 | 
			
		||||
    AlertTemplateSerializer,
 | 
			
		||||
)
 | 
			
		||||
from .tasks import cache_agents_alert_template
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetAddAlerts(APIView):
 | 
			
		||||
@@ -194,6 +195,9 @@ class GetAddAlertTemplates(APIView):
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        serializer.save()
 | 
			
		||||
 | 
			
		||||
        # cache alert_template value on agents
 | 
			
		||||
        cache_agents_alert_template.delay()
 | 
			
		||||
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -212,11 +216,17 @@ class GetUpdateDeleteAlertTemplate(APIView):
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        serializer.save()
 | 
			
		||||
 | 
			
		||||
        # cache alert_template value on agents
 | 
			
		||||
        cache_agents_alert_template.delay()
 | 
			
		||||
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
    def delete(self, request, pk):
 | 
			
		||||
        get_object_or_404(AlertTemplate, pk=pk).delete()
 | 
			
		||||
 | 
			
		||||
        # cache alert_template value on agents
 | 
			
		||||
        cache_agents_alert_template.delay()
 | 
			
		||||
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
import json
 | 
			
		||||
import os
 | 
			
		||||
from itertools import cycle
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from model_bakery import baker
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
@@ -18,8 +18,44 @@ class TestAPIv3(TacticalTestCase):
 | 
			
		||||
    def test_get_checks(self):
 | 
			
		||||
        url = f"/api/v3/{self.agent.agent_id}/checkrunner/"
 | 
			
		||||
 | 
			
		||||
        # add a check
 | 
			
		||||
        check1 = baker.make_recipe("checks.ping_check", agent=self.agent)
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(r.data["check_interval"], self.agent.check_interval)  # type: ignore
 | 
			
		||||
        self.assertEqual(len(r.data["checks"]), 1)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # override check run interval
 | 
			
		||||
        check2 = baker.make_recipe(
 | 
			
		||||
            "checks.ping_check", agent=self.agent, run_interval=20
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(r.data["check_interval"], 20)  # type: ignore
 | 
			
		||||
        self.assertEqual(len(r.data["checks"]), 2)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # Set last_run on both checks and should return an empty list
 | 
			
		||||
        check1.last_run = djangotime.now()
 | 
			
		||||
        check1.save()
 | 
			
		||||
        check2.last_run = djangotime.now()
 | 
			
		||||
        check2.save()
 | 
			
		||||
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(r.data["check_interval"], 20)  # type: ignore
 | 
			
		||||
        self.assertFalse(r.data["checks"])  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # set last_run greater than interval
 | 
			
		||||
        check1.last_run = djangotime.now() - djangotime.timedelta(seconds=200)
 | 
			
		||||
        check1.save()
 | 
			
		||||
        check2.last_run = djangotime.now() - djangotime.timedelta(seconds=200)
 | 
			
		||||
        check2.save()
 | 
			
		||||
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(r.data["check_interval"], 20)  # type: ignore
 | 
			
		||||
        self.assertEquals(len(r.data["checks"]), 2)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        url = "/api/v3/Maj34ACb324j234asdj2n34kASDjh34-DESKTOPTEST123/checkrunner/"
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
@@ -53,3 +89,117 @@ class TestAPIv3(TacticalTestCase):
 | 
			
		||||
            r.json(),
 | 
			
		||||
            {"agent": self.agent.pk, "check_interval": self.agent.check_interval},
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # add check to agent with check interval set
 | 
			
		||||
        check = baker.make_recipe(
 | 
			
		||||
            "checks.ping_check", agent=self.agent, run_interval=30
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        r = self.client.get(url, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            r.json(),
 | 
			
		||||
            {"agent": self.agent.pk, "check_interval": 30},
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # minimum check run interval is 15 seconds
 | 
			
		||||
        check = baker.make_recipe("checks.ping_check", agent=self.agent, run_interval=5)
 | 
			
		||||
 | 
			
		||||
        r = self.client.get(url, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            r.json(),
 | 
			
		||||
            {"agent": self.agent.pk, "check_interval": 15},
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_run_checks(self):
 | 
			
		||||
        # force run all checks regardless of interval
 | 
			
		||||
        agent = baker.make_recipe("agents.online_agent")
 | 
			
		||||
        baker.make_recipe("checks.ping_check", agent=agent)
 | 
			
		||||
        baker.make_recipe("checks.diskspace_check", agent=agent)
 | 
			
		||||
        baker.make_recipe("checks.cpuload_check", agent=agent)
 | 
			
		||||
        baker.make_recipe("checks.memory_check", agent=agent)
 | 
			
		||||
        baker.make_recipe("checks.eventlog_check", agent=agent)
 | 
			
		||||
        for _ in range(10):
 | 
			
		||||
            baker.make_recipe("checks.script_check", agent=agent)
 | 
			
		||||
 | 
			
		||||
        url = f"/api/v3/{agent.agent_id}/runchecks/"
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.json()["agent"], agent.pk)
 | 
			
		||||
        self.assertIsInstance(r.json()["check_interval"], int)
 | 
			
		||||
        self.assertEqual(len(r.json()["checks"]), 15)
 | 
			
		||||
 | 
			
		||||
    def test_checkin_patch(self):
 | 
			
		||||
        from logs.models import PendingAction
 | 
			
		||||
 | 
			
		||||
        url = "/api/v3/checkin/"
 | 
			
		||||
        agent_updated = baker.make_recipe("agents.agent", version="1.3.0")
 | 
			
		||||
        PendingAction.objects.create(
 | 
			
		||||
            agent=agent_updated,
 | 
			
		||||
            action_type="agentupdate",
 | 
			
		||||
            details={
 | 
			
		||||
                "url": agent_updated.winagent_dl,
 | 
			
		||||
                "version": agent_updated.version,
 | 
			
		||||
                "inno": agent_updated.win_inno_exe,
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
        action = agent_updated.pendingactions.filter(action_type="agentupdate").first()
 | 
			
		||||
        self.assertEqual(action.status, "pending")
 | 
			
		||||
 | 
			
		||||
        # test agent failed to update and still on same version
 | 
			
		||||
        payload = {
 | 
			
		||||
            "func": "hello",
 | 
			
		||||
            "agent_id": agent_updated.agent_id,
 | 
			
		||||
            "version": "1.3.0",
 | 
			
		||||
        }
 | 
			
		||||
        r = self.client.patch(url, payload, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        action = agent_updated.pendingactions.filter(action_type="agentupdate").first()
 | 
			
		||||
        self.assertEqual(action.status, "pending")
 | 
			
		||||
 | 
			
		||||
        # test agent successful update
 | 
			
		||||
        payload["version"] = settings.LATEST_AGENT_VER
 | 
			
		||||
        r = self.client.patch(url, payload, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        action = agent_updated.pendingactions.filter(action_type="agentupdate").first()
 | 
			
		||||
        self.assertEqual(action.status, "completed")
 | 
			
		||||
        action.delete()
 | 
			
		||||
 | 
			
		||||
    @patch("apiv3.views.reload_nats")
 | 
			
		||||
    def test_agent_recovery(self, reload_nats):
 | 
			
		||||
        reload_nats.return_value = "ok"
 | 
			
		||||
        r = self.client.get("/api/v3/34jahsdkjasncASDjhg2b3j4r/recover/")
 | 
			
		||||
        self.assertEqual(r.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        agent = baker.make_recipe("agents.online_agent")
 | 
			
		||||
        url = f"/api/v3/{agent.agent_id}/recovery/"
 | 
			
		||||
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(r.json(), {"mode": "pass", "shellcmd": ""})
 | 
			
		||||
        reload_nats.assert_not_called()
 | 
			
		||||
 | 
			
		||||
        baker.make("agents.RecoveryAction", agent=agent, mode="mesh")
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(r.json(), {"mode": "mesh", "shellcmd": ""})
 | 
			
		||||
        reload_nats.assert_not_called()
 | 
			
		||||
 | 
			
		||||
        baker.make(
 | 
			
		||||
            "agents.RecoveryAction",
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            mode="command",
 | 
			
		||||
            command="shutdown /r /t 5 /f",
 | 
			
		||||
        )
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            r.json(), {"mode": "command", "shellcmd": "shutdown /r /t 5 /f"}
 | 
			
		||||
        )
 | 
			
		||||
        reload_nats.assert_not_called()
 | 
			
		||||
 | 
			
		||||
        baker.make("agents.RecoveryAction", agent=agent, mode="rpc")
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(r.json(), {"mode": "rpc", "shellcmd": ""})
 | 
			
		||||
        reload_nats.assert_called_once()
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@ from . import views
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
    path("checkrunner/", views.CheckRunner.as_view()),
 | 
			
		||||
    path("<str:agentid>/checkrunner/", views.CheckRunner.as_view()),
 | 
			
		||||
    path("<str:agentid>/runchecks/", views.RunChecks.as_view()),
 | 
			
		||||
    path("<str:agentid>/checkinterval/", views.CheckRunnerInterval.as_view()),
 | 
			
		||||
    path("<int:pk>/<str:agentid>/taskrunner/", views.TaskRunner.as_view()),
 | 
			
		||||
    path("meshexe/", views.MeshExe.as_view()),
 | 
			
		||||
@@ -17,4 +18,6 @@ urlpatterns = [
 | 
			
		||||
    path("choco/", views.Choco.as_view()),
 | 
			
		||||
    path("winupdates/", views.WinUpdates.as_view()),
 | 
			
		||||
    path("superseded/", views.SupersededWinUpdate.as_view()),
 | 
			
		||||
    path("<int:pk>/chocoresult/", views.ChocoResult.as_view()),
 | 
			
		||||
    path("<str:agentid>/recovery/", views.AgentRecovery.as_view()),
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -22,6 +22,7 @@ from autotasks.serializers import TaskGOGetSerializer, TaskRunnerPatchSerializer
 | 
			
		||||
from checks.models import Check
 | 
			
		||||
from checks.serializers import CheckRunnerGetSerializer
 | 
			
		||||
from checks.utils import bytes2human
 | 
			
		||||
from logs.models import PendingAction
 | 
			
		||||
from software.models import InstalledSoftware
 | 
			
		||||
from tacticalrmm.utils import SoftwareList, filter_software, notify_error, reload_nats
 | 
			
		||||
from winupdate.models import WinUpdate, WinUpdatePolicy
 | 
			
		||||
@@ -35,6 +36,8 @@ class CheckIn(APIView):
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
    def patch(self, request):
 | 
			
		||||
        from alerts.models import Alert
 | 
			
		||||
 | 
			
		||||
        updated = False
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
			
		||||
        if pyver.parse(request.data["version"]) > pyver.parse(
 | 
			
		||||
@@ -50,26 +53,20 @@ class CheckIn(APIView):
 | 
			
		||||
        # change agent update pending status to completed if agent has just updated
 | 
			
		||||
        if (
 | 
			
		||||
            updated
 | 
			
		||||
            and agent.pendingactions.filter(
 | 
			
		||||
            and agent.pendingactions.filter(  # type: ignore
 | 
			
		||||
                action_type="agentupdate", status="pending"
 | 
			
		||||
            ).exists()
 | 
			
		||||
        ):
 | 
			
		||||
            agent.pendingactions.filter(
 | 
			
		||||
            agent.pendingactions.filter(  # type: ignore
 | 
			
		||||
                action_type="agentupdate", status="pending"
 | 
			
		||||
            ).update(status="completed")
 | 
			
		||||
 | 
			
		||||
        # handles any alerting actions
 | 
			
		||||
        agent.handle_alert(checkin=True)
 | 
			
		||||
 | 
			
		||||
        recovery = agent.recoveryactions.filter(last_run=None).last()
 | 
			
		||||
        if recovery is not None:
 | 
			
		||||
            recovery.last_run = djangotime.now()
 | 
			
		||||
            recovery.save(update_fields=["last_run"])
 | 
			
		||||
            handle_agent_recovery_task.delay(pk=recovery.pk)
 | 
			
		||||
            return Response("ok")
 | 
			
		||||
        if Alert.objects.filter(agent=agent, resolved=False).exists():
 | 
			
		||||
            Alert.handle_alert_resolve(agent)
 | 
			
		||||
 | 
			
		||||
        # get any pending actions
 | 
			
		||||
        if agent.pendingactions.filter(status="pending").exists():
 | 
			
		||||
        if agent.pendingactions.filter(status="pending").exists():  # type: ignore
 | 
			
		||||
            agent.handle_pending_actions()
 | 
			
		||||
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
@@ -111,7 +108,7 @@ class CheckIn(APIView):
 | 
			
		||||
            if not InstalledSoftware.objects.filter(agent=agent).exists():
 | 
			
		||||
                InstalledSoftware(agent=agent, software=sw).save()
 | 
			
		||||
            else:
 | 
			
		||||
                s = agent.installedsoftware_set.first()
 | 
			
		||||
                s = agent.installedsoftware_set.first()  # type: ignore
 | 
			
		||||
                s.software = sw
 | 
			
		||||
                s.save(update_fields=["software"])
 | 
			
		||||
 | 
			
		||||
@@ -184,7 +181,7 @@ class WinUpdates(APIView):
 | 
			
		||||
 | 
			
		||||
    def patch(self, request):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
			
		||||
        u = agent.winupdates.filter(guid=request.data["guid"]).last()
 | 
			
		||||
        u = agent.winupdates.filter(guid=request.data["guid"]).last()  # type: ignore
 | 
			
		||||
        success: bool = request.data["success"]
 | 
			
		||||
        if success:
 | 
			
		||||
            u.result = "success"
 | 
			
		||||
@@ -210,8 +207,8 @@ class WinUpdates(APIView):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
			
		||||
        updates = request.data["wua_updates"]
 | 
			
		||||
        for update in updates:
 | 
			
		||||
            if agent.winupdates.filter(guid=update["guid"]).exists():
 | 
			
		||||
                u = agent.winupdates.filter(guid=update["guid"]).last()
 | 
			
		||||
            if agent.winupdates.filter(guid=update["guid"]).exists():  # type: ignore
 | 
			
		||||
                u = agent.winupdates.filter(guid=update["guid"]).last()  # type: ignore
 | 
			
		||||
                u.downloaded = update["downloaded"]
 | 
			
		||||
                u.installed = update["installed"]
 | 
			
		||||
                u.save(update_fields=["downloaded", "installed"])
 | 
			
		||||
@@ -242,7 +239,7 @@ class WinUpdates(APIView):
 | 
			
		||||
 | 
			
		||||
        # more superseded updates cleanup
 | 
			
		||||
        if pyver.parse(agent.version) <= pyver.parse("1.4.2"):
 | 
			
		||||
            for u in agent.winupdates.filter(
 | 
			
		||||
            for u in agent.winupdates.filter(  # type: ignore
 | 
			
		||||
                date_installed__isnull=True, result="failed"
 | 
			
		||||
            ).exclude(installed=True):
 | 
			
		||||
                u.delete()
 | 
			
		||||
@@ -256,25 +253,20 @@ class SupersededWinUpdate(APIView):
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=request.data["agent_id"])
 | 
			
		||||
        updates = agent.winupdates.filter(guid=request.data["guid"])
 | 
			
		||||
        updates = agent.winupdates.filter(guid=request.data["guid"])  # type: ignore
 | 
			
		||||
        for u in updates:
 | 
			
		||||
            u.delete()
 | 
			
		||||
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CheckRunner(APIView):
 | 
			
		||||
    """
 | 
			
		||||
    For the windows golang agent
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
class RunChecks(APIView):
 | 
			
		||||
    authentication_classes = [TokenAuthentication]
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, agentid):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=agentid)
 | 
			
		||||
        checks = Check.objects.filter(agent__pk=agent.pk, overriden_by_policy=False)
 | 
			
		||||
 | 
			
		||||
        ret = {
 | 
			
		||||
            "agent": agent.pk,
 | 
			
		||||
            "check_interval": agent.check_interval,
 | 
			
		||||
@@ -282,6 +274,42 @@ class CheckRunner(APIView):
 | 
			
		||||
        }
 | 
			
		||||
        return Response(ret)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CheckRunner(APIView):
 | 
			
		||||
    authentication_classes = [TokenAuthentication]
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, agentid):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=agentid)
 | 
			
		||||
        checks = agent.agentchecks.filter(overriden_by_policy=False)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        run_list = [
 | 
			
		||||
            check
 | 
			
		||||
            for check in checks
 | 
			
		||||
            # always run if check hasn't run yet
 | 
			
		||||
            if not check.last_run
 | 
			
		||||
            # if a check interval is set, see if the correct amount of seconds have passed
 | 
			
		||||
            or (
 | 
			
		||||
                check.run_interval
 | 
			
		||||
                and (
 | 
			
		||||
                    check.last_run
 | 
			
		||||
                    < djangotime.now()
 | 
			
		||||
                    - djangotime.timedelta(seconds=check.run_interval)
 | 
			
		||||
                )
 | 
			
		||||
                # if check interval isn't set, make sure the agent's check interval has passed before running
 | 
			
		||||
            )
 | 
			
		||||
            or (
 | 
			
		||||
                check.last_run
 | 
			
		||||
                < djangotime.now() - djangotime.timedelta(seconds=agent.check_interval)
 | 
			
		||||
            )
 | 
			
		||||
        ]
 | 
			
		||||
        ret = {
 | 
			
		||||
            "agent": agent.pk,
 | 
			
		||||
            "check_interval": agent.check_run_interval(),
 | 
			
		||||
            "checks": CheckRunnerGetSerializer(run_list, many=True).data,
 | 
			
		||||
        }
 | 
			
		||||
        return Response(ret)
 | 
			
		||||
 | 
			
		||||
    def patch(self, request):
 | 
			
		||||
        check = get_object_or_404(Check, pk=request.data["id"])
 | 
			
		||||
        check.last_run = djangotime.now()
 | 
			
		||||
@@ -297,14 +325,13 @@ class CheckRunnerInterval(APIView):
 | 
			
		||||
 | 
			
		||||
    def get(self, request, agentid):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=agentid)
 | 
			
		||||
        return Response({"agent": agent.pk, "check_interval": agent.check_interval})
 | 
			
		||||
 | 
			
		||||
        return Response(
 | 
			
		||||
            {"agent": agent.pk, "check_interval": agent.check_run_interval()}
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TaskRunner(APIView):
 | 
			
		||||
    """
 | 
			
		||||
    For the windows golang agent
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    authentication_classes = [TokenAuthentication]
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
@@ -314,6 +341,7 @@ class TaskRunner(APIView):
 | 
			
		||||
        return Response(TaskGOGetSerializer(task).data)
 | 
			
		||||
 | 
			
		||||
    def patch(self, request, pk, agentid):
 | 
			
		||||
        from alerts.models import Alert
 | 
			
		||||
        from logs.models import AuditLog
 | 
			
		||||
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=agentid)
 | 
			
		||||
@@ -325,8 +353,17 @@ class TaskRunner(APIView):
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        serializer.save(last_run=djangotime.now())
 | 
			
		||||
 | 
			
		||||
        new_task = AutomatedTask.objects.get(pk=task.pk)
 | 
			
		||||
        new_task.handle_alert()
 | 
			
		||||
        status = "failing" if task.retcode != 0 else "passing"
 | 
			
		||||
 | 
			
		||||
        new_task: AutomatedTask = AutomatedTask.objects.get(pk=task.pk)
 | 
			
		||||
        new_task.status = status
 | 
			
		||||
        new_task.save()
 | 
			
		||||
 | 
			
		||||
        if status == "passing":
 | 
			
		||||
            if Alert.objects.filter(assigned_task=new_task, resolved=False).exists():
 | 
			
		||||
                Alert.handle_alert_resolve(new_task)
 | 
			
		||||
        else:
 | 
			
		||||
            Alert.handle_alert_failure(new_task)
 | 
			
		||||
 | 
			
		||||
        AuditLog.objects.create(
 | 
			
		||||
            username=agent.hostname,
 | 
			
		||||
@@ -404,10 +441,10 @@ class NewAgent(APIView):
 | 
			
		||||
        agent.salt_id = f"{agent.hostname}-{agent.pk}"
 | 
			
		||||
        agent.save(update_fields=["salt_id"])
 | 
			
		||||
 | 
			
		||||
        user = User.objects.create_user(
 | 
			
		||||
        user = User.objects.create_user(  # type: ignore
 | 
			
		||||
            username=request.data["agent_id"],
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            password=User.objects.make_random_password(60),
 | 
			
		||||
            password=User.objects.make_random_password(60),  # type: ignore
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        token = Token.objects.create(user=user)
 | 
			
		||||
@@ -452,7 +489,7 @@ class Software(APIView):
 | 
			
		||||
        if not InstalledSoftware.objects.filter(agent=agent).exists():
 | 
			
		||||
            InstalledSoftware(agent=agent, software=sw).save()
 | 
			
		||||
        else:
 | 
			
		||||
            s = agent.installedsoftware_set.first()
 | 
			
		||||
            s = agent.installedsoftware_set.first()  # type: ignore
 | 
			
		||||
            s.software = sw
 | 
			
		||||
            s.save(update_fields=["software"])
 | 
			
		||||
 | 
			
		||||
@@ -475,3 +512,59 @@ class Installer(APIView):
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ChocoResult(APIView):
 | 
			
		||||
    authentication_classes = [TokenAuthentication]
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
    def patch(self, request, pk):
 | 
			
		||||
        action = get_object_or_404(PendingAction, pk=pk)
 | 
			
		||||
        results: str = request.data["results"]
 | 
			
		||||
 | 
			
		||||
        software_name = action.details["name"].lower()
 | 
			
		||||
        success = [
 | 
			
		||||
            "install",
 | 
			
		||||
            "of",
 | 
			
		||||
            software_name,
 | 
			
		||||
            "was",
 | 
			
		||||
            "successful",
 | 
			
		||||
            "installed",
 | 
			
		||||
        ]
 | 
			
		||||
        duplicate = [software_name, "already", "installed", "--force", "reinstall"]
 | 
			
		||||
        installed = False
 | 
			
		||||
 | 
			
		||||
        if all(x in results.lower() for x in success):
 | 
			
		||||
            installed = True
 | 
			
		||||
        elif all(x in results.lower() for x in duplicate):
 | 
			
		||||
            installed = True
 | 
			
		||||
 | 
			
		||||
        action.details["output"] = results
 | 
			
		||||
        action.details["installed"] = installed
 | 
			
		||||
        action.status = "completed"
 | 
			
		||||
        action.save(update_fields=["details", "status"])
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AgentRecovery(APIView):
 | 
			
		||||
    authentication_classes = [TokenAuthentication]
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
    def get(self, request, agentid):
 | 
			
		||||
        agent = get_object_or_404(Agent, agent_id=agentid)
 | 
			
		||||
        recovery = agent.recoveryactions.filter(last_run=None).last()  # type: ignore
 | 
			
		||||
        ret = {"mode": "pass", "shellcmd": ""}
 | 
			
		||||
        if recovery is None:
 | 
			
		||||
            return Response(ret)
 | 
			
		||||
 | 
			
		||||
        recovery.last_run = djangotime.now()
 | 
			
		||||
        recovery.save(update_fields=["last_run"])
 | 
			
		||||
 | 
			
		||||
        ret["mode"] = recovery.mode
 | 
			
		||||
 | 
			
		||||
        if recovery.mode == "command":
 | 
			
		||||
            ret["shellcmd"] = recovery.command
 | 
			
		||||
        elif recovery.mode == "rpc":
 | 
			
		||||
            reload_nats()
 | 
			
		||||
 | 
			
		||||
        return Response(ret)
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,30 @@
 | 
			
		||||
# Generated by Django 3.1.7 on 2021-03-02 04:15
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('agents', '0030_agent_offline_time'),
 | 
			
		||||
        ('clients', '0009_auto_20210212_1408'),
 | 
			
		||||
        ('automation', '0007_policy_alert_template'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='policy',
 | 
			
		||||
            name='excluded_agents',
 | 
			
		||||
            field=models.ManyToManyField(blank=True, related_name='policy_exclusions', to='agents.Agent'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='policy',
 | 
			
		||||
            name='excluded_clients',
 | 
			
		||||
            field=models.ManyToManyField(blank=True, related_name='policy_exclusions', to='clients.Client'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='policy',
 | 
			
		||||
            name='excluded_sites',
 | 
			
		||||
            field=models.ManyToManyField(blank=True, related_name='policy_exclusions', to='clients.Site'),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -17,8 +17,18 @@ class Policy(BaseAuditModel):
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True,
 | 
			
		||||
    )
 | 
			
		||||
    excluded_sites = models.ManyToManyField(
 | 
			
		||||
        "clients.Site", related_name="policy_exclusions", blank=True
 | 
			
		||||
    )
 | 
			
		||||
    excluded_clients = models.ManyToManyField(
 | 
			
		||||
        "clients.Client", related_name="policy_exclusions", blank=True
 | 
			
		||||
    )
 | 
			
		||||
    excluded_agents = models.ManyToManyField(
 | 
			
		||||
        "agents.Agent", related_name="policy_exclusions", blank=True
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def save(self, *args, **kwargs):
 | 
			
		||||
        from alerts.tasks import cache_agents_alert_template
 | 
			
		||||
        from automation.tasks import generate_agent_checks_from_policies_task
 | 
			
		||||
 | 
			
		||||
        # get old policy if exists
 | 
			
		||||
@@ -33,6 +43,9 @@ class Policy(BaseAuditModel):
 | 
			
		||||
                    create_tasks=True,
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            if old_policy.alert_template != self.alert_template:
 | 
			
		||||
                cache_agents_alert_template.delay()
 | 
			
		||||
 | 
			
		||||
    def delete(self, *args, **kwargs):
 | 
			
		||||
        from automation.tasks import generate_agent_checks_task
 | 
			
		||||
 | 
			
		||||
@@ -43,28 +56,50 @@ class Policy(BaseAuditModel):
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def is_default_server_policy(self):
 | 
			
		||||
        return self.default_server_policy.exists()
 | 
			
		||||
        return self.default_server_policy.exists()  # type: ignore
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def is_default_workstation_policy(self):
 | 
			
		||||
        return self.default_workstation_policy.exists()
 | 
			
		||||
        return self.default_workstation_policy.exists()  # type: ignore
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.name
 | 
			
		||||
 | 
			
		||||
    def is_agent_excluded(self, agent):
 | 
			
		||||
        return (
 | 
			
		||||
            agent in self.excluded_agents.all()
 | 
			
		||||
            or agent.site in self.excluded_sites.all()
 | 
			
		||||
            or agent.client in self.excluded_clients.all()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def related_agents(self):
 | 
			
		||||
        return self.get_related("server") | self.get_related("workstation")
 | 
			
		||||
 | 
			
		||||
    def get_related(self, mon_type):
 | 
			
		||||
        explicit_agents = self.agents.filter(monitoring_type=mon_type)
 | 
			
		||||
        explicit_clients = getattr(self, f"{mon_type}_clients").all()
 | 
			
		||||
        explicit_sites = getattr(self, f"{mon_type}_sites").all()
 | 
			
		||||
        explicit_agents = (
 | 
			
		||||
            self.agents.filter(monitoring_type=mon_type)  # type: ignore
 | 
			
		||||
            .exclude(
 | 
			
		||||
                pk__in=self.excluded_agents.only("pk").values_list("pk", flat=True)
 | 
			
		||||
            )
 | 
			
		||||
            .exclude(site__in=self.excluded_sites.all())
 | 
			
		||||
            .exclude(site__client__in=self.excluded_clients.all())
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        explicit_clients = getattr(self, f"{mon_type}_clients").exclude(
 | 
			
		||||
            pk__in=self.excluded_clients.all()
 | 
			
		||||
        )
 | 
			
		||||
        explicit_sites = getattr(self, f"{mon_type}_sites").exclude(
 | 
			
		||||
            pk__in=self.excluded_sites.all()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        filtered_agents_pks = Policy.objects.none()
 | 
			
		||||
 | 
			
		||||
        filtered_agents_pks |= Agent.objects.filter(
 | 
			
		||||
            site__in=[
 | 
			
		||||
                site for site in explicit_sites if site.client not in explicit_clients
 | 
			
		||||
                site
 | 
			
		||||
                for site in explicit_sites
 | 
			
		||||
                if site.client not in explicit_clients
 | 
			
		||||
                and site.client not in self.excluded_clients.all()
 | 
			
		||||
            ],
 | 
			
		||||
            monitoring_type=mon_type,
 | 
			
		||||
        ).values_list("pk", flat=True)
 | 
			
		||||
@@ -119,23 +154,39 @@ class Policy(BaseAuditModel):
 | 
			
		||||
            client_policy = client.workstation_policy
 | 
			
		||||
            site_policy = site.workstation_policy
 | 
			
		||||
 | 
			
		||||
        if agent_policy and agent_policy.active:
 | 
			
		||||
        if (
 | 
			
		||||
            agent_policy
 | 
			
		||||
            and agent_policy.active
 | 
			
		||||
            and not agent_policy.is_agent_excluded(agent)
 | 
			
		||||
        ):
 | 
			
		||||
            for task in agent_policy.autotasks.all():
 | 
			
		||||
                if task.pk not in added_task_pks:
 | 
			
		||||
                    tasks.append(task)
 | 
			
		||||
                    added_task_pks.append(task.pk)
 | 
			
		||||
        if site_policy and site_policy.active:
 | 
			
		||||
        if (
 | 
			
		||||
            site_policy
 | 
			
		||||
            and site_policy.active
 | 
			
		||||
            and not site_policy.is_agent_excluded(agent)
 | 
			
		||||
        ):
 | 
			
		||||
            for task in site_policy.autotasks.all():
 | 
			
		||||
                if task.pk not in added_task_pks:
 | 
			
		||||
                    tasks.append(task)
 | 
			
		||||
                    added_task_pks.append(task.pk)
 | 
			
		||||
        if client_policy and client_policy.active:
 | 
			
		||||
        if (
 | 
			
		||||
            client_policy
 | 
			
		||||
            and client_policy.active
 | 
			
		||||
            and not client_policy.is_agent_excluded(agent)
 | 
			
		||||
        ):
 | 
			
		||||
            for task in client_policy.autotasks.all():
 | 
			
		||||
                if task.pk not in added_task_pks:
 | 
			
		||||
                    tasks.append(task)
 | 
			
		||||
                    added_task_pks.append(task.pk)
 | 
			
		||||
 | 
			
		||||
        if default_policy and default_policy.active:
 | 
			
		||||
        if (
 | 
			
		||||
            default_policy
 | 
			
		||||
            and default_policy.active
 | 
			
		||||
            and not default_policy.is_agent_excluded(agent)
 | 
			
		||||
        ):
 | 
			
		||||
            for task in default_policy.autotasks.all():
 | 
			
		||||
                if task.pk not in added_task_pks:
 | 
			
		||||
                    tasks.append(task)
 | 
			
		||||
@@ -205,7 +256,11 @@ class Policy(BaseAuditModel):
 | 
			
		||||
        enforced_checks = list()
 | 
			
		||||
        policy_checks = list()
 | 
			
		||||
 | 
			
		||||
        if agent_policy and agent_policy.active:
 | 
			
		||||
        if (
 | 
			
		||||
            agent_policy
 | 
			
		||||
            and agent_policy.active
 | 
			
		||||
            and not agent_policy.is_agent_excluded(agent)
 | 
			
		||||
        ):
 | 
			
		||||
            if agent_policy.enforced:
 | 
			
		||||
                for check in agent_policy.policychecks.all():
 | 
			
		||||
                    enforced_checks.append(check)
 | 
			
		||||
@@ -213,7 +268,11 @@ class Policy(BaseAuditModel):
 | 
			
		||||
                for check in agent_policy.policychecks.all():
 | 
			
		||||
                    policy_checks.append(check)
 | 
			
		||||
 | 
			
		||||
        if site_policy and site_policy.active:
 | 
			
		||||
        if (
 | 
			
		||||
            site_policy
 | 
			
		||||
            and site_policy.active
 | 
			
		||||
            and not site_policy.is_agent_excluded(agent)
 | 
			
		||||
        ):
 | 
			
		||||
            if site_policy.enforced:
 | 
			
		||||
                for check in site_policy.policychecks.all():
 | 
			
		||||
                    enforced_checks.append(check)
 | 
			
		||||
@@ -221,7 +280,11 @@ class Policy(BaseAuditModel):
 | 
			
		||||
                for check in site_policy.policychecks.all():
 | 
			
		||||
                    policy_checks.append(check)
 | 
			
		||||
 | 
			
		||||
        if client_policy and client_policy.active:
 | 
			
		||||
        if (
 | 
			
		||||
            client_policy
 | 
			
		||||
            and client_policy.active
 | 
			
		||||
            and not client_policy.is_agent_excluded(agent)
 | 
			
		||||
        ):
 | 
			
		||||
            if client_policy.enforced:
 | 
			
		||||
                for check in client_policy.policychecks.all():
 | 
			
		||||
                    enforced_checks.append(check)
 | 
			
		||||
@@ -229,7 +292,11 @@ class Policy(BaseAuditModel):
 | 
			
		||||
                for check in client_policy.policychecks.all():
 | 
			
		||||
                    policy_checks.append(check)
 | 
			
		||||
 | 
			
		||||
        if default_policy and default_policy.active:
 | 
			
		||||
        if (
 | 
			
		||||
            default_policy
 | 
			
		||||
            and default_policy.active
 | 
			
		||||
            and not default_policy.is_agent_excluded(agent)
 | 
			
		||||
        ):
 | 
			
		||||
            if default_policy.enforced:
 | 
			
		||||
                for check in default_policy.policychecks.all():
 | 
			
		||||
                    enforced_checks.append(check)
 | 
			
		||||
 
 | 
			
		||||
@@ -4,9 +4,11 @@ from rest_framework.serializers import (
 | 
			
		||||
    SerializerMethodField,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
from agents.serializers import AgentHostnameSerializer
 | 
			
		||||
from autotasks.models import AutomatedTask
 | 
			
		||||
from checks.models import Check
 | 
			
		||||
from clients.models import Client
 | 
			
		||||
from clients.serializers import ClientSerializer, SiteSerializer
 | 
			
		||||
from winupdate.serializers import WinUpdatePolicySerializer
 | 
			
		||||
 | 
			
		||||
from .models import Policy
 | 
			
		||||
@@ -25,6 +27,9 @@ class PolicyTableSerializer(ModelSerializer):
 | 
			
		||||
    agents_count = SerializerMethodField(read_only=True)
 | 
			
		||||
    winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True)
 | 
			
		||||
    alert_template = ReadOnlyField(source="alert_template.id")
 | 
			
		||||
    excluded_clients = ClientSerializer(many=True)
 | 
			
		||||
    excluded_sites = SiteSerializer(many=True)
 | 
			
		||||
    excluded_agents = AgentHostnameSerializer(many=True)
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Policy
 | 
			
		||||
 
 | 
			
		||||
@@ -79,6 +79,7 @@ def update_policy_check_fields_task(checkpk):
 | 
			
		||||
        error_threshold=check.error_threshold,
 | 
			
		||||
        alert_severity=check.alert_severity,
 | 
			
		||||
        name=check.name,
 | 
			
		||||
        run_interval=check.run_interval,
 | 
			
		||||
        disk=check.disk,
 | 
			
		||||
        fails_b4_alert=check.fails_b4_alert,
 | 
			
		||||
        ip=check.ip,
 | 
			
		||||
@@ -98,6 +99,7 @@ def update_policy_check_fields_task(checkpk):
 | 
			
		||||
        event_message=check.event_message,
 | 
			
		||||
        fail_when=check.fail_when,
 | 
			
		||||
        search_last_days=check.search_last_days,
 | 
			
		||||
        number_of_events_b4_alert=check.number_of_events_b4_alert,
 | 
			
		||||
        email_alert=check.email_alert,
 | 
			
		||||
        text_alert=check.text_alert,
 | 
			
		||||
        dashboard_alert=check.dashboard_alert,
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@ from unittest.mock import patch
 | 
			
		||||
from model_bakery import baker, seq
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
from core.models import CoreSettings
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
from winupdate.models import WinUpdatePolicy
 | 
			
		||||
 | 
			
		||||
@@ -31,7 +32,7 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
        serializer = PolicyTableSerializer(policies, many=True)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
@@ -41,13 +42,13 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(resp.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        policy = baker.make("automation.Policy")
 | 
			
		||||
        url = f"/automation/policies/{policy.pk}/"
 | 
			
		||||
        url = f"/automation/policies/{policy.pk}/"  # type: ignore
 | 
			
		||||
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
        serializer = PolicySerializer(policy)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
@@ -79,13 +80,13 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
            "desc": "policy desc",
 | 
			
		||||
            "active": True,
 | 
			
		||||
            "enforced": False,
 | 
			
		||||
            "copyId": policy.pk,
 | 
			
		||||
            "copyId": policy.pk,  # type: ignore
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resp = self.client.post(f"/automation/policies/", data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(policy.autotasks.count(), 3)
 | 
			
		||||
        self.assertEqual(policy.policychecks.count(), 7)
 | 
			
		||||
        self.assertEqual(policy.autotasks.count(), 3)  # type: ignore
 | 
			
		||||
        self.assertEqual(policy.policychecks.count(), 7)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
@@ -96,7 +97,7 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(resp.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        policy = baker.make("automation.Policy", active=True, enforced=False)
 | 
			
		||||
        url = f"/automation/policies/{policy.pk}/"
 | 
			
		||||
        url = f"/automation/policies/{policy.pk}/"  # type: ignore
 | 
			
		||||
 | 
			
		||||
        data = {
 | 
			
		||||
            "name": "Test Policy Update",
 | 
			
		||||
@@ -121,7 +122,7 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
        resp = self.client.put(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        generate_agent_checks_from_policies_task.assert_called_with(
 | 
			
		||||
            policypk=policy.pk, create_tasks=True
 | 
			
		||||
            policypk=policy.pk, create_tasks=True  # type: ignore
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("put", url)
 | 
			
		||||
@@ -138,7 +139,7 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
        agents = baker.make_recipe(
 | 
			
		||||
            "agents.agent", site=site, policy=policy, _quantity=3
 | 
			
		||||
        )
 | 
			
		||||
        url = f"/automation/policies/{policy.pk}/"
 | 
			
		||||
        url = f"/automation/policies/{policy.pk}/"  # type: ignore
 | 
			
		||||
 | 
			
		||||
        resp = self.client.delete(url, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
@@ -153,14 +154,14 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
        # create policy with tasks
 | 
			
		||||
        policy = baker.make("automation.Policy")
 | 
			
		||||
        tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
 | 
			
		||||
        url = f"/automation/{policy.pk}/policyautomatedtasks/"
 | 
			
		||||
        url = f"/automation/{policy.pk}/policyautomatedtasks/"  # type: ignore
 | 
			
		||||
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
        serializer = AutoTasksFieldSerializer(tasks, many=True)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)
 | 
			
		||||
        self.assertEqual(len(resp.data), 3)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)  # type: ignore
 | 
			
		||||
        self.assertEqual(len(resp.data), 3)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
@@ -170,14 +171,14 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
        policy = baker.make("automation.Policy")
 | 
			
		||||
        checks = self.create_checks(policy=policy)
 | 
			
		||||
 | 
			
		||||
        url = f"/automation/{policy.pk}/policychecks/"
 | 
			
		||||
        url = f"/automation/{policy.pk}/policychecks/"  # type: ignore
 | 
			
		||||
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
        serializer = PolicyCheckSerializer(checks, many=True)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)
 | 
			
		||||
        self.assertEqual(len(resp.data), 7)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)  # type: ignore
 | 
			
		||||
        self.assertEqual(len(resp.data), 7)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
@@ -199,7 +200,7 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
        serializer = PolicyCheckStatusSerializer([managed_check], many=True)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)  # type: ignore
 | 
			
		||||
        self.check_not_authenticated("patch", url)
 | 
			
		||||
 | 
			
		||||
    def test_policy_overview(self):
 | 
			
		||||
@@ -212,40 +213,40 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
        )
 | 
			
		||||
        clients = baker.make(
 | 
			
		||||
            "clients.Client",
 | 
			
		||||
            server_policy=cycle(policies),
 | 
			
		||||
            workstation_policy=cycle(policies),
 | 
			
		||||
            server_policy=cycle(policies),  # type: ignore
 | 
			
		||||
            workstation_policy=cycle(policies),  # type: ignore
 | 
			
		||||
            _quantity=5,
 | 
			
		||||
        )
 | 
			
		||||
        baker.make(
 | 
			
		||||
            "clients.Site",
 | 
			
		||||
            client=cycle(clients),
 | 
			
		||||
            server_policy=cycle(policies),
 | 
			
		||||
            workstation_policy=cycle(policies),
 | 
			
		||||
            client=cycle(clients),  # type: ignore
 | 
			
		||||
            server_policy=cycle(policies),  # type: ignore
 | 
			
		||||
            workstation_policy=cycle(policies),  # type: ignore
 | 
			
		||||
            _quantity=4,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        baker.make("clients.Site", client=cycle(clients), _quantity=3)
 | 
			
		||||
        baker.make("clients.Site", client=cycle(clients), _quantity=3)  # type: ignore
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
        clients = Client.objects.all()
 | 
			
		||||
        serializer = PolicyOverviewSerializer(clients, many=True)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    def test_get_related(self):
 | 
			
		||||
        policy = baker.make("automation.Policy")
 | 
			
		||||
        url = f"/automation/policies/{policy.pk}/related/"
 | 
			
		||||
        url = f"/automation/policies/{policy.pk}/related/"  # type: ignore
 | 
			
		||||
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertIsInstance(resp.data["server_clients"], list)
 | 
			
		||||
        self.assertIsInstance(resp.data["workstation_clients"], list)
 | 
			
		||||
        self.assertIsInstance(resp.data["server_sites"], list)
 | 
			
		||||
        self.assertIsInstance(resp.data["workstation_sites"], list)
 | 
			
		||||
        self.assertIsInstance(resp.data["agents"], list)
 | 
			
		||||
        self.assertIsInstance(resp.data["server_clients"], list)  # type: ignore
 | 
			
		||||
        self.assertIsInstance(resp.data["workstation_clients"], list)  # type: ignore
 | 
			
		||||
        self.assertIsInstance(resp.data["server_sites"], list)  # type: ignore
 | 
			
		||||
        self.assertIsInstance(resp.data["workstation_sites"], list)  # type: ignore
 | 
			
		||||
        self.assertIsInstance(resp.data["agents"], list)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
@@ -257,16 +258,16 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        # create policy managed tasks
 | 
			
		||||
        policy_tasks = baker.make(
 | 
			
		||||
            "autotasks.AutomatedTask", parent_task=task.id, _quantity=5
 | 
			
		||||
            "autotasks.AutomatedTask", parent_task=task.id, _quantity=5  # type: ignore
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        url = f"/automation/policyautomatedtaskstatus/{task.id}/task/"
 | 
			
		||||
        url = f"/automation/policyautomatedtaskstatus/{task.id}/task/"  # type: ignore
 | 
			
		||||
 | 
			
		||||
        serializer = PolicyTaskStatusSerializer(policy_tasks, many=True)
 | 
			
		||||
        resp = self.client.patch(url, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)
 | 
			
		||||
        self.assertEqual(len(resp.data), 5)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)  # type: ignore
 | 
			
		||||
        self.assertEqual(len(resp.data), 5)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("patch", url)
 | 
			
		||||
 | 
			
		||||
@@ -284,7 +285,7 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
        resp = self.client.put(url, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        mock_task.assert_called_once_with([task.pk for task in tasks])
 | 
			
		||||
        mock_task.assert_called_once_with([task.pk for task in tasks])  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("put", url)
 | 
			
		||||
 | 
			
		||||
@@ -299,7 +300,7 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
        policy = baker.make("automation.Policy")
 | 
			
		||||
 | 
			
		||||
        data = {
 | 
			
		||||
            "policy": policy.pk,
 | 
			
		||||
            "policy": policy.pk,  # type: ignore
 | 
			
		||||
            "critical": "approve",
 | 
			
		||||
            "important": "approve",
 | 
			
		||||
            "moderate": "ignore",
 | 
			
		||||
@@ -325,11 +326,11 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        policy = baker.make("automation.Policy")
 | 
			
		||||
        patch_policy = baker.make("winupdate.WinUpdatePolicy", policy=policy)
 | 
			
		||||
        url = f"/automation/winupdatepolicy/{patch_policy.pk}/"
 | 
			
		||||
        url = f"/automation/winupdatepolicy/{patch_policy.pk}/"  # type: ignore
 | 
			
		||||
 | 
			
		||||
        data = {
 | 
			
		||||
            "id": patch_policy.pk,
 | 
			
		||||
            "policy": policy.pk,
 | 
			
		||||
            "id": patch_policy.pk,  # type: ignore
 | 
			
		||||
            "policy": policy.pk,  # type: ignore
 | 
			
		||||
            "critical": "approve",
 | 
			
		||||
            "important": "approve",
 | 
			
		||||
            "moderate": "ignore",
 | 
			
		||||
@@ -358,10 +359,10 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        clients = baker.make("clients.Client", _quantity=6)
 | 
			
		||||
        sites = baker.make("clients.Site", client=cycle(clients), _quantity=10)
 | 
			
		||||
        sites = baker.make("clients.Site", client=cycle(clients), _quantity=10)  # type: ignore
 | 
			
		||||
        agents = baker.make_recipe(
 | 
			
		||||
            "agents.agent",
 | 
			
		||||
            site=cycle(sites),
 | 
			
		||||
            site=cycle(sites),  # type: ignore
 | 
			
		||||
            _quantity=6,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@@ -371,24 +372,24 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # test reset agents in site
 | 
			
		||||
        data = {"site": sites[0].id}
 | 
			
		||||
        data = {"site": sites[0].id}  # type: ignore
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        agents = Agent.objects.filter(site=sites[0])
 | 
			
		||||
        agents = Agent.objects.filter(site=sites[0])  # type: ignore
 | 
			
		||||
 | 
			
		||||
        for agent in agents:
 | 
			
		||||
            for k, v in inherit_fields.items():
 | 
			
		||||
                self.assertEqual(getattr(agent.winupdatepolicy.get(), k), v)
 | 
			
		||||
 | 
			
		||||
        # test reset agents in client
 | 
			
		||||
        data = {"client": clients[1].id}
 | 
			
		||||
        data = {"client": clients[1].id}  # type: ignore
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        agents = Agent.objects.filter(site__client=clients[1])
 | 
			
		||||
        agents = Agent.objects.filter(site__client=clients[1])  # type: ignore
 | 
			
		||||
 | 
			
		||||
        for agent in agents:
 | 
			
		||||
            for k, v in inherit_fields.items():
 | 
			
		||||
@@ -425,6 +426,25 @@ class TestPolicyViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("delete", url)
 | 
			
		||||
 | 
			
		||||
    @patch("automation.tasks.generate_agent_checks_from_policies_task.delay")
 | 
			
		||||
    def test_sync_policy(self, generate_checks):
 | 
			
		||||
        url = "/automation/sync/"
 | 
			
		||||
 | 
			
		||||
        # test invalid data
 | 
			
		||||
        data = {"invalid": 7}
 | 
			
		||||
 | 
			
		||||
        resp = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 400)
 | 
			
		||||
 | 
			
		||||
        policy = baker.make("automation.Policy", active=True)
 | 
			
		||||
        data = {"policy": policy.pk}  # type: ignore
 | 
			
		||||
 | 
			
		||||
        resp = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        generate_checks.assert_called_with(policy.pk, create_tasks=True)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
@@ -435,46 +455,46 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        # Get Site and Client from an agent in list
 | 
			
		||||
        clients = baker.make("clients.Client", _quantity=5)
 | 
			
		||||
        sites = baker.make("clients.Site", client=cycle(clients), _quantity=25)
 | 
			
		||||
        sites = baker.make("clients.Site", client=cycle(clients), _quantity=25)  # type: ignore
 | 
			
		||||
        server_agents = baker.make_recipe(
 | 
			
		||||
            "agents.server_agent",
 | 
			
		||||
            site=cycle(sites),
 | 
			
		||||
            site=cycle(sites),  # type: ignore
 | 
			
		||||
            _quantity=25,
 | 
			
		||||
        )
 | 
			
		||||
        workstation_agents = baker.make_recipe(
 | 
			
		||||
            "agents.workstation_agent",
 | 
			
		||||
            site=cycle(sites),
 | 
			
		||||
            site=cycle(sites),  # type: ignore
 | 
			
		||||
            _quantity=25,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        policy = baker.make("automation.Policy", active=True)
 | 
			
		||||
 | 
			
		||||
        # Add Client to Policy
 | 
			
		||||
        policy.server_clients.add(server_agents[13].client)
 | 
			
		||||
        policy.workstation_clients.add(workstation_agents[15].client)
 | 
			
		||||
        policy.server_clients.add(server_agents[13].client)  # type: ignore
 | 
			
		||||
        policy.workstation_clients.add(workstation_agents[15].client)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        resp = self.client.get(
 | 
			
		||||
            f"/automation/policies/{policy.pk}/related/", format="json"
 | 
			
		||||
            f"/automation/policies/{policy.pk}/related/", format="json"  # type: ignore
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEquals(len(resp.data["server_clients"]), 1)
 | 
			
		||||
        self.assertEquals(len(resp.data["server_sites"]), 5)
 | 
			
		||||
        self.assertEquals(len(resp.data["workstation_clients"]), 1)
 | 
			
		||||
        self.assertEquals(len(resp.data["workstation_sites"]), 5)
 | 
			
		||||
        self.assertEquals(len(resp.data["agents"]), 10)
 | 
			
		||||
        self.assertEquals(len(resp.data["server_clients"]), 1)  # type: ignore
 | 
			
		||||
        self.assertEquals(len(resp.data["server_sites"]), 5)  # type: ignore
 | 
			
		||||
        self.assertEquals(len(resp.data["workstation_clients"]), 1)  # type: ignore
 | 
			
		||||
        self.assertEquals(len(resp.data["workstation_sites"]), 5)  # type: ignore
 | 
			
		||||
        self.assertEquals(len(resp.data["agents"]), 10)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # Add Site to Policy and the agents and sites length shouldn't change
 | 
			
		||||
        policy.server_sites.add(server_agents[13].site)
 | 
			
		||||
        policy.workstation_sites.add(workstation_agents[15].site)
 | 
			
		||||
        self.assertEquals(len(resp.data["server_sites"]), 5)
 | 
			
		||||
        self.assertEquals(len(resp.data["workstation_sites"]), 5)
 | 
			
		||||
        self.assertEquals(len(resp.data["agents"]), 10)
 | 
			
		||||
        policy.server_sites.add(server_agents[13].site)  # type: ignore
 | 
			
		||||
        policy.workstation_sites.add(workstation_agents[15].site)  # type: ignore
 | 
			
		||||
        self.assertEquals(len(resp.data["server_sites"]), 5)  # type: ignore
 | 
			
		||||
        self.assertEquals(len(resp.data["workstation_sites"]), 5)  # type: ignore
 | 
			
		||||
        self.assertEquals(len(resp.data["agents"]), 10)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # Add Agent to Policy and the agents length shouldn't change
 | 
			
		||||
        policy.agents.add(server_agents[13])
 | 
			
		||||
        policy.agents.add(workstation_agents[15])
 | 
			
		||||
        self.assertEquals(len(resp.data["agents"]), 10)
 | 
			
		||||
        policy.agents.add(server_agents[13])  # type: ignore
 | 
			
		||||
        policy.agents.add(workstation_agents[15])  # type: ignore
 | 
			
		||||
        self.assertEquals(len(resp.data["agents"]), 10)  # type: ignore
 | 
			
		||||
 | 
			
		||||
    def test_generating_agent_policy_checks(self):
 | 
			
		||||
        from .tasks import generate_agent_checks_from_policies_task
 | 
			
		||||
@@ -485,7 +505,7 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
        agent = baker.make_recipe("agents.agent", policy=policy)
 | 
			
		||||
 | 
			
		||||
        # test policy assigned to agent
 | 
			
		||||
        generate_agent_checks_from_policies_task(policy.id)
 | 
			
		||||
        generate_agent_checks_from_policies_task(policy.id)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # make sure all checks were created. should be 7
 | 
			
		||||
        agent_checks = Agent.objects.get(pk=agent.id).agentchecks.all()
 | 
			
		||||
@@ -505,12 +525,12 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
                self.assertEqual(check.ip, checks[1].ip)
 | 
			
		||||
            elif check.check_type == "cpuload":
 | 
			
		||||
                self.assertEqual(check.parent_check, checks[2].id)
 | 
			
		||||
                self.assertEqual(check.error_threshold, checks[0].error_threshold)
 | 
			
		||||
                self.assertEqual(check.warning_threshold, checks[0].warning_threshold)
 | 
			
		||||
                self.assertEqual(check.error_threshold, checks[2].error_threshold)
 | 
			
		||||
                self.assertEqual(check.warning_threshold, checks[2].warning_threshold)
 | 
			
		||||
            elif check.check_type == "memory":
 | 
			
		||||
                self.assertEqual(check.parent_check, checks[3].id)
 | 
			
		||||
                self.assertEqual(check.error_threshold, checks[0].error_threshold)
 | 
			
		||||
                self.assertEqual(check.warning_threshold, checks[0].warning_threshold)
 | 
			
		||||
                self.assertEqual(check.error_threshold, checks[3].error_threshold)
 | 
			
		||||
                self.assertEqual(check.warning_threshold, checks[3].warning_threshold)
 | 
			
		||||
            elif check.check_type == "winsvc":
 | 
			
		||||
                self.assertEqual(check.parent_check, checks[4].id)
 | 
			
		||||
                self.assertEqual(check.svc_name, checks[4].svc_name)
 | 
			
		||||
@@ -535,7 +555,7 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
        agent = baker.make_recipe("agents.agent", site=site, policy=policy)
 | 
			
		||||
        self.create_checks(agent=agent, script=script)
 | 
			
		||||
 | 
			
		||||
        generate_agent_checks_from_policies_task(policy.id, create_tasks=True)
 | 
			
		||||
        generate_agent_checks_from_policies_task(policy.id, create_tasks=True)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # make sure each agent check says overriden_by_policy
 | 
			
		||||
        self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 14)
 | 
			
		||||
@@ -843,7 +863,7 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(Agent.objects.get(pk=agent.id).agentchecks.count(), 7)
 | 
			
		||||
 | 
			
		||||
        # pick a policy check and delete it from the agent
 | 
			
		||||
        policy_check_id = Policy.objects.get(pk=policy.id).policychecks.first().id
 | 
			
		||||
        policy_check_id = Policy.objects.get(pk=policy.id).policychecks.first().id  # type: ignore
 | 
			
		||||
 | 
			
		||||
        delete_policy_check_task(policy_check_id)
 | 
			
		||||
 | 
			
		||||
@@ -868,7 +888,7 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        # pick a policy check and update it with new values
 | 
			
		||||
        ping_check = (
 | 
			
		||||
            Policy.objects.get(pk=policy.id)
 | 
			
		||||
            Policy.objects.get(pk=policy.id)  # type: ignore
 | 
			
		||||
            .policychecks.filter(check_type="ping")
 | 
			
		||||
            .first()
 | 
			
		||||
        )
 | 
			
		||||
@@ -895,7 +915,7 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
        )
 | 
			
		||||
        agent = baker.make_recipe("agents.server_agent", policy=policy)
 | 
			
		||||
 | 
			
		||||
        generate_agent_tasks_from_policies_task(policy.id)
 | 
			
		||||
        generate_agent_tasks_from_policies_task(policy.id)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        agent_tasks = Agent.objects.get(pk=agent.id).autotasks.all()
 | 
			
		||||
 | 
			
		||||
@@ -905,14 +925,14 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
        for task in agent_tasks:
 | 
			
		||||
            self.assertTrue(task.managed_by_policy)
 | 
			
		||||
            if task.name == "Task1":
 | 
			
		||||
                self.assertEqual(task.parent_task, tasks[0].id)
 | 
			
		||||
                self.assertEqual(task.name, tasks[0].name)
 | 
			
		||||
                self.assertEqual(task.parent_task, tasks[0].id)  # type: ignore
 | 
			
		||||
                self.assertEqual(task.name, tasks[0].name)  # type: ignore
 | 
			
		||||
            if task.name == "Task2":
 | 
			
		||||
                self.assertEqual(task.parent_task, tasks[1].id)
 | 
			
		||||
                self.assertEqual(task.name, tasks[1].name)
 | 
			
		||||
                self.assertEqual(task.parent_task, tasks[1].id)  # type: ignore
 | 
			
		||||
                self.assertEqual(task.name, tasks[1].name)  # type: ignore
 | 
			
		||||
            if task.name == "Task3":
 | 
			
		||||
                self.assertEqual(task.parent_task, tasks[2].id)
 | 
			
		||||
                self.assertEqual(task.name, tasks[2].name)
 | 
			
		||||
                self.assertEqual(task.parent_task, tasks[2].id)  # type: ignore
 | 
			
		||||
                self.assertEqual(task.name, tasks[2].name)  # type: ignore
 | 
			
		||||
 | 
			
		||||
    @patch("autotasks.tasks.delete_win_task_schedule.delay")
 | 
			
		||||
    def test_delete_policy_tasks(self, delete_win_task_schedule):
 | 
			
		||||
@@ -922,10 +942,10 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
        tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=3)
 | 
			
		||||
        agent = baker.make_recipe("agents.server_agent", policy=policy)
 | 
			
		||||
 | 
			
		||||
        delete_policy_autotask_task(tasks[0].id)
 | 
			
		||||
        delete_policy_autotask_task(tasks[0].id)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        delete_win_task_schedule.assert_called_with(
 | 
			
		||||
            agent.autotasks.get(parent_task=tasks[0].id).id
 | 
			
		||||
            agent.autotasks.get(parent_task=tasks[0].id).id  # type: ignore
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @patch("autotasks.tasks.run_win_task.delay")
 | 
			
		||||
@@ -934,12 +954,12 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        tasks = baker.make("autotasks.AutomatedTask", _quantity=3)
 | 
			
		||||
 | 
			
		||||
        run_win_policy_autotask_task([task.id for task in tasks])
 | 
			
		||||
        run_win_policy_autotask_task([task.id for task in tasks])  # type: ignore
 | 
			
		||||
 | 
			
		||||
        run_win_task.side_effect = [task.id for task in tasks]
 | 
			
		||||
        run_win_task.side_effect = [task.id for task in tasks]  # type: ignore
 | 
			
		||||
        self.assertEqual(run_win_task.call_count, 3)
 | 
			
		||||
        for task in tasks:
 | 
			
		||||
            run_win_task.assert_any_call(task.id)
 | 
			
		||||
        for task in tasks:  # type: ignore
 | 
			
		||||
            run_win_task.assert_any_call(task.id)  # type: ignore
 | 
			
		||||
 | 
			
		||||
    @patch("autotasks.tasks.enable_or_disable_win_task.delay")
 | 
			
		||||
    def test_update_policy_tasks(self, enable_or_disable_win_task):
 | 
			
		||||
@@ -952,17 +972,17 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
        )
 | 
			
		||||
        agent = baker.make_recipe("agents.server_agent", policy=policy)
 | 
			
		||||
 | 
			
		||||
        tasks[0].enabled = False
 | 
			
		||||
        tasks[0].save()
 | 
			
		||||
        tasks[0].enabled = False  # type: ignore
 | 
			
		||||
        tasks[0].save()  # type: ignore
 | 
			
		||||
 | 
			
		||||
        update_policy_task_fields_task(tasks[0].id)
 | 
			
		||||
        update_policy_task_fields_task(tasks[0].id)  # type: ignore
 | 
			
		||||
        enable_or_disable_win_task.assert_not_called()
 | 
			
		||||
 | 
			
		||||
        self.assertFalse(agent.autotasks.get(parent_task=tasks[0].id).enabled)
 | 
			
		||||
        self.assertFalse(agent.autotasks.get(parent_task=tasks[0].id).enabled)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        update_policy_task_fields_task(tasks[0].id, update_agent=True)
 | 
			
		||||
        update_policy_task_fields_task(tasks[0].id, update_agent=True)  # type: ignore
 | 
			
		||||
        enable_or_disable_win_task.assert_called_with(
 | 
			
		||||
            agent.autotasks.get(parent_task=tasks[0].id).id, False
 | 
			
		||||
            agent.autotasks.get(parent_task=tasks[0].id).id, False  # type: ignore
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @patch("agents.models.Agent.generate_tasks_from_policies")
 | 
			
		||||
@@ -984,3 +1004,110 @@ class TestPolicyTasks(TacticalTestCase):
 | 
			
		||||
        generate_agent_checks_task([agent.pk for agent in agents], create_tasks=True)
 | 
			
		||||
        self.assertEquals(generate_checks.call_count, 5)
 | 
			
		||||
        self.assertEquals(generate_checks.call_count, 5)
 | 
			
		||||
 | 
			
		||||
    @patch("autotasks.tasks.delete_win_task_schedule.delay")
 | 
			
		||||
    def test_policy_exclusions(self, delete_task):
 | 
			
		||||
        # setup data
 | 
			
		||||
        policy = baker.make("automation.Policy", active=True)
 | 
			
		||||
        baker.make_recipe("checks.memory_check", policy=policy)
 | 
			
		||||
        task = baker.make("autotasks.AutomatedTask", policy=policy)
 | 
			
		||||
        agent = baker.make_recipe(
 | 
			
		||||
            "agents.agent", policy=policy, monitoring_type="server"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # make sure related agents on policy returns correctly
 | 
			
		||||
        self.assertEqual(policy.related_agents().count(), 1)  # type: ignore
 | 
			
		||||
        self.assertEqual(agent.agentchecks.count(), 1)  # type: ignore
 | 
			
		||||
        self.assertEqual(agent.autotasks.count(), 1)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # add agent to policy exclusions
 | 
			
		||||
        policy.excluded_agents.set([agent])  # type: ignore
 | 
			
		||||
 | 
			
		||||
        agent.generate_checks_from_policies()
 | 
			
		||||
        agent.generate_tasks_from_policies()
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(policy.related_agents().count(), 0)  # type: ignore
 | 
			
		||||
        self.assertEqual(agent.agentchecks.count(), 0)  # type: ignore
 | 
			
		||||
        delete_task.assert_called()
 | 
			
		||||
        delete_task.reset_mock()
 | 
			
		||||
 | 
			
		||||
        # delete agent tasks
 | 
			
		||||
        agent.autotasks.all().delete()
 | 
			
		||||
        policy.excluded_agents.clear()  # type: ignore
 | 
			
		||||
 | 
			
		||||
        agent.generate_checks_from_policies()
 | 
			
		||||
        agent.generate_tasks_from_policies()
 | 
			
		||||
 | 
			
		||||
        # make sure related agents on policy returns correctly
 | 
			
		||||
        self.assertEqual(policy.related_agents().count(), 1)  # type: ignore
 | 
			
		||||
        self.assertEqual(agent.agentchecks.count(), 1)  # type: ignore
 | 
			
		||||
        self.assertEqual(agent.autotasks.count(), 1)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # add policy exclusions to site
 | 
			
		||||
        policy.excluded_sites.set([agent.site])  # type: ignore
 | 
			
		||||
 | 
			
		||||
        agent.generate_checks_from_policies()
 | 
			
		||||
        agent.generate_tasks_from_policies()
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(policy.related_agents().count(), 0)  # type: ignore
 | 
			
		||||
        self.assertEqual(agent.agentchecks.count(), 0)  # type: ignore
 | 
			
		||||
        delete_task.assert_called()
 | 
			
		||||
        delete_task.reset_mock()
 | 
			
		||||
 | 
			
		||||
        # delete agent tasks and reset
 | 
			
		||||
        agent.autotasks.all().delete()
 | 
			
		||||
        policy.excluded_sites.clear()  # type: ignore
 | 
			
		||||
 | 
			
		||||
        agent.generate_checks_from_policies()
 | 
			
		||||
        agent.generate_tasks_from_policies()
 | 
			
		||||
 | 
			
		||||
        # make sure related agents on policy returns correctly
 | 
			
		||||
        self.assertEqual(policy.related_agents().count(), 1)  # type: ignore
 | 
			
		||||
        self.assertEqual(agent.agentchecks.count(), 1)  # type: ignore
 | 
			
		||||
        self.assertEqual(agent.autotasks.count(), 1)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # add policy exclusions to client
 | 
			
		||||
        policy.excluded_clients.set([agent.client])  # type: ignore
 | 
			
		||||
 | 
			
		||||
        agent.generate_checks_from_policies()
 | 
			
		||||
        agent.generate_tasks_from_policies()
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(policy.related_agents().count(), 0)  # type: ignore
 | 
			
		||||
        self.assertEqual(agent.agentchecks.count(), 0)  # type: ignore
 | 
			
		||||
        delete_task.assert_called()
 | 
			
		||||
        delete_task.reset_mock()
 | 
			
		||||
 | 
			
		||||
        # delete agent tasks and reset
 | 
			
		||||
        agent.autotasks.all().delete()
 | 
			
		||||
        policy.excluded_clients.clear()  # type: ignore
 | 
			
		||||
        agent.policy = None
 | 
			
		||||
        agent.save()
 | 
			
		||||
 | 
			
		||||
        # test on default policy
 | 
			
		||||
        core = CoreSettings.objects.first()
 | 
			
		||||
        core.server_policy = policy
 | 
			
		||||
        core.save()
 | 
			
		||||
 | 
			
		||||
        agent.generate_checks_from_policies()
 | 
			
		||||
        agent.generate_tasks_from_policies()
 | 
			
		||||
 | 
			
		||||
        # make sure related agents on policy returns correctly
 | 
			
		||||
        self.assertEqual(agent.agentchecks.count(), 1)  # type: ignore
 | 
			
		||||
        self.assertEqual(agent.autotasks.count(), 1)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # add policy exclusions to client
 | 
			
		||||
        policy.excluded_clients.set([agent.client])  # type: ignore
 | 
			
		||||
 | 
			
		||||
        agent.generate_checks_from_policies()
 | 
			
		||||
        agent.generate_tasks_from_policies()
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(policy.related_agents().count(), 0)  # type: ignore
 | 
			
		||||
        self.assertEqual(agent.agentchecks.count(), 0)  # type: ignore
 | 
			
		||||
        delete_task.assert_called()
 | 
			
		||||
        delete_task.reset_mock()
 | 
			
		||||
 | 
			
		||||
    def test_removing_duplicate_pending_task_actions(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def test_creating_checks_with_assigned_tasks(self):
 | 
			
		||||
        pass
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@ urlpatterns = [
 | 
			
		||||
    path("policies/<int:pk>/related/", views.GetRelated.as_view()),
 | 
			
		||||
    path("policies/overview/", views.OverviewPolicy.as_view()),
 | 
			
		||||
    path("policies/<int:pk>/", views.GetUpdateDeletePolicy.as_view()),
 | 
			
		||||
    path("sync/", views.PolicySync.as_view()),
 | 
			
		||||
    path("<int:pk>/policychecks/", views.PolicyCheck.as_view()),
 | 
			
		||||
    path("<int:pk>/policyautomatedtasks/", views.PolicyAutoTask.as_view()),
 | 
			
		||||
    path("policycheckstatus/<int:check>/check/", views.PolicyCheck.as_view()),
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ from autotasks.models import AutomatedTask
 | 
			
		||||
from checks.models import Check
 | 
			
		||||
from clients.models import Client
 | 
			
		||||
from clients.serializers import ClientSerializer, SiteSerializer
 | 
			
		||||
from tacticalrmm.utils import notify_error
 | 
			
		||||
from winupdate.models import WinUpdatePolicy
 | 
			
		||||
from winupdate.serializers import WinUpdatePolicySerializer
 | 
			
		||||
 | 
			
		||||
@@ -72,6 +73,20 @@ class GetUpdateDeletePolicy(APIView):
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PolicySync(APIView):
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        if "policy" in request.data.keys():
 | 
			
		||||
            from automation.tasks import generate_agent_checks_from_policies_task
 | 
			
		||||
 | 
			
		||||
            generate_agent_checks_from_policies_task.delay(
 | 
			
		||||
                request.data["policy"], create_tasks=True
 | 
			
		||||
            )
 | 
			
		||||
            return Response("ok")
 | 
			
		||||
 | 
			
		||||
        else:
 | 
			
		||||
            return notify_error("The request was invalid")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PolicyAutoTask(APIView):
 | 
			
		||||
 | 
			
		||||
    # tasks associated with policy
 | 
			
		||||
@@ -171,7 +186,7 @@ class UpdatePatchPolicy(APIView):
 | 
			
		||||
 | 
			
		||||
        serializer = WinUpdatePolicySerializer(data=request.data, partial=True)
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        serializer.policy = policy
 | 
			
		||||
        serializer.policy = policy  # type: ignore
 | 
			
		||||
        serializer.save()
 | 
			
		||||
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
# Generated by Django 3.1.7 on 2021-02-24 05:37
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('autotasks', '0017_auto_20210210_1512'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='automatedtask',
 | 
			
		||||
            name='run_asap_after_missed',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -7,7 +7,6 @@ from django.conf import settings
 | 
			
		||||
from django.contrib.postgres.fields import ArrayField
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.db.models.fields import DateTimeField
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from loguru import logger
 | 
			
		||||
 | 
			
		||||
from alerts.models import SEVERITY_CHOICES
 | 
			
		||||
@@ -96,6 +95,7 @@ class AutomatedTask(BaseAuditModel):
 | 
			
		||||
    )
 | 
			
		||||
    run_time_date = DateTimeField(null=True, blank=True)
 | 
			
		||||
    remove_if_not_scheduled = models.BooleanField(default=False)
 | 
			
		||||
    run_asap_after_missed = models.BooleanField(default=False)  # added in agent v1.4.7
 | 
			
		||||
    managed_by_policy = models.BooleanField(default=False)
 | 
			
		||||
    parent_task = models.PositiveIntegerField(null=True, blank=True)
 | 
			
		||||
    win_task_name = models.CharField(max_length=255, null=True, blank=True)
 | 
			
		||||
@@ -218,164 +218,30 @@ class AutomatedTask(BaseAuditModel):
 | 
			
		||||
            timeout=self.timeout,
 | 
			
		||||
            enabled=self.enabled,
 | 
			
		||||
            remove_if_not_scheduled=self.remove_if_not_scheduled,
 | 
			
		||||
            run_asap_after_missed=self.run_asap_after_missed,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        create_win_task_schedule.delay(task.pk)
 | 
			
		||||
 | 
			
		||||
    def handle_alert(self) -> None:
 | 
			
		||||
        from alerts.models import Alert
 | 
			
		||||
        from autotasks.tasks import (
 | 
			
		||||
            handle_resolved_task_email_alert,
 | 
			
		||||
            handle_resolved_task_sms_alert,
 | 
			
		||||
            handle_task_email_alert,
 | 
			
		||||
            handle_task_sms_alert,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.status = "failing" if self.retcode != 0 else "passing"
 | 
			
		||||
        self.save()
 | 
			
		||||
 | 
			
		||||
        # return if agent is in maintenance mode
 | 
			
		||||
        if self.agent.maintenance_mode:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        # see if agent has an alert template and use that
 | 
			
		||||
        alert_template = self.agent.get_alert_template()
 | 
			
		||||
 | 
			
		||||
        # resolve alert if it exists
 | 
			
		||||
        if self.status == "passing":
 | 
			
		||||
            if Alert.objects.filter(assigned_task=self, resolved=False).exists():
 | 
			
		||||
                alert = Alert.objects.get(assigned_task=self, resolved=False)
 | 
			
		||||
                alert.resolve()
 | 
			
		||||
 | 
			
		||||
                # check if resolved email should be send
 | 
			
		||||
                if (
 | 
			
		||||
                    not alert.resolved_email_sent
 | 
			
		||||
                    and self.email_alert
 | 
			
		||||
                    or alert_template
 | 
			
		||||
                    and alert_template.task_email_on_resolved
 | 
			
		||||
                ):
 | 
			
		||||
                    handle_resolved_task_email_alert.delay(pk=alert.pk)
 | 
			
		||||
 | 
			
		||||
                # check if resolved text should be sent
 | 
			
		||||
                if (
 | 
			
		||||
                    not alert.resolved_sms_sent
 | 
			
		||||
                    and self.text_alert
 | 
			
		||||
                    or alert_template
 | 
			
		||||
                    and alert_template.task_text_on_resolved
 | 
			
		||||
                ):
 | 
			
		||||
                    handle_resolved_task_sms_alert.delay(pk=alert.pk)
 | 
			
		||||
 | 
			
		||||
                # check if resolved script should be run
 | 
			
		||||
                if (
 | 
			
		||||
                    alert_template
 | 
			
		||||
                    and alert_template.resolved_action
 | 
			
		||||
                    and not alert.resolved_action_run
 | 
			
		||||
                ):
 | 
			
		||||
 | 
			
		||||
                    r = self.agent.run_script(
 | 
			
		||||
                        scriptpk=alert_template.resolved_action.pk,
 | 
			
		||||
                        args=alert_template.resolved_action_args,
 | 
			
		||||
                        timeout=alert_template.resolved_action_timeout,
 | 
			
		||||
                        wait=True,
 | 
			
		||||
                        full=True,
 | 
			
		||||
                        run_on_any=True,
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
                    # command was successful
 | 
			
		||||
                    if type(r) == dict:
 | 
			
		||||
                        alert.resolved_action_retcode = r["retcode"]
 | 
			
		||||
                        alert.resolved_action_stdout = r["stdout"]
 | 
			
		||||
                        alert.resolved_action_stderr = r["stderr"]
 | 
			
		||||
                        alert.resolved_action_execution_time = "{:.4f}".format(
 | 
			
		||||
                            r["execution_time"]
 | 
			
		||||
                        )
 | 
			
		||||
                        alert.resolved_action_run = djangotime.now()
 | 
			
		||||
                        alert.save()
 | 
			
		||||
                    else:
 | 
			
		||||
                        logger.error(
 | 
			
		||||
                            f"Resolved action: {alert_template.action.name} failed to run on any agent for {self.agent.hostname} resolved alert for task: {self.name}"
 | 
			
		||||
                        )
 | 
			
		||||
 | 
			
		||||
        # create alert if task is failing
 | 
			
		||||
        else:
 | 
			
		||||
            if not Alert.objects.filter(assigned_task=self, resolved=False).exists():
 | 
			
		||||
                alert = Alert.create_task_alert(self)
 | 
			
		||||
            else:
 | 
			
		||||
                alert = Alert.objects.get(assigned_task=self, resolved=False)
 | 
			
		||||
 | 
			
		||||
                # check if alert severity changed on task and update the alert
 | 
			
		||||
                if self.alert_severity != alert.severity:
 | 
			
		||||
                    alert.severity = self.alert_severity
 | 
			
		||||
                    alert.save(update_fields=["severity"])
 | 
			
		||||
 | 
			
		||||
            # create alert in dashboard if enabled
 | 
			
		||||
            if (
 | 
			
		||||
    def should_create_alert(self, alert_template=None):
 | 
			
		||||
        return (
 | 
			
		||||
            self.dashboard_alert
 | 
			
		||||
                or alert_template
 | 
			
		||||
                and alert_template.task_always_alert
 | 
			
		||||
            ):
 | 
			
		||||
                alert.hidden = False
 | 
			
		||||
                alert.save()
 | 
			
		||||
 | 
			
		||||
            # send email if enabled
 | 
			
		||||
            if (
 | 
			
		||||
                not alert.email_sent
 | 
			
		||||
                and self.email_alert
 | 
			
		||||
                or alert_template
 | 
			
		||||
                and self.alert_severity in alert_template.task_email_alert_severity
 | 
			
		||||
                and alert_template.check_always_email
 | 
			
		||||
            ):
 | 
			
		||||
                handle_task_email_alert.delay(
 | 
			
		||||
                    pk=alert.pk,
 | 
			
		||||
                    alert_template=alert_template.check_periodic_alert_days
 | 
			
		||||
                    if alert_template
 | 
			
		||||
                    else None,
 | 
			
		||||
            or self.email_alert
 | 
			
		||||
            or self.text_alert
 | 
			
		||||
            or (
 | 
			
		||||
                alert_template
 | 
			
		||||
                and (
 | 
			
		||||
                    alert_template.task_always_alert
 | 
			
		||||
                    or alert_template.task_always_email
 | 
			
		||||
                    or alert_template.task_always_text
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            # send text if enabled
 | 
			
		||||
            if (
 | 
			
		||||
                not alert.sms_sent
 | 
			
		||||
                and self.text_alert
 | 
			
		||||
                or alert_template
 | 
			
		||||
                and self.alert_severity in alert_template.task_text_alert_severity
 | 
			
		||||
                and alert_template.check_always_text
 | 
			
		||||
            ):
 | 
			
		||||
                handle_task_sms_alert.delay(
 | 
			
		||||
                    pk=alert.pk,
 | 
			
		||||
                    alert_template=alert_template.check_periodic_alert_days
 | 
			
		||||
                    if alert_template
 | 
			
		||||
                    else None,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            # check if any scripts should be run
 | 
			
		||||
            if alert_template and alert_template.action and not alert.action_run:
 | 
			
		||||
                r = self.agent.run_script(
 | 
			
		||||
                    scriptpk=alert_template.action.pk,
 | 
			
		||||
                    args=alert_template.action_args,
 | 
			
		||||
                    timeout=alert_template.action_timeout,
 | 
			
		||||
                    wait=True,
 | 
			
		||||
                    full=True,
 | 
			
		||||
                    run_on_any=True,
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
                # command was successful
 | 
			
		||||
                if type(r) == dict:
 | 
			
		||||
                    alert.action_retcode = r["retcode"]
 | 
			
		||||
                    alert.action_stdout = r["stdout"]
 | 
			
		||||
                    alert.action_stderr = r["stderr"]
 | 
			
		||||
                    alert.action_execution_time = "{:.4f}".format(r["execution_time"])
 | 
			
		||||
                    alert.action_run = djangotime.now()
 | 
			
		||||
                    alert.save()
 | 
			
		||||
                else:
 | 
			
		||||
                    logger.error(
 | 
			
		||||
                        f"Failure action: {alert_template.action.name} failed to run on any agent for {self.agent.hostname} failure alert for task: {self.name}"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def send_email(self):
 | 
			
		||||
        from core.models import CoreSettings
 | 
			
		||||
 | 
			
		||||
        CORE = CoreSettings.objects.first()
 | 
			
		||||
        alert_template = self.agent.get_alert_template()
 | 
			
		||||
 | 
			
		||||
        if self.agent:
 | 
			
		||||
            subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Failed"
 | 
			
		||||
@@ -387,14 +253,13 @@ class AutomatedTask(BaseAuditModel):
 | 
			
		||||
            + f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        CORE.send_mail(subject, body, alert_template)
 | 
			
		||||
        CORE.send_mail(subject, body, self.agent.alert_template)
 | 
			
		||||
 | 
			
		||||
    def send_sms(self):
 | 
			
		||||
 | 
			
		||||
        from core.models import CoreSettings
 | 
			
		||||
 | 
			
		||||
        CORE = CoreSettings.objects.first()
 | 
			
		||||
        alert_template = self.agent.get_alert_template()
 | 
			
		||||
 | 
			
		||||
        if self.agent:
 | 
			
		||||
            subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Failed"
 | 
			
		||||
@@ -406,13 +271,11 @@ class AutomatedTask(BaseAuditModel):
 | 
			
		||||
            + f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        CORE.send_sms(body, alert_template=alert_template)
 | 
			
		||||
        CORE.send_sms(body, alert_template=self.agent.alert_template)
 | 
			
		||||
 | 
			
		||||
    def send_resolved_email(self):
 | 
			
		||||
        from core.models import CoreSettings
 | 
			
		||||
 | 
			
		||||
        alert_template = self.agent.get_alert_template()
 | 
			
		||||
 | 
			
		||||
        CORE = CoreSettings.objects.first()
 | 
			
		||||
        subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved"
 | 
			
		||||
        body = (
 | 
			
		||||
@@ -420,16 +283,15 @@ class AutomatedTask(BaseAuditModel):
 | 
			
		||||
            + f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        CORE.send_mail(subject, body, alert_template=alert_template)
 | 
			
		||||
        CORE.send_mail(subject, body, alert_template=self.agent.alert_template)
 | 
			
		||||
 | 
			
		||||
    def send_resolved_sms(self):
 | 
			
		||||
        from core.models import CoreSettings
 | 
			
		||||
 | 
			
		||||
        alert_template = self.agent.get_alert_template()
 | 
			
		||||
        CORE = CoreSettings.objects.first()
 | 
			
		||||
        subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved"
 | 
			
		||||
        body = (
 | 
			
		||||
            subject
 | 
			
		||||
            + f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}"
 | 
			
		||||
        )
 | 
			
		||||
        CORE.send_sms(body, alert_template=alert_template)
 | 
			
		||||
        CORE.send_sms(body, alert_template=self.agent.alert_template)
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,7 @@ class TaskSerializer(serializers.ModelSerializer):
 | 
			
		||||
    def get_alert_template(self, obj):
 | 
			
		||||
 | 
			
		||||
        if obj.agent:
 | 
			
		||||
            alert_template = obj.agent.get_alert_template()
 | 
			
		||||
            alert_template = obj.agent.alert_template
 | 
			
		||||
        else:
 | 
			
		||||
            alert_template = None
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -45,7 +45,7 @@ def create_win_task_schedule(pk, pending_action=False):
 | 
			
		||||
            task.run_time_date = now.astimezone(agent_tz).replace(
 | 
			
		||||
                tzinfo=pytz.utc
 | 
			
		||||
            ) + djangotime.timedelta(minutes=5)
 | 
			
		||||
            task.save()
 | 
			
		||||
            task.save(update_fields=["run_time_date"])
 | 
			
		||||
 | 
			
		||||
        nats_data = {
 | 
			
		||||
            "func": "schedtask",
 | 
			
		||||
@@ -62,9 +62,12 @@ def create_win_task_schedule(pk, pending_action=False):
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if task.remove_if_not_scheduled and pyver.parse(
 | 
			
		||||
        if task.run_asap_after_missed and pyver.parse(
 | 
			
		||||
            task.agent.version
 | 
			
		||||
        ) >= pyver.parse("1.1.2"):
 | 
			
		||||
        ) >= pyver.parse("1.4.7"):
 | 
			
		||||
            nats_data["schedtaskpayload"]["run_asap_after_missed"] = True
 | 
			
		||||
 | 
			
		||||
        if task.remove_if_not_scheduled:
 | 
			
		||||
            nats_data["schedtaskpayload"]["deleteafter"] = True
 | 
			
		||||
 | 
			
		||||
    elif task.task_type == "checkfailure" or task.task_type == "manual":
 | 
			
		||||
 
 | 
			
		||||
@@ -29,7 +29,6 @@ class TestAutotaskViews(TacticalTestCase):
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        policy = baker.make("automation.Policy")
 | 
			
		||||
        check = baker.make_recipe("checks.diskspace_check", agent=agent)
 | 
			
		||||
        old_agent = baker.make_recipe("agents.agent", version="1.1.0")
 | 
			
		||||
 | 
			
		||||
        # test script set to invalid pk
 | 
			
		||||
        data = {"autotask": {"script": 500}}
 | 
			
		||||
@@ -52,15 +51,6 @@ class TestAutotaskViews(TacticalTestCase):
 | 
			
		||||
        resp = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        # test old agent version
 | 
			
		||||
        data = {
 | 
			
		||||
            "autotask": {"script": script.id},
 | 
			
		||||
            "agent": old_agent.id,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resp = self.client.post(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 400)
 | 
			
		||||
 | 
			
		||||
        # test add task to agent
 | 
			
		||||
        data = {
 | 
			
		||||
            "autotask": {
 | 
			
		||||
@@ -203,13 +193,6 @@ class TestAutotaskViews(TacticalTestCase):
 | 
			
		||||
        nats_cmd.assert_called_with({"func": "runtask", "taskpk": task.id}, wait=False)
 | 
			
		||||
        nats_cmd.reset_mock()
 | 
			
		||||
 | 
			
		||||
        old_agent = baker.make_recipe("agents.agent", version="1.0.2")
 | 
			
		||||
        task2 = baker.make("autotasks.AutomatedTask", agent=old_agent)
 | 
			
		||||
        url = f"/tasks/runwintask/{task2.id}/"
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 400)
 | 
			
		||||
        nats_cmd.assert_not_called()
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -34,9 +34,6 @@ class AddAutoTask(APIView):
 | 
			
		||||
            parent = {"policy": policy}
 | 
			
		||||
        else:
 | 
			
		||||
            agent = get_object_or_404(Agent, pk=data["agent"])
 | 
			
		||||
            if not agent.has_gotasks:
 | 
			
		||||
                return notify_error("Requires agent version 1.1.1 or greater")
 | 
			
		||||
 | 
			
		||||
            parent = {"agent": agent}
 | 
			
		||||
 | 
			
		||||
        check = None
 | 
			
		||||
@@ -128,8 +125,5 @@ class AutoTask(APIView):
 | 
			
		||||
@api_view()
 | 
			
		||||
def run_task(request, pk):
 | 
			
		||||
    task = get_object_or_404(AutomatedTask, pk=pk)
 | 
			
		||||
    if not task.agent.has_nats:
 | 
			
		||||
        return notify_error("Requires agent version 1.1.0 or greater")
 | 
			
		||||
 | 
			
		||||
    asyncio.run(task.agent.nats_cmd({"func": "runtask", "taskpk": task.pk}, wait=False))
 | 
			
		||||
    return Response(f"{task.name} will now be run on {task.agent.hostname}")
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@ from model_bakery.recipe import Recipe
 | 
			
		||||
check = Recipe("checks.Check")
 | 
			
		||||
 | 
			
		||||
diskspace_check = check.extend(
 | 
			
		||||
    check_type="diskspace", disk="C:", warning_threshold=30, error_threshold=75
 | 
			
		||||
    check_type="diskspace", disk="C:", warning_threshold=30, error_threshold=10
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
cpuload_check = check.extend(
 | 
			
		||||
@@ -13,7 +13,7 @@ cpuload_check = check.extend(
 | 
			
		||||
ping_check = check.extend(check_type="ping", ip="10.10.10.10")
 | 
			
		||||
 | 
			
		||||
memory_check = check.extend(
 | 
			
		||||
    check_type="memory", warning_threshold=30, error_threshold=75
 | 
			
		||||
    check_type="memory", warning_threshold=60, error_threshold=75
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
winsvc_check = check.extend(
 | 
			
		||||
@@ -21,6 +21,7 @@ winsvc_check = check.extend(
 | 
			
		||||
    svc_name="ServiceName",
 | 
			
		||||
    svc_display_name="ServiceName",
 | 
			
		||||
    svc_policy_mode="manual",
 | 
			
		||||
    pass_if_svc_not_exist=False,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
eventlog_check = check.extend(
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
# Generated by Django 3.1.7 on 2021-03-06 02:18
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('checks', '0021_auto_20210212_1429'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='check',
 | 
			
		||||
            name='number_of_events_b4_alert',
 | 
			
		||||
            field=models.PositiveIntegerField(blank=True, default=1, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										18
									
								
								api/tacticalrmm/checks/migrations/0023_check_run_interval.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								api/tacticalrmm/checks/migrations/0023_check_run_interval.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
# Generated by Django 3.1.7 on 2021-03-06 02:59
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('checks', '0022_check_number_of_events_b4_alert'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='check',
 | 
			
		||||
            name='run_interval',
 | 
			
		||||
            field=models.PositiveIntegerField(blank=True, default=0),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -3,27 +3,19 @@ import json
 | 
			
		||||
import os
 | 
			
		||||
import string
 | 
			
		||||
from statistics import mean
 | 
			
		||||
from typing import Any, List, Union
 | 
			
		||||
from typing import Any
 | 
			
		||||
 | 
			
		||||
import pytz
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.contrib.postgres.fields import ArrayField
 | 
			
		||||
from django.core.validators import MaxValueValidator, MinValueValidator
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from loguru import logger
 | 
			
		||||
from rest_framework.fields import JSONField
 | 
			
		||||
 | 
			
		||||
from alerts.models import SEVERITY_CHOICES
 | 
			
		||||
from core.models import CoreSettings
 | 
			
		||||
from logs.models import BaseAuditModel
 | 
			
		||||
 | 
			
		||||
from .tasks import (
 | 
			
		||||
    handle_check_email_alert_task,
 | 
			
		||||
    handle_check_sms_alert_task,
 | 
			
		||||
    handle_resolved_check_email_alert_task,
 | 
			
		||||
    handle_resolved_check_sms_alert_task,
 | 
			
		||||
)
 | 
			
		||||
from .utils import bytes2human
 | 
			
		||||
 | 
			
		||||
logger.configure(**settings.LOG_CONFIG)
 | 
			
		||||
@@ -101,6 +93,7 @@ class Check(BaseAuditModel):
 | 
			
		||||
    fail_count = models.PositiveIntegerField(default=0)
 | 
			
		||||
    outage_history = models.JSONField(null=True, blank=True)  # store
 | 
			
		||||
    extra_details = models.JSONField(null=True, blank=True)
 | 
			
		||||
    run_interval = models.PositiveIntegerField(blank=True, default=0)
 | 
			
		||||
    # check specific fields
 | 
			
		||||
 | 
			
		||||
    # for eventlog, script, ip, and service alert severity
 | 
			
		||||
@@ -189,6 +182,9 @@ class Check(BaseAuditModel):
 | 
			
		||||
        max_length=255, choices=EVT_LOG_FAIL_WHEN_CHOICES, null=True, blank=True
 | 
			
		||||
    )
 | 
			
		||||
    search_last_days = models.PositiveIntegerField(null=True, blank=True)
 | 
			
		||||
    number_of_events_b4_alert = models.PositiveIntegerField(
 | 
			
		||||
        null=True, blank=True, default=1
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        if self.agent:
 | 
			
		||||
@@ -206,9 +202,9 @@ class Check(BaseAuditModel):
 | 
			
		||||
            if self.error_threshold:
 | 
			
		||||
                text += f" Error Threshold: {self.error_threshold}%"
 | 
			
		||||
 | 
			
		||||
            return f"{self.get_check_type_display()}: Drive {self.disk} < {text}"
 | 
			
		||||
            return f"{self.get_check_type_display()}: Drive {self.disk} - {text}"  # type: ignore
 | 
			
		||||
        elif self.check_type == "ping":
 | 
			
		||||
            return f"{self.get_check_type_display()}: {self.name}"
 | 
			
		||||
            return f"{self.get_check_type_display()}: {self.name}"  # type: ignore
 | 
			
		||||
        elif self.check_type == "cpuload" or self.check_type == "memory":
 | 
			
		||||
 | 
			
		||||
            text = ""
 | 
			
		||||
@@ -217,13 +213,13 @@ class Check(BaseAuditModel):
 | 
			
		||||
            if self.error_threshold:
 | 
			
		||||
                text += f" Error Threshold: {self.error_threshold}%"
 | 
			
		||||
 | 
			
		||||
            return f"{self.get_check_type_display()} > {text}"
 | 
			
		||||
            return f"{self.get_check_type_display()} - {text}"  # type: ignore
 | 
			
		||||
        elif self.check_type == "winsvc":
 | 
			
		||||
            return f"{self.get_check_type_display()}: {self.svc_display_name}"
 | 
			
		||||
            return f"{self.get_check_type_display()}: {self.svc_display_name}"  # type: ignore
 | 
			
		||||
        elif self.check_type == "eventlog":
 | 
			
		||||
            return f"{self.get_check_type_display()}: {self.name}"
 | 
			
		||||
            return f"{self.get_check_type_display()}: {self.name}"  # type: ignore
 | 
			
		||||
        elif self.check_type == "script":
 | 
			
		||||
            return f"{self.get_check_type_display()}: {self.script.name}"
 | 
			
		||||
            return f"{self.get_check_type_display()}: {self.script.name}"  # type: ignore
 | 
			
		||||
        else:
 | 
			
		||||
            return "n/a"
 | 
			
		||||
 | 
			
		||||
@@ -242,7 +238,7 @@ class Check(BaseAuditModel):
 | 
			
		||||
        return self.last_run
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def non_editable_fields(self) -> List[str]:
 | 
			
		||||
    def non_editable_fields(self) -> list[str]:
 | 
			
		||||
        return [
 | 
			
		||||
            "check_type",
 | 
			
		||||
            "status",
 | 
			
		||||
@@ -267,147 +263,27 @@ class Check(BaseAuditModel):
 | 
			
		||||
            "modified_time",
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
    def handle_alert(self) -> None:
 | 
			
		||||
        from alerts.models import Alert, AlertTemplate
 | 
			
		||||
    def should_create_alert(self, alert_template=None):
 | 
			
		||||
 | 
			
		||||
        # return if agent is in maintenance mode
 | 
			
		||||
        if self.agent.maintenance_mode:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        # see if agent has an alert template and use that
 | 
			
		||||
        alert_template: Union[AlertTemplate, None] = self.agent.get_alert_template()
 | 
			
		||||
 | 
			
		||||
        # resolve alert if it exists
 | 
			
		||||
        if self.status == "passing":
 | 
			
		||||
            if Alert.objects.filter(assigned_check=self, resolved=False).exists():
 | 
			
		||||
                alert = Alert.objects.get(assigned_check=self, resolved=False)
 | 
			
		||||
                alert.resolve()
 | 
			
		||||
 | 
			
		||||
                # check if a resolved email notification should be send
 | 
			
		||||
                if (
 | 
			
		||||
                    alert_template
 | 
			
		||||
                    and alert_template.check_email_on_resolved
 | 
			
		||||
                    and not alert.resolved_email_sent
 | 
			
		||||
                ):
 | 
			
		||||
                    handle_resolved_check_email_alert_task.delay(pk=alert.pk)
 | 
			
		||||
 | 
			
		||||
                # check if resolved text should be sent
 | 
			
		||||
                if (
 | 
			
		||||
                    alert_template
 | 
			
		||||
                    and alert_template.check_text_on_resolved
 | 
			
		||||
                    and not alert.resolved_sms_sent
 | 
			
		||||
                ):
 | 
			
		||||
                    handle_resolved_check_sms_alert_task.delay(pk=alert.pk)
 | 
			
		||||
 | 
			
		||||
                # check if resolved script should be run
 | 
			
		||||
                if (
 | 
			
		||||
                    alert_template
 | 
			
		||||
                    and alert_template.resolved_action
 | 
			
		||||
                    and not alert.resolved_action_run
 | 
			
		||||
                ):
 | 
			
		||||
                    r = self.agent.run_script(
 | 
			
		||||
                        scriptpk=alert_template.resolved_action.pk,
 | 
			
		||||
                        args=alert_template.resolved_action_args,
 | 
			
		||||
                        timeout=alert_template.resolved_action_timeout,
 | 
			
		||||
                        wait=True,
 | 
			
		||||
                        full=True,
 | 
			
		||||
                        run_on_any=True,
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
                    # command was successful
 | 
			
		||||
                    if type(r) == dict:
 | 
			
		||||
                        alert.resolved_action_retcode = r["retcode"]
 | 
			
		||||
                        alert.resolved_action_stdout = r["stdout"]
 | 
			
		||||
                        alert.resolved_action_stderr = r["stderr"]
 | 
			
		||||
                        alert.resolved_action_execution_time = "{:.4f}".format(
 | 
			
		||||
                            r["execution_time"]
 | 
			
		||||
                        )
 | 
			
		||||
                        alert.resolved_action_run = djangotime.now()
 | 
			
		||||
                        alert.save()
 | 
			
		||||
                    else:
 | 
			
		||||
                        logger.error(
 | 
			
		||||
                            f"Resolved action: {alert_template.action.name} failed to run on any agent for {self.agent.hostname} resolved alert for {self.check_type} check"
 | 
			
		||||
                        )
 | 
			
		||||
 | 
			
		||||
        elif self.fail_count >= self.fails_b4_alert:
 | 
			
		||||
            if not Alert.objects.filter(assigned_check=self, resolved=False).exists():
 | 
			
		||||
                alert = Alert.create_check_alert(self)
 | 
			
		||||
            else:
 | 
			
		||||
                alert = Alert.objects.get(assigned_check=self, resolved=False)
 | 
			
		||||
 | 
			
		||||
                # check if alert severity changed on check and update the alert
 | 
			
		||||
                if self.alert_severity != alert.severity:
 | 
			
		||||
                    alert.severity = self.alert_severity
 | 
			
		||||
                    alert.save(update_fields=["severity"])
 | 
			
		||||
 | 
			
		||||
            # create alert in dashboard if enabled
 | 
			
		||||
            if (
 | 
			
		||||
        return (
 | 
			
		||||
            self.dashboard_alert
 | 
			
		||||
                or alert_template
 | 
			
		||||
                and self.alert_severity in alert_template.check_dashboard_alert_severity
 | 
			
		||||
                and alert_template.check_always_alert
 | 
			
		||||
            ):
 | 
			
		||||
                alert.hidden = False
 | 
			
		||||
                alert.save()
 | 
			
		||||
 | 
			
		||||
            # send email if enabled
 | 
			
		||||
            if (
 | 
			
		||||
                not alert.email_sent
 | 
			
		||||
                and self.email_alert
 | 
			
		||||
                or alert_template
 | 
			
		||||
                and self.alert_severity in alert_template.check_email_alert_severity
 | 
			
		||||
                and alert_template.check_always_email
 | 
			
		||||
            ):
 | 
			
		||||
                handle_check_email_alert_task.delay(
 | 
			
		||||
                    pk=alert.pk,
 | 
			
		||||
                    alert_interval=alert_template.check_periodic_alert_days
 | 
			
		||||
                    if alert_template
 | 
			
		||||
                    else None,
 | 
			
		||||
            or self.email_alert
 | 
			
		||||
            or self.text_alert
 | 
			
		||||
            or (
 | 
			
		||||
                alert_template
 | 
			
		||||
                and (
 | 
			
		||||
                    alert_template.check_always_alert
 | 
			
		||||
                    or alert_template.check_always_email
 | 
			
		||||
                    or alert_template.check_always_text
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            # send text if enabled
 | 
			
		||||
            if (
 | 
			
		||||
                not alert.sms_sent
 | 
			
		||||
                and self.text_alert
 | 
			
		||||
                or alert_template
 | 
			
		||||
                and self.alert_severity in alert_template.check_text_alert_severity
 | 
			
		||||
                and alert_template.check_always_text
 | 
			
		||||
            ):
 | 
			
		||||
                handle_check_sms_alert_task.delay(
 | 
			
		||||
                    pk=alert.pk,
 | 
			
		||||
                    alert_interval=alert_template.check_periodic_alert_days
 | 
			
		||||
                    if alert_template
 | 
			
		||||
                    else None,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            # check if any scripts should be run
 | 
			
		||||
            if alert_template and alert_template.action and not alert.action_run:
 | 
			
		||||
                r = self.agent.run_script(
 | 
			
		||||
                    scriptpk=alert_template.action.pk,
 | 
			
		||||
                    args=alert_template.action_args,
 | 
			
		||||
                    timeout=alert_template.action_timeout,
 | 
			
		||||
                    wait=True,
 | 
			
		||||
                    full=True,
 | 
			
		||||
                    run_on_any=True,
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
                # command was successful
 | 
			
		||||
                if type(r) == dict:
 | 
			
		||||
                    alert.action_retcode = r["retcode"]
 | 
			
		||||
                    alert.action_stdout = r["stdout"]
 | 
			
		||||
                    alert.action_stderr = r["stderr"]
 | 
			
		||||
                    alert.action_execution_time = "{:.4f}".format(r["execution_time"])
 | 
			
		||||
                    alert.action_run = djangotime.now()
 | 
			
		||||
                    alert.save()
 | 
			
		||||
                else:
 | 
			
		||||
                    logger.error(
 | 
			
		||||
                        f"Failure action: {alert_template.action.name} failed to run on any agent for {self.agent.hostname} failure alert for {self.check_type} check{r}"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def add_check_history(self, value: int, more_info: Any = None) -> None:
 | 
			
		||||
        CheckHistory.objects.create(check_history=self, y=value, results=more_info)
 | 
			
		||||
 | 
			
		||||
    def handle_checkv2(self, data):
 | 
			
		||||
        from alerts.models import Alert
 | 
			
		||||
 | 
			
		||||
        # cpuload or mem checks
 | 
			
		||||
        if self.check_type == "cpuload" or self.check_type == "memory":
 | 
			
		||||
@@ -616,13 +492,13 @@ class Check(BaseAuditModel):
 | 
			
		||||
                            log.append(i)
 | 
			
		||||
 | 
			
		||||
            if self.fail_when == "contains":
 | 
			
		||||
                if log:
 | 
			
		||||
                if log and len(log) >= self.number_of_events_b4_alert:
 | 
			
		||||
                    self.status = "failing"
 | 
			
		||||
                else:
 | 
			
		||||
                    self.status = "passing"
 | 
			
		||||
 | 
			
		||||
            elif self.fail_when == "not_contains":
 | 
			
		||||
                if log:
 | 
			
		||||
                if log and len(log) >= self.number_of_events_b4_alert:
 | 
			
		||||
                    self.status = "passing"
 | 
			
		||||
                else:
 | 
			
		||||
                    self.status = "failing"
 | 
			
		||||
@@ -640,11 +516,14 @@ class Check(BaseAuditModel):
 | 
			
		||||
            self.fail_count += 1
 | 
			
		||||
            self.save(update_fields=["status", "fail_count", "alert_severity"])
 | 
			
		||||
 | 
			
		||||
            if self.fail_count >= self.fails_b4_alert:
 | 
			
		||||
                Alert.handle_alert_failure(self)
 | 
			
		||||
 | 
			
		||||
        elif self.status == "passing":
 | 
			
		||||
            self.fail_count = 0
 | 
			
		||||
            self.save(update_fields=["status", "fail_count", "alert_severity"])
 | 
			
		||||
 | 
			
		||||
        self.handle_alert()
 | 
			
		||||
            if Alert.objects.filter(assigned_check=self, resolved=False).exists():
 | 
			
		||||
                Alert.handle_alert_resolve(self)
 | 
			
		||||
 | 
			
		||||
        return self.status
 | 
			
		||||
 | 
			
		||||
@@ -688,6 +567,7 @@ class Check(BaseAuditModel):
 | 
			
		||||
            text_alert=self.text_alert,
 | 
			
		||||
            fails_b4_alert=self.fails_b4_alert,
 | 
			
		||||
            extra_details=self.extra_details,
 | 
			
		||||
            run_interval=self.run_interval,
 | 
			
		||||
            error_threshold=self.error_threshold,
 | 
			
		||||
            warning_threshold=self.warning_threshold,
 | 
			
		||||
            disk=self.disk,
 | 
			
		||||
@@ -711,6 +591,7 @@ class Check(BaseAuditModel):
 | 
			
		||||
            event_message=self.event_message,
 | 
			
		||||
            fail_when=self.fail_when,
 | 
			
		||||
            search_last_days=self.search_last_days,
 | 
			
		||||
            number_of_events_b4_alert=self.number_of_events_b4_alert,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def is_duplicate(self, check):
 | 
			
		||||
@@ -738,11 +619,10 @@ class Check(BaseAuditModel):
 | 
			
		||||
    def send_email(self):
 | 
			
		||||
 | 
			
		||||
        CORE = CoreSettings.objects.first()
 | 
			
		||||
        alert_template = self.agent.get_alert_template()
 | 
			
		||||
 | 
			
		||||
        body: str = ""
 | 
			
		||||
        if self.agent:
 | 
			
		||||
            subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Failed"
 | 
			
		||||
            subject = f"{self.agent.client.name}, {self.agent.site.name}, {self.agent.hostname} - {self} Failed"
 | 
			
		||||
        else:
 | 
			
		||||
            subject = f"{self} Failed"
 | 
			
		||||
 | 
			
		||||
@@ -753,12 +633,15 @@ class Check(BaseAuditModel):
 | 
			
		||||
            if self.error_threshold:
 | 
			
		||||
                text += f" Error Threshold: {self.error_threshold}%"
 | 
			
		||||
 | 
			
		||||
            try:
 | 
			
		||||
                percent_used = [
 | 
			
		||||
                    d["percent"] for d in self.agent.disks if d["device"] == self.disk
 | 
			
		||||
                ][0]
 | 
			
		||||
                percent_free = 100 - percent_used
 | 
			
		||||
 | 
			
		||||
                body = subject + f" - Free: {percent_free}%, {text}"
 | 
			
		||||
            except:
 | 
			
		||||
                body = subject + f" - Disk {self.disk} does not exist"
 | 
			
		||||
 | 
			
		||||
        elif self.check_type == "script":
 | 
			
		||||
 | 
			
		||||
@@ -787,16 +670,7 @@ class Check(BaseAuditModel):
 | 
			
		||||
                body = subject + f" - Average memory usage: {avg}%, {text}"
 | 
			
		||||
 | 
			
		||||
        elif self.check_type == "winsvc":
 | 
			
		||||
 | 
			
		||||
            try:
 | 
			
		||||
                status = list(
 | 
			
		||||
                    filter(lambda x: x["name"] == self.svc_name, self.agent.services)
 | 
			
		||||
                )[0]["status"]
 | 
			
		||||
            # catch services that don't exist if policy check
 | 
			
		||||
            except:
 | 
			
		||||
                status = "Unknown"
 | 
			
		||||
 | 
			
		||||
            body = subject + f" - Status: {status.upper()}"
 | 
			
		||||
            body = subject + f" - Status: {self.more_info}"
 | 
			
		||||
 | 
			
		||||
        elif self.check_type == "eventlog":
 | 
			
		||||
 | 
			
		||||
@@ -820,12 +694,11 @@ class Check(BaseAuditModel):
 | 
			
		||||
                except:
 | 
			
		||||
                    continue
 | 
			
		||||
 | 
			
		||||
        CORE.send_mail(subject, body, alert_template=alert_template)
 | 
			
		||||
        CORE.send_mail(subject, body, alert_template=self.agent.alert_template)
 | 
			
		||||
 | 
			
		||||
    def send_sms(self):
 | 
			
		||||
 | 
			
		||||
        CORE = CoreSettings.objects.first()
 | 
			
		||||
        alert_template = self.agent.get_alert_template()
 | 
			
		||||
        body: str = ""
 | 
			
		||||
 | 
			
		||||
        if self.agent:
 | 
			
		||||
@@ -840,11 +713,15 @@ class Check(BaseAuditModel):
 | 
			
		||||
            if self.error_threshold:
 | 
			
		||||
                text += f" Error Threshold: {self.error_threshold}%"
 | 
			
		||||
 | 
			
		||||
            try:
 | 
			
		||||
                percent_used = [
 | 
			
		||||
                    d["percent"] for d in self.agent.disks if d["device"] == self.disk
 | 
			
		||||
                ][0]
 | 
			
		||||
                percent_free = 100 - percent_used
 | 
			
		||||
                body = subject + f" - Free: {percent_free}%, {text}"
 | 
			
		||||
            except:
 | 
			
		||||
                body = subject + f" - Disk {self.disk} does not exist"
 | 
			
		||||
 | 
			
		||||
        elif self.check_type == "script":
 | 
			
		||||
            body = subject + f" - Return code: {self.retcode}"
 | 
			
		||||
        elif self.check_type == "ping":
 | 
			
		||||
@@ -862,28 +739,25 @@ class Check(BaseAuditModel):
 | 
			
		||||
            elif self.check_type == "memory":
 | 
			
		||||
                body = subject + f" - Average memory usage: {avg}%, {text}"
 | 
			
		||||
        elif self.check_type == "winsvc":
 | 
			
		||||
            status = list(
 | 
			
		||||
                filter(lambda x: x["name"] == self.svc_name, self.agent.services)
 | 
			
		||||
            )[0]["status"]
 | 
			
		||||
            body = subject + f" - Status: {status.upper()}"
 | 
			
		||||
            body = subject + f" - Status: {self.more_info}"
 | 
			
		||||
        elif self.check_type == "eventlog":
 | 
			
		||||
            body = subject
 | 
			
		||||
 | 
			
		||||
        CORE.send_sms(body, alert_template=alert_template)
 | 
			
		||||
        CORE.send_sms(body, alert_template=self.agent.alert_template)
 | 
			
		||||
 | 
			
		||||
    def send_resolved_email(self):
 | 
			
		||||
        CORE = CoreSettings.objects.first()
 | 
			
		||||
        alert_template = self.agent.get_alert_template()
 | 
			
		||||
 | 
			
		||||
        subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved"
 | 
			
		||||
        body = f"{self} is now back to normal"
 | 
			
		||||
 | 
			
		||||
        CORE.send_mail(subject, body, alert_template=alert_template)
 | 
			
		||||
        CORE.send_mail(subject, body, alert_template=self.agent.alert_template)
 | 
			
		||||
 | 
			
		||||
    def send_resolved_sms(self):
 | 
			
		||||
        CORE = CoreSettings.objects.first()
 | 
			
		||||
        alert_template = self.agent.get_alert_template()
 | 
			
		||||
 | 
			
		||||
        subject = f"{self.agent.client.name}, {self.agent.site.name}, {self} Resolved"
 | 
			
		||||
        CORE.send_sms(subject, alert_template=alert_template)
 | 
			
		||||
        CORE.send_sms(subject, alert_template=self.agent.alert_template)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CheckHistory(models.Model):
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,7 @@ class CheckSerializer(serializers.ModelSerializer):
 | 
			
		||||
 | 
			
		||||
    def get_alert_template(self, obj):
 | 
			
		||||
        if obj.agent:
 | 
			
		||||
            alert_template = obj.agent.get_alert_template()
 | 
			
		||||
            alert_template = obj.agent.alert_template
 | 
			
		||||
        else:
 | 
			
		||||
            alert_template = None
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@ class TestCheckViews(TacticalTestCase):
 | 
			
		||||
        serializer = CheckSerializer(disk_check)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)  # type: ignore
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    def test_add_disk_check(self):
 | 
			
		||||
@@ -211,7 +211,7 @@ class TestCheckViews(TacticalTestCase):
 | 
			
		||||
        serializer = CheckSerializer(disk_check)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)  # type: ignore
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
    def test_add_policy_disk_check(self):
 | 
			
		||||
@@ -221,7 +221,7 @@ class TestCheckViews(TacticalTestCase):
 | 
			
		||||
        url = "/checks/checks/"
 | 
			
		||||
 | 
			
		||||
        valid_payload = {
 | 
			
		||||
            "policy": policy.pk,
 | 
			
		||||
            "policy": policy.pk,  # type: ignore
 | 
			
		||||
            "check": {
 | 
			
		||||
                "check_type": "diskspace",
 | 
			
		||||
                "disk": "M:",
 | 
			
		||||
@@ -233,7 +233,7 @@ class TestCheckViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        # should fail because both error and warning thresholds are 0
 | 
			
		||||
        invalid_payload = {
 | 
			
		||||
            "policy": policy.pk,
 | 
			
		||||
            "policy": policy.pk,  # type: ignore
 | 
			
		||||
            "check": {
 | 
			
		||||
                "check_type": "diskspace",
 | 
			
		||||
                "error_threshold": 0,
 | 
			
		||||
@@ -247,7 +247,7 @@ class TestCheckViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        # should fail because warning is less than error
 | 
			
		||||
        invalid_payload = {
 | 
			
		||||
            "policy": policy.pk,
 | 
			
		||||
            "policy": policy.pk,  # type: ignore
 | 
			
		||||
            "check": {
 | 
			
		||||
                "check_type": "diskspace",
 | 
			
		||||
                "error_threshold": 80,
 | 
			
		||||
@@ -261,7 +261,7 @@ class TestCheckViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        # this should fail because we already have a check for drive M: in setup
 | 
			
		||||
        invalid_payload = {
 | 
			
		||||
            "policy": policy.pk,
 | 
			
		||||
            "policy": policy.pk,  # type: ignore
 | 
			
		||||
            "check": {
 | 
			
		||||
                "check_type": "diskspace",
 | 
			
		||||
                "disk": "M:",
 | 
			
		||||
@@ -277,8 +277,8 @@ class TestCheckViews(TacticalTestCase):
 | 
			
		||||
    def test_get_disks_for_policies(self):
 | 
			
		||||
        url = "/checks/getalldisks/"
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertIsInstance(r.data, list)
 | 
			
		||||
        self.assertEqual(26, len(r.data))
 | 
			
		||||
        self.assertIsInstance(r.data, list)  # type: ignore
 | 
			
		||||
        self.assertEqual(26, len(r.data))  # type: ignore
 | 
			
		||||
 | 
			
		||||
    def test_edit_check_alert(self):
 | 
			
		||||
        # setup data
 | 
			
		||||
@@ -310,14 +310,8 @@ class TestCheckViews(TacticalTestCase):
 | 
			
		||||
    @patch("agents.models.Agent.nats_cmd")
 | 
			
		||||
    def test_run_checks(self, nats_cmd):
 | 
			
		||||
        agent = baker.make_recipe("agents.agent", version="1.4.1")
 | 
			
		||||
        agent_old = baker.make_recipe("agents.agent", version="1.0.2")
 | 
			
		||||
        agent_b4_141 = baker.make_recipe("agents.agent", version="1.4.0")
 | 
			
		||||
 | 
			
		||||
        url = f"/checks/runchecks/{agent_old.pk}/"
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
        self.assertEqual(r.json(), "Requires agent version 1.1.0 or greater")
 | 
			
		||||
 | 
			
		||||
        url = f"/checks/runchecks/{agent_b4_141.pk}/"
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
@@ -361,8 +355,8 @@ class TestCheckViews(TacticalTestCase):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # need to manually set the date back 35 days
 | 
			
		||||
        for check_history in check_history_data:
 | 
			
		||||
            check_history.x = djangotime.now() - djangotime.timedelta(days=35)
 | 
			
		||||
        for check_history in check_history_data:  # type: ignore
 | 
			
		||||
            check_history.x = djangotime.now() - djangotime.timedelta(days=35)  # type: ignore
 | 
			
		||||
            check_history.save()
 | 
			
		||||
 | 
			
		||||
        # test invalid check pk
 | 
			
		||||
@@ -375,20 +369,22 @@ class TestCheckViews(TacticalTestCase):
 | 
			
		||||
        data = {"timeFilter": 30}
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(len(resp.data), 30)
 | 
			
		||||
        self.assertEqual(len(resp.data), 30)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # test with timeFilter equal to 0
 | 
			
		||||
        data = {"timeFilter": 0}
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(len(resp.data), 60)
 | 
			
		||||
        self.assertEqual(len(resp.data), 60)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("patch", url)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
        self.setup_coresettings()
 | 
			
		||||
        self.agent = baker.make_recipe("agents.agent")
 | 
			
		||||
 | 
			
		||||
    def test_prune_check_history(self):
 | 
			
		||||
        from .tasks import prune_check_history
 | 
			
		||||
@@ -403,8 +399,8 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # need to manually set the date back 35 days
 | 
			
		||||
        for check_history in check_history_data:
 | 
			
		||||
            check_history.x = djangotime.now() - djangotime.timedelta(days=35)
 | 
			
		||||
        for check_history in check_history_data:  # type: ignore
 | 
			
		||||
            check_history.x = djangotime.now() - djangotime.timedelta(days=35)  # type: ignore
 | 
			
		||||
            check_history.save()
 | 
			
		||||
 | 
			
		||||
        # prune data 30 days old
 | 
			
		||||
@@ -414,3 +410,758 @@ class TestCheckTasks(TacticalTestCase):
 | 
			
		||||
        # prune all Check history Data
 | 
			
		||||
        prune_check_history(0)
 | 
			
		||||
        self.assertEqual(CheckHistory.objects.count(), 0)
 | 
			
		||||
 | 
			
		||||
    def test_handle_script_check(self):
 | 
			
		||||
        from checks.models import Check
 | 
			
		||||
 | 
			
		||||
        url = "/api/v3/checkrunner/"
 | 
			
		||||
 | 
			
		||||
        script = baker.make_recipe("checks.script_check", agent=self.agent)
 | 
			
		||||
 | 
			
		||||
        # test failing
 | 
			
		||||
        data = {
 | 
			
		||||
            "id": script.id,
 | 
			
		||||
            "retcode": 500,
 | 
			
		||||
            "stderr": "error",
 | 
			
		||||
            "stdout": "message",
 | 
			
		||||
            "runtime": 5.000,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=script.id)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(new_check.status, "failing")
 | 
			
		||||
        self.assertEqual(new_check.alert_severity, "error")
 | 
			
		||||
 | 
			
		||||
        # test passing
 | 
			
		||||
        data = {
 | 
			
		||||
            "id": script.id,
 | 
			
		||||
            "retcode": 0,
 | 
			
		||||
            "stderr": "error",
 | 
			
		||||
            "stdout": "message",
 | 
			
		||||
            "runtime": 5.000,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=script.id)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(new_check.status, "passing")
 | 
			
		||||
 | 
			
		||||
        # test failing info
 | 
			
		||||
        script.info_return_codes = [20, 30, 50]
 | 
			
		||||
        script.save()
 | 
			
		||||
 | 
			
		||||
        data = {
 | 
			
		||||
            "id": script.id,
 | 
			
		||||
            "retcode": 30,
 | 
			
		||||
            "stderr": "error",
 | 
			
		||||
            "stdout": "message",
 | 
			
		||||
            "runtime": 5.000,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=script.id)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(new_check.status, "failing")
 | 
			
		||||
        self.assertEqual(new_check.alert_severity, "info")
 | 
			
		||||
 | 
			
		||||
        # test failing warning
 | 
			
		||||
        script.warning_return_codes = [80, 100, 1040]
 | 
			
		||||
        script.save()
 | 
			
		||||
 | 
			
		||||
        data = {
 | 
			
		||||
            "id": script.id,
 | 
			
		||||
            "retcode": 1040,
 | 
			
		||||
            "stderr": "error",
 | 
			
		||||
            "stdout": "message",
 | 
			
		||||
            "runtime": 5.000,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=script.id)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(new_check.status, "failing")
 | 
			
		||||
        self.assertEqual(new_check.alert_severity, "warning")
 | 
			
		||||
 | 
			
		||||
    def test_handle_diskspace_check(self):
 | 
			
		||||
        from checks.models import Check
 | 
			
		||||
 | 
			
		||||
        url = "/api/v3/checkrunner/"
 | 
			
		||||
 | 
			
		||||
        diskspace = baker.make_recipe(
 | 
			
		||||
            "checks.diskspace_check",
 | 
			
		||||
            warning_threshold=20,
 | 
			
		||||
            error_threshold=10,
 | 
			
		||||
            agent=self.agent,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # test warning threshold failure
 | 
			
		||||
        data = {
 | 
			
		||||
            "id": diskspace.id,
 | 
			
		||||
            "exists": True,
 | 
			
		||||
            "percent_used": 85,
 | 
			
		||||
            "total": 500,
 | 
			
		||||
            "free": 400,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=diskspace.id)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(new_check.status, "failing")
 | 
			
		||||
        self.assertEqual(new_check.alert_severity, "warning")
 | 
			
		||||
 | 
			
		||||
        # test error failure
 | 
			
		||||
        data = {
 | 
			
		||||
            "id": diskspace.id,
 | 
			
		||||
            "exists": True,
 | 
			
		||||
            "percent_used": 95,
 | 
			
		||||
            "total": 500,
 | 
			
		||||
            "free": 400,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=diskspace.id)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(new_check.status, "failing")
 | 
			
		||||
        self.assertEqual(new_check.alert_severity, "error")
 | 
			
		||||
 | 
			
		||||
        # test disk not exist
 | 
			
		||||
        data = {"id": diskspace.id, "exists": False}
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=diskspace.id)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(new_check.status, "failing")
 | 
			
		||||
        self.assertEqual(new_check.alert_severity, "error")
 | 
			
		||||
 | 
			
		||||
        # test warning threshold 0
 | 
			
		||||
        diskspace.warning_threshold = 0
 | 
			
		||||
        diskspace.save()
 | 
			
		||||
        data = {
 | 
			
		||||
            "id": diskspace.id,
 | 
			
		||||
            "exists": True,
 | 
			
		||||
            "percent_used": 95,
 | 
			
		||||
            "total": 500,
 | 
			
		||||
            "free": 400,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=diskspace.id)
 | 
			
		||||
        self.assertEqual(new_check.status, "failing")
 | 
			
		||||
        self.assertEqual(new_check.alert_severity, "error")
 | 
			
		||||
 | 
			
		||||
        # test error threshold 0
 | 
			
		||||
        diskspace.warning_threshold = 50
 | 
			
		||||
        diskspace.error_threshold = 0
 | 
			
		||||
        diskspace.save()
 | 
			
		||||
        data = {
 | 
			
		||||
            "id": diskspace.id,
 | 
			
		||||
            "exists": True,
 | 
			
		||||
            "percent_used": 95,
 | 
			
		||||
            "total": 500,
 | 
			
		||||
            "free": 400,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=diskspace.id)
 | 
			
		||||
        self.assertEqual(new_check.status, "failing")
 | 
			
		||||
        self.assertEqual(new_check.alert_severity, "warning")
 | 
			
		||||
 | 
			
		||||
        # test passing
 | 
			
		||||
        data = {
 | 
			
		||||
            "id": diskspace.id,
 | 
			
		||||
            "exists": True,
 | 
			
		||||
            "percent_used": 50,
 | 
			
		||||
            "total": 500,
 | 
			
		||||
            "free": 400,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=diskspace.id)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(new_check.status, "passing")
 | 
			
		||||
 | 
			
		||||
    def test_handle_cpuload_check(self):
 | 
			
		||||
        from checks.models import Check
 | 
			
		||||
 | 
			
		||||
        url = "/api/v3/checkrunner/"
 | 
			
		||||
 | 
			
		||||
        cpuload = baker.make_recipe(
 | 
			
		||||
            "checks.cpuload_check",
 | 
			
		||||
            warning_threshold=70,
 | 
			
		||||
            error_threshold=90,
 | 
			
		||||
            agent=self.agent,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # test failing warning
 | 
			
		||||
        data = {"id": cpuload.id, "percent": 80}
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=cpuload.id)
 | 
			
		||||
        self.assertEqual(new_check.status, "failing")
 | 
			
		||||
        self.assertEqual(new_check.alert_severity, "warning")
 | 
			
		||||
 | 
			
		||||
        # test failing error
 | 
			
		||||
        data = {"id": cpuload.id, "percent": 95}
 | 
			
		||||
 | 
			
		||||
        # reset check history
 | 
			
		||||
        cpuload.history = []
 | 
			
		||||
        cpuload.save()
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=cpuload.id)
 | 
			
		||||
        self.assertEqual(new_check.status, "failing")
 | 
			
		||||
        self.assertEqual(new_check.alert_severity, "error")
 | 
			
		||||
 | 
			
		||||
        # test passing
 | 
			
		||||
        data = {"id": cpuload.id, "percent": 50}
 | 
			
		||||
 | 
			
		||||
        # reset check history
 | 
			
		||||
        cpuload.history = []
 | 
			
		||||
        cpuload.save()
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=cpuload.id)
 | 
			
		||||
        self.assertEqual(new_check.status, "passing")
 | 
			
		||||
 | 
			
		||||
        # test warning threshold 0
 | 
			
		||||
        cpuload.warning_threshold = 0
 | 
			
		||||
        cpuload.save()
 | 
			
		||||
        data = {"id": cpuload.id, "percent": 95}
 | 
			
		||||
 | 
			
		||||
        # reset check history
 | 
			
		||||
        cpuload.history = []
 | 
			
		||||
        cpuload.save()
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=cpuload.id)
 | 
			
		||||
        self.assertEqual(new_check.status, "failing")
 | 
			
		||||
        self.assertEqual(new_check.alert_severity, "error")
 | 
			
		||||
 | 
			
		||||
        # test error threshold 0
 | 
			
		||||
        cpuload.warning_threshold = 50
 | 
			
		||||
        cpuload.error_threshold = 0
 | 
			
		||||
        cpuload.save()
 | 
			
		||||
        data = {"id": cpuload.id, "percent": 95}
 | 
			
		||||
 | 
			
		||||
        # reset check history
 | 
			
		||||
        cpuload.history = []
 | 
			
		||||
        cpuload.save()
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=cpuload.id)
 | 
			
		||||
        self.assertEqual(new_check.status, "failing")
 | 
			
		||||
        self.assertEqual(new_check.alert_severity, "warning")
 | 
			
		||||
 | 
			
		||||
    def test_handle_memory_check(self):
 | 
			
		||||
        from checks.models import Check
 | 
			
		||||
 | 
			
		||||
        url = "/api/v3/checkrunner/"
 | 
			
		||||
 | 
			
		||||
        memory = baker.make_recipe(
 | 
			
		||||
            "checks.memory_check",
 | 
			
		||||
            warning_threshold=70,
 | 
			
		||||
            error_threshold=90,
 | 
			
		||||
            agent=self.agent,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # test failing warning
 | 
			
		||||
        data = {"id": memory.id, "percent": 80}
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=memory.id)
 | 
			
		||||
        self.assertEqual(new_check.status, "failing")
 | 
			
		||||
        self.assertEqual(new_check.alert_severity, "warning")
 | 
			
		||||
 | 
			
		||||
        # test failing error
 | 
			
		||||
        data = {"id": memory.id, "percent": 95}
 | 
			
		||||
 | 
			
		||||
        # reset check history
 | 
			
		||||
        memory.history = []
 | 
			
		||||
        memory.save()
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=memory.id)
 | 
			
		||||
        self.assertEqual(new_check.status, "failing")
 | 
			
		||||
        self.assertEqual(new_check.alert_severity, "error")
 | 
			
		||||
 | 
			
		||||
        # test passing
 | 
			
		||||
        data = {"id": memory.id, "percent": 50}
 | 
			
		||||
 | 
			
		||||
        # reset check history
 | 
			
		||||
        memory.history = []
 | 
			
		||||
        memory.save()
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=memory.id)
 | 
			
		||||
        self.assertEqual(new_check.status, "passing")
 | 
			
		||||
 | 
			
		||||
        # test warning threshold 0
 | 
			
		||||
        memory.warning_threshold = 0
 | 
			
		||||
        memory.save()
 | 
			
		||||
        data = {"id": memory.id, "percent": 95}
 | 
			
		||||
 | 
			
		||||
        # reset check history
 | 
			
		||||
        memory.history = []
 | 
			
		||||
        memory.save()
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=memory.id)
 | 
			
		||||
        self.assertEqual(new_check.status, "failing")
 | 
			
		||||
        self.assertEqual(new_check.alert_severity, "error")
 | 
			
		||||
 | 
			
		||||
        # test error threshold 0
 | 
			
		||||
        memory.warning_threshold = 50
 | 
			
		||||
        memory.error_threshold = 0
 | 
			
		||||
        memory.save()
 | 
			
		||||
        data = {"id": memory.id, "percent": 95}
 | 
			
		||||
 | 
			
		||||
        # reset check history
 | 
			
		||||
        memory.history = []
 | 
			
		||||
        memory.save()
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=memory.id)
 | 
			
		||||
        self.assertEqual(new_check.status, "failing")
 | 
			
		||||
        self.assertEqual(new_check.alert_severity, "warning")
 | 
			
		||||
 | 
			
		||||
    def test_handle_ping_check(self):
 | 
			
		||||
        from checks.models import Check
 | 
			
		||||
 | 
			
		||||
        url = "/api/v3/checkrunner/"
 | 
			
		||||
 | 
			
		||||
        ping = baker.make_recipe(
 | 
			
		||||
            "checks.ping_check", agent=self.agent, alert_severity="info"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # test failing info
 | 
			
		||||
        data = {
 | 
			
		||||
            "id": ping.id,
 | 
			
		||||
            "output": "Reply from 192.168.1.27: Destination host unreachable",
 | 
			
		||||
            "has_stdout": True,
 | 
			
		||||
            "has_stderr": False,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=ping.id)
 | 
			
		||||
        self.assertEqual(new_check.status, "failing")
 | 
			
		||||
        self.assertEqual(new_check.alert_severity, "info")
 | 
			
		||||
 | 
			
		||||
        # test failing warning
 | 
			
		||||
        data = {
 | 
			
		||||
            "id": ping.id,
 | 
			
		||||
            "output": "Reply from 192.168.1.27: Destination host unreachable",
 | 
			
		||||
            "has_stdout": True,
 | 
			
		||||
            "has_stderr": False,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ping.alert_severity = "warning"
 | 
			
		||||
        ping.save()
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=ping.id)
 | 
			
		||||
        self.assertEqual(new_check.status, "failing")
 | 
			
		||||
        self.assertEqual(new_check.alert_severity, "warning")
 | 
			
		||||
 | 
			
		||||
        # test failing error
 | 
			
		||||
        data = {
 | 
			
		||||
            "id": ping.id,
 | 
			
		||||
            "output": "Reply from 192.168.1.27: Destination host unreachable",
 | 
			
		||||
            "has_stdout": True,
 | 
			
		||||
            "has_stderr": False,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ping.alert_severity = "error"
 | 
			
		||||
        ping.save()
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=ping.id)
 | 
			
		||||
        self.assertEqual(new_check.status, "failing")
 | 
			
		||||
        self.assertEqual(new_check.alert_severity, "error")
 | 
			
		||||
 | 
			
		||||
        # test failing error
 | 
			
		||||
        data = {
 | 
			
		||||
            "id": ping.id,
 | 
			
		||||
            "output": "some output",
 | 
			
		||||
            "has_stdout": False,
 | 
			
		||||
            "has_stderr": True,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=ping.id)
 | 
			
		||||
        self.assertEqual(new_check.status, "failing")
 | 
			
		||||
        self.assertEqual(new_check.alert_severity, "error")
 | 
			
		||||
 | 
			
		||||
        # test passing
 | 
			
		||||
        data = {
 | 
			
		||||
            "id": ping.id,
 | 
			
		||||
            "output": "Reply from 192.168.1.1: bytes=32 time<1ms TTL=64",
 | 
			
		||||
            "has_stdout": True,
 | 
			
		||||
            "has_stderr": False,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=ping.id)
 | 
			
		||||
        self.assertEqual(new_check.status, "passing")
 | 
			
		||||
 | 
			
		||||
    @patch("agents.models.Agent.nats_cmd")
 | 
			
		||||
    def test_handle_winsvc_check(self, nats_cmd):
 | 
			
		||||
        from checks.models import Check
 | 
			
		||||
 | 
			
		||||
        url = "/api/v3/checkrunner/"
 | 
			
		||||
 | 
			
		||||
        winsvc = baker.make_recipe(
 | 
			
		||||
            "checks.winsvc_check", agent=self.agent, alert_severity="info"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # test passing running
 | 
			
		||||
        data = {"id": winsvc.id, "exists": True, "status": "running"}
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=winsvc.id)
 | 
			
		||||
        self.assertEqual(new_check.status, "passing")
 | 
			
		||||
 | 
			
		||||
        # test passing start pending
 | 
			
		||||
        winsvc.pass_if_start_pending = True
 | 
			
		||||
        winsvc.save()
 | 
			
		||||
 | 
			
		||||
        data = {"id": winsvc.id, "exists": True, "status": "start_pending"}
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=winsvc.id)
 | 
			
		||||
        self.assertEqual(new_check.status, "passing")
 | 
			
		||||
 | 
			
		||||
        # test failing no start
 | 
			
		||||
        data = {"id": winsvc.id, "exists": True, "status": "not running"}
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=winsvc.id)
 | 
			
		||||
        self.assertEqual(new_check.status, "failing")
 | 
			
		||||
        self.assertEqual(new_check.alert_severity, "info")
 | 
			
		||||
 | 
			
		||||
        # test failing and attempt start
 | 
			
		||||
        winsvc.restart_if_stopped = True
 | 
			
		||||
        winsvc.alert_severity = "warning"
 | 
			
		||||
        winsvc.save()
 | 
			
		||||
 | 
			
		||||
        nats_cmd.return_value = "timeout"
 | 
			
		||||
 | 
			
		||||
        data = {"id": winsvc.id, "exists": True, "status": "not running"}
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=winsvc.id)
 | 
			
		||||
        self.assertEqual(new_check.status, "failing")
 | 
			
		||||
        self.assertEqual(new_check.alert_severity, "warning")
 | 
			
		||||
        nats_cmd.assert_called()
 | 
			
		||||
        nats_cmd.reset_mock()
 | 
			
		||||
 | 
			
		||||
        # test failing and attempt start
 | 
			
		||||
        winsvc.alert_severity = "error"
 | 
			
		||||
        winsvc.save()
 | 
			
		||||
        nats_cmd.return_value = {"success": False, "errormsg": "Some Error"}
 | 
			
		||||
 | 
			
		||||
        data = {"id": winsvc.id, "exists": True, "status": "not running"}
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=winsvc.id)
 | 
			
		||||
        self.assertEqual(new_check.status, "failing")
 | 
			
		||||
        self.assertEqual(new_check.alert_severity, "error")
 | 
			
		||||
        nats_cmd.assert_called()
 | 
			
		||||
        nats_cmd.reset_mock()
 | 
			
		||||
 | 
			
		||||
        # test success and attempt start
 | 
			
		||||
        nats_cmd.return_value = {"success": True}
 | 
			
		||||
 | 
			
		||||
        data = {"id": winsvc.id, "exists": True, "status": "not running"}
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=winsvc.id)
 | 
			
		||||
        self.assertEqual(new_check.status, "passing")
 | 
			
		||||
        nats_cmd.assert_called()
 | 
			
		||||
        nats_cmd.reset_mock()
 | 
			
		||||
 | 
			
		||||
        # test failing and service not exist
 | 
			
		||||
        data = {"id": winsvc.id, "exists": False, "status": ""}
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=winsvc.id)
 | 
			
		||||
        self.assertEqual(new_check.status, "failing")
 | 
			
		||||
 | 
			
		||||
        # test success and service not exist
 | 
			
		||||
        winsvc.pass_if_svc_not_exist = True
 | 
			
		||||
        winsvc.save()
 | 
			
		||||
        data = {"id": winsvc.id, "exists": False, "status": ""}
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=winsvc.id)
 | 
			
		||||
        self.assertEqual(new_check.status, "passing")
 | 
			
		||||
 | 
			
		||||
    def test_handle_eventlog_check(self):
 | 
			
		||||
        from checks.models import Check
 | 
			
		||||
 | 
			
		||||
        url = "/api/v3/checkrunner/"
 | 
			
		||||
 | 
			
		||||
        eventlog = baker.make_recipe(
 | 
			
		||||
            "checks.eventlog_check",
 | 
			
		||||
            event_type="warning",
 | 
			
		||||
            fail_when="contains",
 | 
			
		||||
            event_id=123,
 | 
			
		||||
            alert_severity="warning",
 | 
			
		||||
            agent=self.agent,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        data = {
 | 
			
		||||
            "id": eventlog.id,
 | 
			
		||||
            "log": [
 | 
			
		||||
                {
 | 
			
		||||
                    "eventType": "warning",
 | 
			
		||||
                    "eventID": 150,
 | 
			
		||||
                    "source": "source",
 | 
			
		||||
                    "message": "a test message",
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "eventType": "warning",
 | 
			
		||||
                    "eventID": 123,
 | 
			
		||||
                    "source": "source",
 | 
			
		||||
                    "message": "a test message",
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "eventType": "error",
 | 
			
		||||
                    "eventID": 123,
 | 
			
		||||
                    "source": "source",
 | 
			
		||||
                    "message": "a test message",
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    "eventType": "error",
 | 
			
		||||
                    "eventID": 123,
 | 
			
		||||
                    "source": "source",
 | 
			
		||||
                    "message": "a test message",
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        # test failing when contains
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=eventlog.id)
 | 
			
		||||
 | 
			
		||||
        self.assertEquals(new_check.alert_severity, "warning")
 | 
			
		||||
        self.assertEquals(new_check.status, "failing")
 | 
			
		||||
 | 
			
		||||
        # test passing when not contains and message
 | 
			
		||||
        eventlog.event_message = "doesnt exist"
 | 
			
		||||
        eventlog.save()
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=eventlog.id)
 | 
			
		||||
 | 
			
		||||
        self.assertEquals(new_check.status, "passing")
 | 
			
		||||
 | 
			
		||||
        # test failing when not contains and message and source
 | 
			
		||||
        eventlog.fail_when = "not_contains"
 | 
			
		||||
        eventlog.alert_severity = "error"
 | 
			
		||||
        eventlog.event_message = "doesnt exist"
 | 
			
		||||
        eventlog.event_source = "doesnt exist"
 | 
			
		||||
        eventlog.save()
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=eventlog.id)
 | 
			
		||||
 | 
			
		||||
        self.assertEquals(new_check.status, "failing")
 | 
			
		||||
        self.assertEquals(new_check.alert_severity, "error")
 | 
			
		||||
 | 
			
		||||
        # test passing when contains with source and message
 | 
			
		||||
        eventlog.event_message = "test"
 | 
			
		||||
        eventlog.event_source = "source"
 | 
			
		||||
        eventlog.save()
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=eventlog.id)
 | 
			
		||||
 | 
			
		||||
        self.assertEquals(new_check.status, "passing")
 | 
			
		||||
 | 
			
		||||
        # test failing with wildcard not contains and source
 | 
			
		||||
        eventlog.event_id_is_wildcard = True
 | 
			
		||||
        eventlog.event_source = "doesn't exist"
 | 
			
		||||
        eventlog.event_message = ""
 | 
			
		||||
        eventlog.event_id = 0
 | 
			
		||||
        eventlog.save()
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=eventlog.id)
 | 
			
		||||
 | 
			
		||||
        self.assertEquals(new_check.status, "failing")
 | 
			
		||||
        self.assertEquals(new_check.alert_severity, "error")
 | 
			
		||||
 | 
			
		||||
        # test passing with wildcard contains
 | 
			
		||||
        eventlog.event_source = ""
 | 
			
		||||
        eventlog.event_message = ""
 | 
			
		||||
        eventlog.save()
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=eventlog.id)
 | 
			
		||||
 | 
			
		||||
        self.assertEquals(new_check.status, "passing")
 | 
			
		||||
 | 
			
		||||
        # test failing with wildcard contains and message
 | 
			
		||||
        eventlog.fail_when = "contains"
 | 
			
		||||
        eventlog.event_type = "error"
 | 
			
		||||
        eventlog.alert_severity = "info"
 | 
			
		||||
        eventlog.event_message = "test"
 | 
			
		||||
        eventlog.event_source = ""
 | 
			
		||||
        eventlog.save()
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=eventlog.id)
 | 
			
		||||
 | 
			
		||||
        self.assertEquals(new_check.status, "failing")
 | 
			
		||||
        self.assertEquals(new_check.alert_severity, "info")
 | 
			
		||||
 | 
			
		||||
        # test passing with wildcard not contains message and source
 | 
			
		||||
        eventlog.event_message = "doesnt exist"
 | 
			
		||||
        eventlog.event_source = "doesnt exist"
 | 
			
		||||
        eventlog.save()
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=eventlog.id)
 | 
			
		||||
 | 
			
		||||
        self.assertEquals(new_check.status, "passing")
 | 
			
		||||
 | 
			
		||||
        # test multiple events found and contains
 | 
			
		||||
        # this should pass since only two events are found
 | 
			
		||||
        eventlog.number_of_events_b4_alert = 3
 | 
			
		||||
        eventlog.event_id_is_wildcard = False
 | 
			
		||||
        eventlog.event_source = None
 | 
			
		||||
        eventlog.event_message = None
 | 
			
		||||
        eventlog.event_id = 123
 | 
			
		||||
        eventlog.event_type = "error"
 | 
			
		||||
        eventlog.fail_when = "contains"
 | 
			
		||||
        eventlog.save()
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=eventlog.id)
 | 
			
		||||
 | 
			
		||||
        self.assertEquals(new_check.status, "passing")
 | 
			
		||||
 | 
			
		||||
        # this should pass since there are two events returned
 | 
			
		||||
        eventlog.number_of_events_b4_alert = 2
 | 
			
		||||
        eventlog.save()
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=eventlog.id)
 | 
			
		||||
 | 
			
		||||
        self.assertEquals(new_check.status, "failing")
 | 
			
		||||
 | 
			
		||||
        # test not contains
 | 
			
		||||
        # this should fail since only two events are found
 | 
			
		||||
        eventlog.number_of_events_b4_alert = 3
 | 
			
		||||
        eventlog.event_id_is_wildcard = False
 | 
			
		||||
        eventlog.event_source = None
 | 
			
		||||
        eventlog.event_message = None
 | 
			
		||||
        eventlog.event_id = 123
 | 
			
		||||
        eventlog.event_type = "error"
 | 
			
		||||
        eventlog.fail_when = "not_contains"
 | 
			
		||||
        eventlog.save()
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=eventlog.id)
 | 
			
		||||
 | 
			
		||||
        self.assertEquals(new_check.status, "failing")
 | 
			
		||||
 | 
			
		||||
        # this should pass since there are two events returned
 | 
			
		||||
        eventlog.number_of_events_b4_alert = 2
 | 
			
		||||
        eventlog.save()
 | 
			
		||||
 | 
			
		||||
        resp = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_check = Check.objects.get(pk=eventlog.id)
 | 
			
		||||
 | 
			
		||||
        self.assertEquals(new_check.status, "passing")
 | 
			
		||||
 
 | 
			
		||||
@@ -59,7 +59,7 @@ class AddCheck(APIView):
 | 
			
		||||
        if policy:
 | 
			
		||||
            generate_agent_checks_from_policies_task.delay(policypk=policy.pk)
 | 
			
		||||
        elif agent:
 | 
			
		||||
            checks = agent.agentchecks.filter(
 | 
			
		||||
            checks = agent.agentchecks.filter(  # type: ignore
 | 
			
		||||
                check_type=obj.check_type, managed_by_policy=True
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
@@ -149,7 +149,7 @@ class CheckHistory(APIView):
 | 
			
		||||
                    - djangotime.timedelta(days=request.data["timeFilter"]),
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
        check_history = check.check_history.filter(timeFilter).order_by("-x")
 | 
			
		||||
        check_history = check.check_history.filter(timeFilter).order_by("-x")  # type: ignore
 | 
			
		||||
 | 
			
		||||
        return Response(
 | 
			
		||||
            CheckHistorySerializer(
 | 
			
		||||
@@ -161,8 +161,6 @@ class CheckHistory(APIView):
 | 
			
		||||
@api_view()
 | 
			
		||||
def run_checks(request, pk):
 | 
			
		||||
    agent = get_object_or_404(Agent, pk=pk)
 | 
			
		||||
    if not agent.has_nats:
 | 
			
		||||
        return notify_error("Requires agent version 1.1.0 or greater")
 | 
			
		||||
 | 
			
		||||
    if pyver.parse(agent.version) >= pyver.parse("1.4.1"):
 | 
			
		||||
        r = asyncio.run(agent.nats_cmd({"func": "runchecks"}, timeout=15))
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,9 @@
 | 
			
		||||
from django.contrib import admin
 | 
			
		||||
 | 
			
		||||
from .models import Client, Deployment, Site
 | 
			
		||||
from .models import Client, ClientCustomField, Deployment, Site, SiteCustomField
 | 
			
		||||
 | 
			
		||||
admin.site.register(Client)
 | 
			
		||||
admin.site.register(Site)
 | 
			
		||||
admin.site.register(Deployment)
 | 
			
		||||
admin.site.register(ClientCustomField)
 | 
			
		||||
admin.site.register(SiteCustomField)
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,33 @@
 | 
			
		||||
# 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'),
 | 
			
		||||
        ('clients', '0009_auto_20210212_1408'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='SiteCustomField',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('value', models.TextField(blank=True, null=True)),
 | 
			
		||||
                ('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='site_fields', to='core.customfield')),
 | 
			
		||||
                ('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='custom_fields', to='clients.site')),
 | 
			
		||||
            ],
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='ClientCustomField',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('value', models.TextField(blank=True, null=True)),
 | 
			
		||||
                ('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='custom_fields', to='clients.client')),
 | 
			
		||||
                ('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='client_fields', to='core.customfield')),
 | 
			
		||||
            ],
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,17 @@
 | 
			
		||||
# Generated by Django 3.1.7 on 2021-03-21 15:11
 | 
			
		||||
 | 
			
		||||
from django.db import migrations
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('clients', '0010_clientcustomfield_sitecustomfield'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterUniqueTogether(
 | 
			
		||||
            name='site',
 | 
			
		||||
            unique_together={('client', 'name')},
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
# Generated by Django 3.1.7 on 2021-03-26 06:52
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('clients', '0011_auto_20210321_1511'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='deployment',
 | 
			
		||||
            name='created',
 | 
			
		||||
            field=models.DateTimeField(auto_now_add=True, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,24 @@
 | 
			
		||||
# 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 = [
 | 
			
		||||
        ('clients', '0012_deployment_created'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='clientcustomfield',
 | 
			
		||||
            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),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='sitecustomfield',
 | 
			
		||||
            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,23 @@
 | 
			
		||||
# Generated by Django 3.1.7 on 2021-03-29 03:01
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('clients', '0013_auto_20210329_0251'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='clientcustomfield',
 | 
			
		||||
            name='checkbox_value',
 | 
			
		||||
            field=models.BooleanField(blank=True, default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='sitecustomfield',
 | 
			
		||||
            name='checkbox_value',
 | 
			
		||||
            field=models.BooleanField(blank=True, default=False),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,27 @@
 | 
			
		||||
# Generated by Django 3.1.7 on 2021-03-29 17:09
 | 
			
		||||
 | 
			
		||||
from django.db import migrations
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('clients', '0014_auto_20210329_0301'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.RenameField(
 | 
			
		||||
            model_name='clientcustomfield',
 | 
			
		||||
            old_name='checkbox_value',
 | 
			
		||||
            new_name='bool_value',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RenameField(
 | 
			
		||||
            model_name='clientcustomfield',
 | 
			
		||||
            old_name='value',
 | 
			
		||||
            new_name='string_value',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='sitecustomfield',
 | 
			
		||||
            name='value',
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,23 @@
 | 
			
		||||
# Generated by Django 3.1.7 on 2021-03-29 18:27
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('clients', '0015_auto_20210329_1709'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.RenameField(
 | 
			
		||||
            model_name='sitecustomfield',
 | 
			
		||||
            old_name='checkbox_value',
 | 
			
		||||
            new_name='bool_value',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='sitecustomfield',
 | 
			
		||||
            name='string_value',
 | 
			
		||||
            field=models.TextField(blank=True, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import uuid
 | 
			
		||||
 | 
			
		||||
from django.contrib.postgres.fields import ArrayField
 | 
			
		||||
from django.db import models
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
@@ -32,6 +33,7 @@ class Client(BaseAuditModel):
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def save(self, *args, **kw):
 | 
			
		||||
        from alerts.tasks import cache_agents_alert_template
 | 
			
		||||
        from automation.tasks import generate_agent_checks_by_location_task
 | 
			
		||||
 | 
			
		||||
        # get old client if exists
 | 
			
		||||
@@ -54,6 +56,9 @@ class Client(BaseAuditModel):
 | 
			
		||||
                create_tasks=True,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        if old_client and old_client.alert_template != self.alert_template:
 | 
			
		||||
            cache_agents_alert_template.delay()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        ordering = ("name",)
 | 
			
		||||
 | 
			
		||||
@@ -127,6 +132,7 @@ class Site(BaseAuditModel):
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def save(self, *args, **kw):
 | 
			
		||||
        from alerts.tasks import cache_agents_alert_template
 | 
			
		||||
        from automation.tasks import generate_agent_checks_by_location_task
 | 
			
		||||
 | 
			
		||||
        # get old client if exists
 | 
			
		||||
@@ -149,8 +155,12 @@ class Site(BaseAuditModel):
 | 
			
		||||
                create_tasks=True,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        if old_site and old_site.alert_template != self.alert_template:
 | 
			
		||||
            cache_agents_alert_template.delay()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        ordering = ("name",)
 | 
			
		||||
        unique_together = (("client", "name"),)
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.name
 | 
			
		||||
@@ -217,6 +227,7 @@ class Deployment(models.Model):
 | 
			
		||||
    )
 | 
			
		||||
    arch = models.CharField(max_length=255, choices=ARCH_CHOICES, default="64")
 | 
			
		||||
    expiry = models.DateTimeField(null=True, blank=True)
 | 
			
		||||
    created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
 | 
			
		||||
    auth_token = models.ForeignKey(
 | 
			
		||||
        "knox.AuthToken", related_name="deploytokens", on_delete=models.CASCADE
 | 
			
		||||
    )
 | 
			
		||||
@@ -225,3 +236,73 @@ class Deployment(models.Model):
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return f"{self.client} - {self.site} - {self.mon_type}"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ClientCustomField(models.Model):
 | 
			
		||||
    client = models.ForeignKey(
 | 
			
		||||
        Client,
 | 
			
		||||
        related_name="custom_fields",
 | 
			
		||||
        on_delete=models.CASCADE,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    field = models.ForeignKey(
 | 
			
		||||
        "core.CustomField",
 | 
			
		||||
        related_name="client_fields",
 | 
			
		||||
        on_delete=models.CASCADE,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    string_value = models.TextField(null=True, blank=True)
 | 
			
		||||
    bool_value = models.BooleanField(blank=True, default=False)
 | 
			
		||||
    multiple_value = ArrayField(
 | 
			
		||||
        models.TextField(null=True, blank=True),
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        default=list,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.field.name
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def value(self):
 | 
			
		||||
        if self.field.type == "multiple":
 | 
			
		||||
            return self.multiple_value
 | 
			
		||||
        elif self.field.type == "checkbox":
 | 
			
		||||
            return self.bool_value
 | 
			
		||||
        else:
 | 
			
		||||
            return self.string_value
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SiteCustomField(models.Model):
 | 
			
		||||
    site = models.ForeignKey(
 | 
			
		||||
        Site,
 | 
			
		||||
        related_name="custom_fields",
 | 
			
		||||
        on_delete=models.CASCADE,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    field = models.ForeignKey(
 | 
			
		||||
        "core.CustomField",
 | 
			
		||||
        related_name="site_fields",
 | 
			
		||||
        on_delete=models.CASCADE,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    string_value = models.TextField(null=True, blank=True)
 | 
			
		||||
    bool_value = models.BooleanField(blank=True, default=False)
 | 
			
		||||
    multiple_value = ArrayField(
 | 
			
		||||
        models.TextField(null=True, blank=True),
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        default=list,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.field.name
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def value(self):
 | 
			
		||||
        if self.field.type == "multiple":
 | 
			
		||||
            return self.multiple_value
 | 
			
		||||
        elif self.field.type == "checkbox":
 | 
			
		||||
            return self.bool_value
 | 
			
		||||
        else:
 | 
			
		||||
            return self.string_value
 | 
			
		||||
 
 | 
			
		||||
@@ -1,42 +1,87 @@
 | 
			
		||||
from rest_framework.serializers import ModelSerializer, ReadOnlyField, ValidationError
 | 
			
		||||
 | 
			
		||||
from .models import Client, Deployment, Site
 | 
			
		||||
from .models import Client, ClientCustomField, Deployment, Site, SiteCustomField
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SiteCustomFieldSerializer(ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = SiteCustomField
 | 
			
		||||
        fields = (
 | 
			
		||||
            "id",
 | 
			
		||||
            "field",
 | 
			
		||||
            "site",
 | 
			
		||||
            "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 SiteSerializer(ModelSerializer):
 | 
			
		||||
    client_name = ReadOnlyField(source="client.name")
 | 
			
		||||
    custom_fields = SiteCustomFieldSerializer(many=True, read_only=True)
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Site
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
        fields = (
 | 
			
		||||
            "id",
 | 
			
		||||
            "name",
 | 
			
		||||
            "server_policy",
 | 
			
		||||
            "workstation_policy",
 | 
			
		||||
            "alert_template",
 | 
			
		||||
            "client_name",
 | 
			
		||||
            "client",
 | 
			
		||||
            "custom_fields",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def validate(self, val):
 | 
			
		||||
        if "name" in val.keys() and "|" in val["name"]:
 | 
			
		||||
            raise ValidationError("Site name cannot contain the | character")
 | 
			
		||||
 | 
			
		||||
        if self.context:
 | 
			
		||||
            client = Client.objects.get(pk=self.context["clientpk"])
 | 
			
		||||
            if Site.objects.filter(client=client, name=val["name"]).exists():
 | 
			
		||||
                raise ValidationError(f"Site {val['name']} already exists")
 | 
			
		||||
 | 
			
		||||
        return val
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ClientCustomFieldSerializer(ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = ClientCustomField
 | 
			
		||||
        fields = (
 | 
			
		||||
            "id",
 | 
			
		||||
            "field",
 | 
			
		||||
            "client",
 | 
			
		||||
            "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 ClientSerializer(ModelSerializer):
 | 
			
		||||
    sites = SiteSerializer(many=True, read_only=True)
 | 
			
		||||
    custom_fields = ClientCustomFieldSerializer(many=True, read_only=True)
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Client
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
        fields = (
 | 
			
		||||
            "id",
 | 
			
		||||
            "name",
 | 
			
		||||
            "server_policy",
 | 
			
		||||
            "workstation_policy",
 | 
			
		||||
            "alert_template",
 | 
			
		||||
            "sites",
 | 
			
		||||
            "custom_fields",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def validate(self, val):
 | 
			
		||||
 | 
			
		||||
        if "site" in self.context:
 | 
			
		||||
            if "|" in self.context["site"]:
 | 
			
		||||
                raise ValidationError("Site name cannot contain the | character")
 | 
			
		||||
            if len(self.context["site"]) > 255:
 | 
			
		||||
                raise ValidationError("Site name too long")
 | 
			
		||||
 | 
			
		||||
        if "name" in val.keys() and "|" in val["name"]:
 | 
			
		||||
            raise ValidationError("Client name cannot contain the | character")
 | 
			
		||||
 | 
			
		||||
@@ -83,4 +128,5 @@ class DeploymentSerializer(ModelSerializer):
 | 
			
		||||
            "arch",
 | 
			
		||||
            "expiry",
 | 
			
		||||
            "install_flags",
 | 
			
		||||
            "created",
 | 
			
		||||
        ]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,12 @@
 | 
			
		||||
import uuid
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
 | 
			
		||||
from model_bakery import baker
 | 
			
		||||
from rest_framework.serializers import ValidationError
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
 | 
			
		||||
from .models import Client, Deployment, Site
 | 
			
		||||
from .models import Client, ClientCustomField, Deployment, Site, SiteCustomField
 | 
			
		||||
from .serializers import (
 | 
			
		||||
    ClientSerializer,
 | 
			
		||||
    ClientTreeSerializer,
 | 
			
		||||
@@ -28,18 +29,29 @@ class TestClientViews(TacticalTestCase):
 | 
			
		||||
        r = self.client.get(url, format="json")
 | 
			
		||||
        serializer = ClientSerializer(clients, many=True)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(r.data, serializer.data)
 | 
			
		||||
        self.assertEqual(r.data, serializer.data)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    def test_add_client(self):
 | 
			
		||||
        url = "/clients/clients/"
 | 
			
		||||
        payload = {"client": "Company 1", "site": "Site 1"}
 | 
			
		||||
 | 
			
		||||
        # test successfull add client
 | 
			
		||||
        payload = {
 | 
			
		||||
            "client": {"name": "Client1"},
 | 
			
		||||
            "site": {"name": "Site1"},
 | 
			
		||||
            "custom_fields": [],
 | 
			
		||||
        }
 | 
			
		||||
        r = self.client.post(url, payload, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        payload["client"] = "Company1|askd"
 | 
			
		||||
        serializer = ClientSerializer(data={"name": payload["client"]}, context=payload)
 | 
			
		||||
        # test add client with | in name
 | 
			
		||||
        payload = {
 | 
			
		||||
            "client": {"name": "Client2|d"},
 | 
			
		||||
            "site": {"name": "Site1"},
 | 
			
		||||
            "custom_fields": [],
 | 
			
		||||
        }
 | 
			
		||||
        serializer = ClientSerializer(data=payload["client"])
 | 
			
		||||
        with self.assertRaisesMessage(
 | 
			
		||||
            ValidationError, "Client name cannot contain the | character"
 | 
			
		||||
        ):
 | 
			
		||||
@@ -48,19 +60,22 @@ class TestClientViews(TacticalTestCase):
 | 
			
		||||
        r = self.client.post(url, payload, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
 | 
			
		||||
        payload = {"client": "Company 156", "site": "Site2|a34"}
 | 
			
		||||
        serializer = ClientSerializer(data={"name": payload["client"]}, context=payload)
 | 
			
		||||
        with self.assertRaisesMessage(
 | 
			
		||||
            ValidationError, "Site name cannot contain the | character"
 | 
			
		||||
        ):
 | 
			
		||||
            self.assertFalse(serializer.is_valid(raise_exception=True))
 | 
			
		||||
 | 
			
		||||
        # test add client with | in Site name
 | 
			
		||||
        payload = {
 | 
			
		||||
            "client": {"name": "Client2"},
 | 
			
		||||
            "site": {"name": "Site1|fds"},
 | 
			
		||||
            "custom_fields": [],
 | 
			
		||||
        }
 | 
			
		||||
        r = self.client.post(url, payload, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
 | 
			
		||||
        # test unique
 | 
			
		||||
        payload = {"client": "Company 1", "site": "Site 1"}
 | 
			
		||||
        serializer = ClientSerializer(data={"name": payload["client"]}, context=payload)
 | 
			
		||||
        payload = {
 | 
			
		||||
            "client": {"name": "Client1"},
 | 
			
		||||
            "site": {"name": "Site1"},
 | 
			
		||||
            "custom_fields": [],
 | 
			
		||||
        }
 | 
			
		||||
        serializer = ClientSerializer(data=payload["client"])
 | 
			
		||||
        with self.assertRaisesMessage(
 | 
			
		||||
            ValidationError, "client with this name already exists."
 | 
			
		||||
        ):
 | 
			
		||||
@@ -69,67 +84,129 @@ class TestClientViews(TacticalTestCase):
 | 
			
		||||
        r = self.client.post(url, payload, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
 | 
			
		||||
        # test long site name
 | 
			
		||||
        payload = {"client": "Company 2394", "site": "Site123" * 100}
 | 
			
		||||
        serializer = ClientSerializer(data={"name": payload["client"]}, context=payload)
 | 
			
		||||
        with self.assertRaisesMessage(ValidationError, "Site name too long"):
 | 
			
		||||
            self.assertFalse(serializer.is_valid(raise_exception=True))
 | 
			
		||||
 | 
			
		||||
        r = self.client.post(url, payload, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
 | 
			
		||||
        # test initial setup
 | 
			
		||||
        payload = {
 | 
			
		||||
            "client": {"client": "Company 4", "site": "HQ"},
 | 
			
		||||
            "initialsetup": True,
 | 
			
		||||
            "client": {"name": "Setup Client"},
 | 
			
		||||
            "site": {"name": "Setup  Site"},
 | 
			
		||||
            "timezone": "America/Los_Angeles",
 | 
			
		||||
            "initialsetup": True,
 | 
			
		||||
        }
 | 
			
		||||
        r = self.client.post(url, payload, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        # test add with custom fields
 | 
			
		||||
        field = baker.make("core.CustomField", model="client", type="text")
 | 
			
		||||
        payload = {
 | 
			
		||||
            "client": {"name": "Custom Field Client"},
 | 
			
		||||
            "site": {"name": "Setup  Site"},
 | 
			
		||||
            "custom_fields": [{"field": field.id, "string_value": "new Value"}],  # type: ignore
 | 
			
		||||
        }
 | 
			
		||||
        r = self.client.post(url, payload, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        client = Client.objects.get(name="Custom Field Client")
 | 
			
		||||
        self.assertTrue(
 | 
			
		||||
            ClientCustomField.objects.filter(client=client, field=field).exists()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
    def test_get_client(self):
 | 
			
		||||
        # setup data
 | 
			
		||||
        client = baker.make("clients.Client")
 | 
			
		||||
 | 
			
		||||
        url = f"/clients/{client.id}/client/"  # type: ignore
 | 
			
		||||
        r = self.client.get(url, format="json")
 | 
			
		||||
        serializer = ClientSerializer(client)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(r.data, serializer.data)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    def test_edit_client(self):
 | 
			
		||||
        # setup data
 | 
			
		||||
        client = baker.make("clients.Client")
 | 
			
		||||
        client = baker.make("clients.Client", name="OldClientName")
 | 
			
		||||
 | 
			
		||||
        # test invalid id
 | 
			
		||||
        r = self.client.put("/clients/500/client/", format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        data = {"id": client.id, "name": "New Name"}
 | 
			
		||||
 | 
			
		||||
        url = f"/clients/{client.id}/client/"
 | 
			
		||||
        # test successfull edit client
 | 
			
		||||
        data = {"client": {"name": "NewClientName"}, "custom_fields": []}
 | 
			
		||||
        url = f"/clients/{client.id}/client/"  # type: ignore
 | 
			
		||||
        r = self.client.put(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertTrue(Client.objects.filter(name="New Name").exists())
 | 
			
		||||
        self.assertTrue(Client.objects.filter(name="NewClientName").exists())
 | 
			
		||||
        self.assertFalse(Client.objects.filter(name="OldClientName").exists())
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("put", url)
 | 
			
		||||
 | 
			
		||||
    def test_delete_client(self):
 | 
			
		||||
        # setup data
 | 
			
		||||
        client = baker.make("clients.Client")
 | 
			
		||||
        site = baker.make("clients.Site", client=client)
 | 
			
		||||
        agent = baker.make_recipe("agents.agent", site=site)
 | 
			
		||||
 | 
			
		||||
        # test invalid id
 | 
			
		||||
        r = self.client.delete("/clients/500/client/", format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        url = f"/clients/{client.id}/client/"
 | 
			
		||||
 | 
			
		||||
        # test deleting with agents under client
 | 
			
		||||
        r = self.client.delete(url, format="json")
 | 
			
		||||
        # test edit client with | in name
 | 
			
		||||
        data = {"client": {"name": "NewClie|ntName"}, "custom_fields": []}
 | 
			
		||||
        url = f"/clients/{client.id}/client/"  # type: ignore
 | 
			
		||||
        r = self.client.put(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
 | 
			
		||||
        # test successful deletion
 | 
			
		||||
        agent.delete()
 | 
			
		||||
        r = self.client.delete(url, format="json")
 | 
			
		||||
        # test add with custom fields new value
 | 
			
		||||
        field = baker.make("core.CustomField", model="client", type="checkbox")
 | 
			
		||||
        payload = {
 | 
			
		||||
            "client": {
 | 
			
		||||
                "id": client.id,  # type: ignore
 | 
			
		||||
                "name": "Custom Field Client",
 | 
			
		||||
            },
 | 
			
		||||
            "custom_fields": [{"field": field.id, "bool_value": True}],  # type: ignore
 | 
			
		||||
        }
 | 
			
		||||
        r = self.client.put(url, payload, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertFalse(Client.objects.filter(pk=client.id).exists())
 | 
			
		||||
        self.assertFalse(Site.objects.filter(pk=site.id).exists())
 | 
			
		||||
 | 
			
		||||
        client = Client.objects.get(name="Custom Field Client")
 | 
			
		||||
        self.assertTrue(
 | 
			
		||||
            ClientCustomField.objects.filter(client=client, field=field).exists()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # edit custom field value
 | 
			
		||||
        payload = {
 | 
			
		||||
            "client": {
 | 
			
		||||
                "id": client.id,  # type: ignore
 | 
			
		||||
                "name": "Custom Field Client",
 | 
			
		||||
            },
 | 
			
		||||
            "custom_fields": [{"field": field.id, "bool_value": False}],  # type: ignore
 | 
			
		||||
        }
 | 
			
		||||
        r = self.client.put(url, payload, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        self.assertFalse(
 | 
			
		||||
            ClientCustomField.objects.get(client=client, field=field).value
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("put", url)
 | 
			
		||||
 | 
			
		||||
    @patch("automation.tasks.generate_all_agent_checks_task.delay")
 | 
			
		||||
    @patch("automation.tasks.generate_all_agent_checks_task.delay")
 | 
			
		||||
    def test_delete_client(self, task1, task2):
 | 
			
		||||
        from agents.models import Agent
 | 
			
		||||
 | 
			
		||||
        task1.return_value = "ok"
 | 
			
		||||
        task2.return_value = "ok"
 | 
			
		||||
        # setup data
 | 
			
		||||
        client_to_delete = baker.make("clients.Client")
 | 
			
		||||
        client_to_move = baker.make("clients.Client")
 | 
			
		||||
        site_to_move = baker.make("clients.Site", client=client_to_move)
 | 
			
		||||
        agent = baker.make_recipe("agents.agent", site=site_to_move)
 | 
			
		||||
 | 
			
		||||
        # test invalid id
 | 
			
		||||
        r = self.client.delete("/clients/334/953/", format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        url = f"/clients/{client_to_delete.id}/{site_to_move.id}/"  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # test successful deletion
 | 
			
		||||
        r = self.client.delete(url, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        agent_moved = Agent.objects.get(pk=agent.pk)
 | 
			
		||||
        self.assertEqual(agent_moved.site.id, site_to_move.id)  # type: ignore
 | 
			
		||||
        self.assertFalse(Client.objects.filter(pk=client_to_delete.id).exists())  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("delete", url)
 | 
			
		||||
 | 
			
		||||
    def test_get_sites(self):
 | 
			
		||||
        # setup data
 | 
			
		||||
        baker.make("clients.Site", _quantity=5)
 | 
			
		||||
@@ -139,29 +216,31 @@ class TestClientViews(TacticalTestCase):
 | 
			
		||||
        r = self.client.get(url, format="json")
 | 
			
		||||
        serializer = SiteSerializer(sites, many=True)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(r.data, serializer.data)
 | 
			
		||||
        self.assertEqual(r.data, serializer.data)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    def test_add_site(self):
 | 
			
		||||
        # setup data
 | 
			
		||||
        site = baker.make("clients.Site")
 | 
			
		||||
        client = baker.make("clients.Client")
 | 
			
		||||
        site = baker.make("clients.Site", client=client)
 | 
			
		||||
 | 
			
		||||
        url = "/clients/sites/"
 | 
			
		||||
 | 
			
		||||
        # test success add
 | 
			
		||||
        payload = {"client": site.client.id, "name": "LA Office"}
 | 
			
		||||
        payload = {
 | 
			
		||||
            "site": {"client": client.id, "name": "LA Office"},  # type: ignore
 | 
			
		||||
            "custom_fields": [],
 | 
			
		||||
        }
 | 
			
		||||
        r = self.client.post(url, payload, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertTrue(
 | 
			
		||||
            Site.objects.filter(
 | 
			
		||||
                name="LA Office", client__name=site.client.name
 | 
			
		||||
            ).exists()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # test with | symbol
 | 
			
		||||
        payload = {"client": site.client.id, "name": "LA Off|ice  |*&@#$"}
 | 
			
		||||
        serializer = SiteSerializer(data=payload, context={"clientpk": site.client.id})
 | 
			
		||||
        payload = {
 | 
			
		||||
            "site": {"client": client.id, "name": "LA Office  |*&@#$"},  # type: ignore
 | 
			
		||||
            "custom_fields": [],
 | 
			
		||||
        }
 | 
			
		||||
        serializer = SiteSerializer(data=payload["site"])
 | 
			
		||||
        with self.assertRaisesMessage(
 | 
			
		||||
            ValidationError, "Site name cannot contain the | character"
 | 
			
		||||
        ):
 | 
			
		||||
@@ -171,55 +250,139 @@ class TestClientViews(TacticalTestCase):
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
 | 
			
		||||
        # test site already exists
 | 
			
		||||
        payload = {"client": site.client.id, "name": "LA Office"}
 | 
			
		||||
        serializer = SiteSerializer(data=payload, context={"clientpk": site.client.id})
 | 
			
		||||
        with self.assertRaisesMessage(ValidationError, "Site LA Office already exists"):
 | 
			
		||||
        payload = {
 | 
			
		||||
            "site": {"client": site.client.id, "name": "LA Office"},  # type: ignore
 | 
			
		||||
            "custom_fields": [],
 | 
			
		||||
        }
 | 
			
		||||
        serializer = SiteSerializer(data=payload["site"])
 | 
			
		||||
        with self.assertRaisesMessage(
 | 
			
		||||
            ValidationError, "The fields client, name must make a unique set."
 | 
			
		||||
        ):
 | 
			
		||||
            self.assertFalse(serializer.is_valid(raise_exception=True))
 | 
			
		||||
 | 
			
		||||
        # test add with custom fields
 | 
			
		||||
        field = baker.make(
 | 
			
		||||
            "core.CustomField",
 | 
			
		||||
            model="site",
 | 
			
		||||
            type="single",
 | 
			
		||||
            options=["one", "two", "three"],
 | 
			
		||||
        )
 | 
			
		||||
        payload = {
 | 
			
		||||
            "site": {"client": client.id, "name": "Custom Field Site"},  # type: ignore
 | 
			
		||||
            "custom_fields": [{"field": field.id, "string_value": "one"}],  # type: ignore
 | 
			
		||||
        }
 | 
			
		||||
        r = self.client.post(url, payload, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        site = Site.objects.get(name="Custom Field Site")
 | 
			
		||||
        self.assertTrue(SiteCustomField.objects.filter(site=site, field=field).exists())
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
    def test_edit_site(self):
 | 
			
		||||
    def test_get_site(self):
 | 
			
		||||
        # setup data
 | 
			
		||||
        site = baker.make("clients.Site")
 | 
			
		||||
 | 
			
		||||
        url = f"/clients/sites/{site.id}/"  # type: ignore
 | 
			
		||||
        r = self.client.get(url, format="json")
 | 
			
		||||
        serializer = SiteSerializer(site)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(r.data, serializer.data)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    def test_edit_site(self):
 | 
			
		||||
        # setup data
 | 
			
		||||
        client = baker.make("clients.Client")
 | 
			
		||||
        site = baker.make("clients.Site", client=client)
 | 
			
		||||
 | 
			
		||||
        # test invalid id
 | 
			
		||||
        r = self.client.put("/clients/500/site/", format="json")
 | 
			
		||||
        r = self.client.put("/clients/sites/688/", format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        data = {"id": site.id, "name": "New Name", "client": site.client.id}
 | 
			
		||||
        data = {
 | 
			
		||||
            "site": {"client": client.id, "name": "New Site Name"},  # type: ignore
 | 
			
		||||
            "custom_fields": [],
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        url = f"/clients/{site.id}/site/"
 | 
			
		||||
        url = f"/clients/sites/{site.id}/"  # type: ignore
 | 
			
		||||
        r = self.client.put(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertTrue(Site.objects.filter(name="New Name").exists())
 | 
			
		||||
        self.assertTrue(
 | 
			
		||||
            Site.objects.filter(client=client, name="New Site Name").exists()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # test add with custom fields new value
 | 
			
		||||
        field = baker.make(
 | 
			
		||||
            "core.CustomField",
 | 
			
		||||
            model="site",
 | 
			
		||||
            type="multiple",
 | 
			
		||||
            options=["one", "two", "three"],
 | 
			
		||||
        )
 | 
			
		||||
        payload = {
 | 
			
		||||
            "site": {
 | 
			
		||||
                "id": site.id,  # type: ignore
 | 
			
		||||
                "client": site.client.id,  # type: ignore
 | 
			
		||||
                "name": "Custom Field Site",
 | 
			
		||||
            },
 | 
			
		||||
            "custom_fields": [{"field": field.id, "multiple_value": ["two", "three"]}],  # type: ignore
 | 
			
		||||
        }
 | 
			
		||||
        r = self.client.put(url, payload, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        site = Site.objects.get(name="Custom Field Site")
 | 
			
		||||
        self.assertTrue(SiteCustomField.objects.filter(site=site, field=field).exists())
 | 
			
		||||
 | 
			
		||||
        # edit custom field value
 | 
			
		||||
        payload = {
 | 
			
		||||
            "site": {
 | 
			
		||||
                "id": site.id,  # type: ignore
 | 
			
		||||
                "client": client.id,  # type: ignore
 | 
			
		||||
                "name": "Custom Field Site",
 | 
			
		||||
            },
 | 
			
		||||
            "custom_fields": [{"field": field.id, "multiple_value": ["one"]}],  # type: ignore
 | 
			
		||||
        }
 | 
			
		||||
        r = self.client.put(url, payload, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        self.assertTrue(
 | 
			
		||||
            SiteCustomField.objects.get(site=site, field=field).value,
 | 
			
		||||
            ["one"],
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("put", url)
 | 
			
		||||
 | 
			
		||||
    def test_delete_site(self):
 | 
			
		||||
    @patch("automation.tasks.generate_all_agent_checks_task.delay")
 | 
			
		||||
    @patch("automation.tasks.generate_all_agent_checks_task.delay")
 | 
			
		||||
    def test_delete_site(self, task1, task2):
 | 
			
		||||
        from agents.models import Agent
 | 
			
		||||
 | 
			
		||||
        task1.return_value = "ok"
 | 
			
		||||
        task2.return_value = "ok"
 | 
			
		||||
        # setup data
 | 
			
		||||
        site = baker.make("clients.Site")
 | 
			
		||||
        agent = baker.make_recipe("agents.agent", site=site)
 | 
			
		||||
        client = baker.make("clients.Client")
 | 
			
		||||
        site_to_delete = baker.make("clients.Site", client=client)
 | 
			
		||||
        site_to_move = baker.make("clients.Site")
 | 
			
		||||
        agent = baker.make_recipe("agents.agent", site=site_to_delete)
 | 
			
		||||
 | 
			
		||||
        # test invalid id
 | 
			
		||||
        r = self.client.delete("/clients/500/site/", format="json")
 | 
			
		||||
        r = self.client.delete("/clients/500/445/", format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        url = f"/clients/{site.id}/site/"
 | 
			
		||||
        url = f"/clients/sites/{site_to_delete.id}/{site_to_move.id}/"  # type: ignore
 | 
			
		||||
 | 
			
		||||
        # test deleting with last site under client
 | 
			
		||||
        r = self.client.delete(url, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
 | 
			
		||||
        # test deletion when agents exist under site
 | 
			
		||||
        baker.make("clients.Site", client=site.client)
 | 
			
		||||
        r = self.client.delete(url, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
        self.assertEqual(r.json(), "A client must have at least 1 site.")
 | 
			
		||||
 | 
			
		||||
        # test successful deletion
 | 
			
		||||
        agent.delete()
 | 
			
		||||
        site_to_move.client = client  # type: ignore
 | 
			
		||||
        site_to_move.save(update_fields=["client"])  # type: ignore
 | 
			
		||||
        r = self.client.delete(url, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertFalse(Site.objects.filter(pk=site.id).exists())
 | 
			
		||||
        agent_moved = Agent.objects.get(pk=agent.pk)
 | 
			
		||||
        self.assertEqual(agent_moved.site.id, site_to_move.id)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("delete", url)
 | 
			
		||||
 | 
			
		||||
@@ -233,7 +396,7 @@ class TestClientViews(TacticalTestCase):
 | 
			
		||||
        r = self.client.get(url, format="json")
 | 
			
		||||
        serializer = ClientTreeSerializer(clients, many=True)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(r.data, serializer.data)
 | 
			
		||||
        self.assertEqual(r.data, serializer.data)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
@@ -245,7 +408,7 @@ class TestClientViews(TacticalTestCase):
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        serializer = DeploymentSerializer(deployments, many=True)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(r.data, serializer.data)
 | 
			
		||||
        self.assertEqual(r.data, serializer.data)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
@@ -255,8 +418,8 @@ class TestClientViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        url = "/clients/deployments/"
 | 
			
		||||
        payload = {
 | 
			
		||||
            "client": site.client.id,
 | 
			
		||||
            "site": site.id,
 | 
			
		||||
            "client": site.client.id,  # type: ignore
 | 
			
		||||
            "site": site.id,  # type: ignore
 | 
			
		||||
            "expires": "2037-11-23 18:53",
 | 
			
		||||
            "power": 1,
 | 
			
		||||
            "ping": 0,
 | 
			
		||||
@@ -284,10 +447,10 @@ class TestClientViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        url = "/clients/deployments/"
 | 
			
		||||
 | 
			
		||||
        url = f"/clients/{deployment.id}/deployment/"
 | 
			
		||||
        url = f"/clients/{deployment.id}/deployment/"  # type: ignore
 | 
			
		||||
        r = self.client.delete(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertFalse(Deployment.objects.filter(pk=deployment.id).exists())
 | 
			
		||||
        self.assertFalse(Deployment.objects.filter(pk=deployment.id).exists())  # type: ignore
 | 
			
		||||
 | 
			
		||||
        url = "/clients/32348/deployment/"
 | 
			
		||||
        r = self.client.delete(url)
 | 
			
		||||
@@ -301,7 +464,7 @@ class TestClientViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
        self.assertEqual(r.data, "invalid")
 | 
			
		||||
        self.assertEqual(r.data, "invalid")  # type: ignore
 | 
			
		||||
 | 
			
		||||
        uid = uuid.uuid4()
 | 
			
		||||
        url = f"/clients/{uid}/deploy/"
 | 
			
		||||
 
 | 
			
		||||
@@ -4,10 +4,12 @@ from . import views
 | 
			
		||||
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
    path("clients/", views.GetAddClients.as_view()),
 | 
			
		||||
    path("<int:pk>/client/", views.GetUpdateDeleteClient.as_view()),
 | 
			
		||||
    path("<int:pk>/client/", views.GetUpdateClient.as_view()),
 | 
			
		||||
    path("<int:pk>/<int:sitepk>/", views.DeleteClient.as_view()),
 | 
			
		||||
    path("tree/", views.GetClientTree.as_view()),
 | 
			
		||||
    path("sites/", views.GetAddSites.as_view()),
 | 
			
		||||
    path("<int:pk>/site/", views.GetUpdateDeleteSite.as_view()),
 | 
			
		||||
    path("sites/<int:pk>/", views.GetUpdateSite.as_view()),
 | 
			
		||||
    path("sites/<int:pk>/<int:sitepk>/", views.DeleteSite.as_view()),
 | 
			
		||||
    path("deployments/", views.AgentDeployment.as_view()),
 | 
			
		||||
    path("<int:pk>/deployment/", views.AgentDeployment.as_view()),
 | 
			
		||||
    path("<str:uid>/deploy/", views.GenerateAgent.as_view()),
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,12 @@
 | 
			
		||||
import datetime as dt
 | 
			
		||||
import os
 | 
			
		||||
import re
 | 
			
		||||
import subprocess
 | 
			
		||||
import uuid
 | 
			
		||||
 | 
			
		||||
import pytz
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.http import HttpResponse
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from django.utils import timezone as djangotime
 | 
			
		||||
from loguru import logger
 | 
			
		||||
from rest_framework.permissions import AllowAny
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
@@ -17,14 +15,18 @@ from agents.models import Agent
 | 
			
		||||
from core.models import CoreSettings
 | 
			
		||||
from tacticalrmm.utils import notify_error
 | 
			
		||||
 | 
			
		||||
from .models import Client, Deployment, Site
 | 
			
		||||
from .models import Client, ClientCustomField, Deployment, Site, SiteCustomField
 | 
			
		||||
from .serializers import (
 | 
			
		||||
    ClientCustomFieldSerializer,
 | 
			
		||||
    ClientSerializer,
 | 
			
		||||
    ClientTreeSerializer,
 | 
			
		||||
    DeploymentSerializer,
 | 
			
		||||
    SiteCustomFieldSerializer,
 | 
			
		||||
    SiteSerializer,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
logger.configure(**settings.LOG_CONFIG)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetAddClients(APIView):
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
@@ -32,44 +34,98 @@ class GetAddClients(APIView):
 | 
			
		||||
        return Response(ClientSerializer(clients, many=True).data)
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        # create client
 | 
			
		||||
        client_serializer = ClientSerializer(data=request.data["client"])
 | 
			
		||||
        client_serializer.is_valid(raise_exception=True)
 | 
			
		||||
        client = client_serializer.save()
 | 
			
		||||
 | 
			
		||||
        if "initialsetup" in request.data:
 | 
			
		||||
            client = {"name": request.data["client"]["client"].strip()}
 | 
			
		||||
            site = {"name": request.data["client"]["site"].strip()}
 | 
			
		||||
            serializer = ClientSerializer(data=client, context=request.data["client"])
 | 
			
		||||
            serializer.is_valid(raise_exception=True)
 | 
			
		||||
        # create site
 | 
			
		||||
        site_serializer = SiteSerializer(
 | 
			
		||||
            data={"client": client.id, "name": request.data["site"]["name"]}
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # make sure site serializer doesn't return errors and save
 | 
			
		||||
        if site_serializer.is_valid():
 | 
			
		||||
            site_serializer.save()
 | 
			
		||||
        else:
 | 
			
		||||
            # delete client since site serializer was invalid
 | 
			
		||||
            client.delete()
 | 
			
		||||
            site_serializer.is_valid(raise_exception=True)
 | 
			
		||||
 | 
			
		||||
        if "initialsetup" in request.data.keys():
 | 
			
		||||
            core = CoreSettings.objects.first()
 | 
			
		||||
            core.default_time_zone = request.data["timezone"]
 | 
			
		||||
            core.save(update_fields=["default_time_zone"])
 | 
			
		||||
        else:
 | 
			
		||||
            client = {"name": request.data["client"].strip()}
 | 
			
		||||
            site = {"name": request.data["site"].strip()}
 | 
			
		||||
            serializer = ClientSerializer(data=client, context=request.data)
 | 
			
		||||
            serializer.is_valid(raise_exception=True)
 | 
			
		||||
 | 
			
		||||
        obj = serializer.save()
 | 
			
		||||
        Site(client=obj, name=site["name"]).save()
 | 
			
		||||
        # save custom fields
 | 
			
		||||
        if "custom_fields" in request.data.keys():
 | 
			
		||||
            for field in request.data["custom_fields"]:
 | 
			
		||||
 | 
			
		||||
        return Response(f"{obj} was added!")
 | 
			
		||||
                custom_field = field
 | 
			
		||||
                custom_field["client"] = client.id
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetUpdateDeleteClient(APIView):
 | 
			
		||||
    def put(self, request, pk):
 | 
			
		||||
        client = get_object_or_404(Client, pk=pk)
 | 
			
		||||
 | 
			
		||||
        serializer = ClientSerializer(data=request.data, instance=client, partial=True)
 | 
			
		||||
                serializer = ClientCustomFieldSerializer(data=custom_field)
 | 
			
		||||
                serializer.is_valid(raise_exception=True)
 | 
			
		||||
                serializer.save()
 | 
			
		||||
 | 
			
		||||
        return Response("The Client was renamed")
 | 
			
		||||
        return Response(f"{client} was added!")
 | 
			
		||||
 | 
			
		||||
    def delete(self, request, pk):
 | 
			
		||||
 | 
			
		||||
class GetUpdateClient(APIView):
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
        client = get_object_or_404(Client, pk=pk)
 | 
			
		||||
        agent_count = Agent.objects.filter(site__client=client).count()
 | 
			
		||||
        if agent_count > 0:
 | 
			
		||||
            return notify_error(
 | 
			
		||||
                f"Cannot delete {client} while {agent_count} agents exist in it. Move the agents to another client first."
 | 
			
		||||
        return Response(ClientSerializer(client).data)
 | 
			
		||||
 | 
			
		||||
    def put(self, request, pk):
 | 
			
		||||
        client = get_object_or_404(Client, pk=pk)
 | 
			
		||||
 | 
			
		||||
        serializer = ClientSerializer(
 | 
			
		||||
            data=request.data["client"], instance=client, partial=True
 | 
			
		||||
        )
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        serializer.save()
 | 
			
		||||
 | 
			
		||||
        # update custom fields
 | 
			
		||||
        if "custom_fields" in request.data.keys():
 | 
			
		||||
            for field in request.data["custom_fields"]:
 | 
			
		||||
 | 
			
		||||
                custom_field = field
 | 
			
		||||
                custom_field["client"] = pk
 | 
			
		||||
 | 
			
		||||
                if ClientCustomField.objects.filter(field=field["field"], client=pk):
 | 
			
		||||
                    value = ClientCustomField.objects.get(
 | 
			
		||||
                        field=field["field"], client=pk
 | 
			
		||||
                    )
 | 
			
		||||
                    serializer = ClientCustomFieldSerializer(
 | 
			
		||||
                        instance=value, data=custom_field
 | 
			
		||||
                    )
 | 
			
		||||
                    serializer.is_valid(raise_exception=True)
 | 
			
		||||
                    serializer.save()
 | 
			
		||||
                else:
 | 
			
		||||
                    serializer = ClientCustomFieldSerializer(data=custom_field)
 | 
			
		||||
                    serializer.is_valid(raise_exception=True)
 | 
			
		||||
                    serializer.save()
 | 
			
		||||
 | 
			
		||||
        return Response("The Client was updated")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DeleteClient(APIView):
 | 
			
		||||
    def delete(self, request, pk, sitepk):
 | 
			
		||||
        from automation.tasks import generate_all_agent_checks_task
 | 
			
		||||
 | 
			
		||||
        client = get_object_or_404(Client, pk=pk)
 | 
			
		||||
        agents = Agent.objects.filter(site__client=client)
 | 
			
		||||
 | 
			
		||||
        if not sitepk:
 | 
			
		||||
            return notify_error(
 | 
			
		||||
                "There needs to be a site specified to move existing agents to"
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        site = get_object_or_404(Site, pk=sitepk)
 | 
			
		||||
        agents.update(site=site)
 | 
			
		||||
 | 
			
		||||
        generate_all_agent_checks_task.delay("workstation", create_tasks=True)
 | 
			
		||||
        generate_all_agent_checks_task.delay("server", create_tasks=True)
 | 
			
		||||
 | 
			
		||||
        client.delete()
 | 
			
		||||
        return Response(f"{client.name} was deleted!")
 | 
			
		||||
@@ -87,39 +143,90 @@ class GetAddSites(APIView):
 | 
			
		||||
        return Response(SiteSerializer(sites, many=True).data)
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        name = request.data["name"].strip()
 | 
			
		||||
        serializer = SiteSerializer(data=request.data["site"])
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        site = serializer.save()
 | 
			
		||||
 | 
			
		||||
        # save custom fields
 | 
			
		||||
        if "custom_fields" in request.data.keys():
 | 
			
		||||
 | 
			
		||||
            for field in request.data["custom_fields"]:
 | 
			
		||||
 | 
			
		||||
                custom_field = field
 | 
			
		||||
                custom_field["site"] = site.id
 | 
			
		||||
 | 
			
		||||
                serializer = SiteCustomFieldSerializer(data=custom_field)
 | 
			
		||||
                serializer.is_valid(raise_exception=True)
 | 
			
		||||
                serializer.save()
 | 
			
		||||
 | 
			
		||||
        return Response(f"Site {site.name} was added!")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetUpdateSite(APIView):
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
        site = get_object_or_404(Site, pk=pk)
 | 
			
		||||
        return Response(SiteSerializer(site).data)
 | 
			
		||||
 | 
			
		||||
    def put(self, request, pk):
 | 
			
		||||
        site = get_object_or_404(Site, pk=pk)
 | 
			
		||||
 | 
			
		||||
        if "client" in request.data["site"].keys() and (
 | 
			
		||||
            site.client.id != request.data["site"]["client"]
 | 
			
		||||
            and site.client.sites.count() == 1
 | 
			
		||||
        ):
 | 
			
		||||
            return notify_error("A client must have at least one site")
 | 
			
		||||
 | 
			
		||||
        serializer = SiteSerializer(
 | 
			
		||||
            data={"name": name, "client": request.data["client"]},
 | 
			
		||||
            context={"clientpk": request.data["client"]},
 | 
			
		||||
            instance=site, data=request.data["site"], partial=True
 | 
			
		||||
        )
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        serializer.save()
 | 
			
		||||
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
        # update custom field
 | 
			
		||||
        if "custom_fields" in request.data.keys():
 | 
			
		||||
 | 
			
		||||
            for field in request.data["custom_fields"]:
 | 
			
		||||
 | 
			
		||||
class GetUpdateDeleteSite(APIView):
 | 
			
		||||
    def put(self, request, pk):
 | 
			
		||||
                custom_field = field
 | 
			
		||||
                custom_field["site"] = pk
 | 
			
		||||
 | 
			
		||||
        site = get_object_or_404(Site, pk=pk)
 | 
			
		||||
        serializer = SiteSerializer(instance=site, data=request.data, partial=True)
 | 
			
		||||
                if SiteCustomField.objects.filter(field=field["field"], site=pk):
 | 
			
		||||
                    value = SiteCustomField.objects.get(field=field["field"], site=pk)
 | 
			
		||||
                    serializer = SiteCustomFieldSerializer(
 | 
			
		||||
                        instance=value, data=custom_field, partial=True
 | 
			
		||||
                    )
 | 
			
		||||
                    serializer.is_valid(raise_exception=True)
 | 
			
		||||
                    serializer.save()
 | 
			
		||||
                else:
 | 
			
		||||
                    serializer = SiteCustomFieldSerializer(data=custom_field)
 | 
			
		||||
                    serializer.is_valid(raise_exception=True)
 | 
			
		||||
                    serializer.save()
 | 
			
		||||
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
        return Response("Site was edited!")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DeleteSite(APIView):
 | 
			
		||||
    def delete(self, request, pk, sitepk):
 | 
			
		||||
        from automation.tasks import generate_all_agent_checks_task
 | 
			
		||||
 | 
			
		||||
    def delete(self, request, pk):
 | 
			
		||||
        site = get_object_or_404(Site, pk=pk)
 | 
			
		||||
        if site.client.sites.count() == 1:
 | 
			
		||||
            return notify_error(f"A client must have at least 1 site.")
 | 
			
		||||
            return notify_error("A client must have at least 1 site.")
 | 
			
		||||
 | 
			
		||||
        agent_count = Agent.objects.filter(site=site).count()
 | 
			
		||||
        agents = Agent.objects.filter(site=site)
 | 
			
		||||
 | 
			
		||||
        if agent_count > 0:
 | 
			
		||||
        if not sitepk:
 | 
			
		||||
            return notify_error(
 | 
			
		||||
                f"Cannot delete {site.name} while {agent_count} agents exist in it. Move the agents to another site first."
 | 
			
		||||
                "There needs to be a site specified to move the agents to"
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        agent_site = get_object_or_404(Site, pk=sitepk)
 | 
			
		||||
 | 
			
		||||
        agents.update(site=agent_site)
 | 
			
		||||
 | 
			
		||||
        generate_all_agent_checks_task.delay("workstation", create_tasks=True)
 | 
			
		||||
        generate_all_agent_checks_task.delay("server", create_tasks=True)
 | 
			
		||||
 | 
			
		||||
        site.delete()
 | 
			
		||||
        return Response(f"{site.name} was deleted!")
 | 
			
		||||
 | 
			
		||||
@@ -176,6 +283,8 @@ class GenerateAgent(APIView):
 | 
			
		||||
    permission_classes = (AllowAny,)
 | 
			
		||||
 | 
			
		||||
    def get(self, request, uid):
 | 
			
		||||
        from tacticalrmm.utils import generate_winagent_exe
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            _ = uuid.UUID(uid, version=4)
 | 
			
		||||
        except ValueError:
 | 
			
		||||
@@ -183,99 +292,22 @@ class GenerateAgent(APIView):
 | 
			
		||||
 | 
			
		||||
        d = get_object_or_404(Deployment, uid=uid)
 | 
			
		||||
 | 
			
		||||
        go_bin = "/usr/local/rmmgo/go/bin/go"
 | 
			
		||||
 | 
			
		||||
        if not os.path.exists(go_bin):
 | 
			
		||||
            return notify_error("Missing golang")
 | 
			
		||||
 | 
			
		||||
        api = f"https://{request.get_host()}"
 | 
			
		||||
        inno = (
 | 
			
		||||
            f"winagent-v{settings.LATEST_AGENT_VER}.exe"
 | 
			
		||||
            if d.arch == "64"
 | 
			
		||||
            else f"winagent-v{settings.LATEST_AGENT_VER}-x86.exe"
 | 
			
		||||
        )
 | 
			
		||||
        download_url = settings.DL_64 if d.arch == "64" else settings.DL_32
 | 
			
		||||
 | 
			
		||||
        client = d.client.name.replace(" ", "").lower()
 | 
			
		||||
        site = d.site.name.replace(" ", "").lower()
 | 
			
		||||
        client = re.sub(r"([^a-zA-Z0-9]+)", "", client)
 | 
			
		||||
        site = re.sub(r"([^a-zA-Z0-9]+)", "", site)
 | 
			
		||||
 | 
			
		||||
        ext = ".exe" if d.arch == "64" else "-x86.exe"
 | 
			
		||||
 | 
			
		||||
        file_name = f"rmm-{client}-{site}-{d.mon_type}{ext}"
 | 
			
		||||
        exe = os.path.join(settings.EXE_DIR, file_name)
 | 
			
		||||
 | 
			
		||||
        if os.path.exists(exe):
 | 
			
		||||
            try:
 | 
			
		||||
                os.remove(exe)
 | 
			
		||||
            except:
 | 
			
		||||
                pass
 | 
			
		||||
 | 
			
		||||
        goarch = "amd64" if d.arch == "64" else "386"
 | 
			
		||||
        cmd = [
 | 
			
		||||
            "env",
 | 
			
		||||
            "GOOS=windows",
 | 
			
		||||
            f"GOARCH={goarch}",
 | 
			
		||||
            go_bin,
 | 
			
		||||
            "build",
 | 
			
		||||
            f"-ldflags=\"-s -w -X 'main.Inno={inno}'",
 | 
			
		||||
            f"-X 'main.Api={api}'",
 | 
			
		||||
            f"-X 'main.Client={d.client.pk}'",
 | 
			
		||||
            f"-X 'main.Site={d.site.pk}'",
 | 
			
		||||
            f"-X 'main.Atype={d.mon_type}'",
 | 
			
		||||
            f"-X 'main.Rdp={d.install_flags['rdp']}'",
 | 
			
		||||
            f"-X 'main.Ping={d.install_flags['ping']}'",
 | 
			
		||||
            f"-X 'main.Power={d.install_flags['power']}'",
 | 
			
		||||
            f"-X 'main.DownloadUrl={download_url}'",
 | 
			
		||||
            f"-X 'main.Token={d.token_key}'\"",
 | 
			
		||||
            "-o",
 | 
			
		||||
            exe,
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        gen = [
 | 
			
		||||
            "env",
 | 
			
		||||
            "GOOS=windows",
 | 
			
		||||
            f"GOARCH={goarch}",
 | 
			
		||||
            go_bin,
 | 
			
		||||
            "generate",
 | 
			
		||||
        ]
 | 
			
		||||
        try:
 | 
			
		||||
            r1 = subprocess.run(
 | 
			
		||||
                " ".join(gen),
 | 
			
		||||
                capture_output=True,
 | 
			
		||||
                shell=True,
 | 
			
		||||
                cwd=os.path.join(settings.BASE_DIR, "core/goinstaller"),
 | 
			
		||||
        return generate_winagent_exe(
 | 
			
		||||
            client=d.client.pk,
 | 
			
		||||
            site=d.site.pk,
 | 
			
		||||
            agent_type=d.mon_type,
 | 
			
		||||
            rdp=d.install_flags["rdp"],
 | 
			
		||||
            ping=d.install_flags["ping"],
 | 
			
		||||
            power=d.install_flags["power"],
 | 
			
		||||
            arch=d.arch,
 | 
			
		||||
            token=d.token_key,
 | 
			
		||||
            api=f"https://{request.get_host()}",
 | 
			
		||||
            file_name=file_name,
 | 
			
		||||
        )
 | 
			
		||||
        except:
 | 
			
		||||
            return notify_error("genfailed")
 | 
			
		||||
 | 
			
		||||
        if r1.returncode != 0:
 | 
			
		||||
            return notify_error("genfailed")
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            r = subprocess.run(
 | 
			
		||||
                " ".join(cmd),
 | 
			
		||||
                capture_output=True,
 | 
			
		||||
                shell=True,
 | 
			
		||||
                cwd=os.path.join(settings.BASE_DIR, "core/goinstaller"),
 | 
			
		||||
            )
 | 
			
		||||
        except:
 | 
			
		||||
            return notify_error("buildfailed")
 | 
			
		||||
 | 
			
		||||
        if r.returncode != 0:
 | 
			
		||||
            return notify_error("buildfailed")
 | 
			
		||||
 | 
			
		||||
        if settings.DEBUG:
 | 
			
		||||
            with open(exe, "rb") as f:
 | 
			
		||||
                response = HttpResponse(
 | 
			
		||||
                    f.read(),
 | 
			
		||||
                    content_type="application/vnd.microsoft.portable-executable",
 | 
			
		||||
                )
 | 
			
		||||
                response["Content-Disposition"] = f"inline; filename={file_name}"
 | 
			
		||||
                return response
 | 
			
		||||
        else:
 | 
			
		||||
            response = HttpResponse()
 | 
			
		||||
            response["Content-Disposition"] = f"attachment; filename={file_name}"
 | 
			
		||||
            response["X-Accel-Redirect"] = f"/private/exe/{file_name}"
 | 
			
		||||
            return response
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
from django.contrib import admin
 | 
			
		||||
 | 
			
		||||
from .models import CoreSettings
 | 
			
		||||
from .models import CoreSettings, CustomField
 | 
			
		||||
 | 
			
		||||
admin.site.register(CoreSettings)
 | 
			
		||||
admin.site.register(CustomField)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										79
									
								
								api/tacticalrmm/core/consumers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								api/tacticalrmm/core/consumers.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,79 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
 | 
			
		||||
from channels.db import database_sync_to_async
 | 
			
		||||
from channels.generic.websocket import AsyncJsonWebsocketConsumer
 | 
			
		||||
from django.contrib.auth.models import AnonymousUser
 | 
			
		||||
 | 
			
		||||
from agents.models import Agent
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DashInfo(AsyncJsonWebsocketConsumer):
 | 
			
		||||
    async def connect(self):
 | 
			
		||||
 | 
			
		||||
        self.user = self.scope["user"]
 | 
			
		||||
 | 
			
		||||
        if isinstance(self.user, AnonymousUser):
 | 
			
		||||
            await self.close()
 | 
			
		||||
 | 
			
		||||
        await self.accept()
 | 
			
		||||
        self.connected = True
 | 
			
		||||
        self.dash_info = asyncio.create_task(self.send_dash_info())
 | 
			
		||||
 | 
			
		||||
    async def disconnect(self, close_code):
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            self.dash_info.cancel()
 | 
			
		||||
        except:
 | 
			
		||||
            pass
 | 
			
		||||
 | 
			
		||||
        self.connected = False
 | 
			
		||||
        await self.close()
 | 
			
		||||
 | 
			
		||||
    async def receive(self, json_data=None):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    @database_sync_to_async
 | 
			
		||||
    def get_dashboard_info(self):
 | 
			
		||||
        server_offline_count = len(
 | 
			
		||||
            [
 | 
			
		||||
                agent
 | 
			
		||||
                for agent in Agent.objects.filter(monitoring_type="server").only(
 | 
			
		||||
                    "pk",
 | 
			
		||||
                    "last_seen",
 | 
			
		||||
                    "overdue_time",
 | 
			
		||||
                    "offline_time",
 | 
			
		||||
                )
 | 
			
		||||
                if not agent.status == "online"
 | 
			
		||||
            ]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        workstation_offline_count = len(
 | 
			
		||||
            [
 | 
			
		||||
                agent
 | 
			
		||||
                for agent in Agent.objects.filter(monitoring_type="workstation").only(
 | 
			
		||||
                    "pk",
 | 
			
		||||
                    "last_seen",
 | 
			
		||||
                    "overdue_time",
 | 
			
		||||
                    "offline_time",
 | 
			
		||||
                )
 | 
			
		||||
                if not agent.status == "online"
 | 
			
		||||
            ]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        ret = {
 | 
			
		||||
            "total_server_offline_count": server_offline_count,
 | 
			
		||||
            "total_workstation_offline_count": workstation_offline_count,
 | 
			
		||||
            "total_server_count": Agent.objects.filter(
 | 
			
		||||
                monitoring_type="server"
 | 
			
		||||
            ).count(),
 | 
			
		||||
            "total_workstation_count": Agent.objects.filter(
 | 
			
		||||
                monitoring_type="workstation"
 | 
			
		||||
            ).count(),
 | 
			
		||||
        }
 | 
			
		||||
        return ret
 | 
			
		||||
 | 
			
		||||
    async def send_dash_info(self):
 | 
			
		||||
        while self.connected:
 | 
			
		||||
            c = await self.get_dashboard_info()
 | 
			
		||||
            await self.send_json(c)
 | 
			
		||||
            await asyncio.sleep(30)
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							@@ -1,17 +0,0 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
 | 
			
		||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
 | 
			
		||||
  <assemblyIdentity
 | 
			
		||||
    type="win32"
 | 
			
		||||
    name="TacticalRMMInstaller"
 | 
			
		||||
    version="1.0.0.0"
 | 
			
		||||
    processorArchitecture="*"/>
 | 
			
		||||
 <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
 | 
			
		||||
   <security>
 | 
			
		||||
     <requestedPrivileges>
 | 
			
		||||
       <requestedExecutionLevel
 | 
			
		||||
         level="requireAdministrator"
 | 
			
		||||
         uiAccess="false"/>
 | 
			
		||||
       </requestedPrivileges>
 | 
			
		||||
   </security>
 | 
			
		||||
 </trustInfo>
 | 
			
		||||
</assembly>
 | 
			
		||||
@@ -1,178 +0,0 @@
 | 
			
		||||
//go:generate goversioninfo -icon=onit.ico -manifest=goversioninfo.exe.manifest -gofile=versioninfo.go
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bufio"
 | 
			
		||||
	"flag"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/exec"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	Inno        string
 | 
			
		||||
	Api         string
 | 
			
		||||
	Client      string
 | 
			
		||||
	Site        string
 | 
			
		||||
	Atype       string
 | 
			
		||||
	Power       string
 | 
			
		||||
	Rdp         string
 | 
			
		||||
	Ping        string
 | 
			
		||||
	Token       string
 | 
			
		||||
	DownloadUrl string
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func downloadAgent(filepath string) (err error) {
 | 
			
		||||
 | 
			
		||||
	out, err := os.Create(filepath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	defer out.Close()
 | 
			
		||||
 | 
			
		||||
	resp, err := http.Get(DownloadUrl)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	defer resp.Body.Close()
 | 
			
		||||
 | 
			
		||||
	if resp.StatusCode != http.StatusOK {
 | 
			
		||||
		return fmt.Errorf("Bad response: %s", resp.Status)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_, err = io.Copy(out, resp.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
 | 
			
		||||
	debugLog := flag.String("log", "", "Verbose output")
 | 
			
		||||
	localMesh := flag.String("local-mesh", "", "Use local mesh agent")
 | 
			
		||||
	silent := flag.Bool("silent", false, "Do not popup any message boxes during installation")
 | 
			
		||||
	cert := flag.String("cert", "", "Path to ca.pem")
 | 
			
		||||
	timeout := flag.String("timeout", "", "Timeout for subprocess calls")
 | 
			
		||||
	flag.Parse()
 | 
			
		||||
 | 
			
		||||
	var debug bool = false
 | 
			
		||||
 | 
			
		||||
	if strings.TrimSpace(strings.ToLower(*debugLog)) == "debug" {
 | 
			
		||||
		debug = true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	agentBinary := filepath.Join(os.Getenv("windir"), "Temp", Inno)
 | 
			
		||||
	tacrmm := filepath.Join(os.Getenv("PROGRAMFILES"), "TacticalAgent", "tacticalrmm.exe")
 | 
			
		||||
 | 
			
		||||
	cmdArgs := []string{
 | 
			
		||||
		"-m", "install", "--api", Api, "--client-id",
 | 
			
		||||
		Client, "--site-id", Site, "--agent-type", Atype,
 | 
			
		||||
		"--auth", Token,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if debug {
 | 
			
		||||
		cmdArgs = append(cmdArgs, "-log", "debug")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if *silent {
 | 
			
		||||
		cmdArgs = append(cmdArgs, "-silent")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(strings.TrimSpace(*localMesh)) != 0 {
 | 
			
		||||
		cmdArgs = append(cmdArgs, "-local-mesh", *localMesh)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(strings.TrimSpace(*cert)) != 0 {
 | 
			
		||||
		cmdArgs = append(cmdArgs, "-cert", *cert)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(strings.TrimSpace(*timeout)) != 0 {
 | 
			
		||||
		cmdArgs = append(cmdArgs, "-timeout", *timeout)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if Rdp == "1" {
 | 
			
		||||
		cmdArgs = append(cmdArgs, "-rdp")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if Ping == "1" {
 | 
			
		||||
		cmdArgs = append(cmdArgs, "-ping")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if Power == "1" {
 | 
			
		||||
		cmdArgs = append(cmdArgs, "-power")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if debug {
 | 
			
		||||
		fmt.Println("Installer:", agentBinary)
 | 
			
		||||
		fmt.Println("Tactical Agent:", tacrmm)
 | 
			
		||||
		fmt.Println("Download URL:", DownloadUrl)
 | 
			
		||||
		fmt.Println("Install command:", tacrmm, strings.Join(cmdArgs, " "))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fmt.Println("Downloading agent...")
 | 
			
		||||
	dl := downloadAgent(agentBinary)
 | 
			
		||||
	if dl != nil {
 | 
			
		||||
		fmt.Println("ERROR: unable to download agent from", DownloadUrl)
 | 
			
		||||
		fmt.Println(dl)
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
	defer os.Remove(agentBinary)
 | 
			
		||||
 | 
			
		||||
	fmt.Println("Extracting files...")
 | 
			
		||||
	winagentCmd := exec.Command(agentBinary, "/VERYSILENT", "/SUPPRESSMSGBOXES")
 | 
			
		||||
	err := winagentCmd.Run()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Println(err)
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	time.Sleep(5 * time.Second)
 | 
			
		||||
 | 
			
		||||
	fmt.Println("Installation starting.")
 | 
			
		||||
	cmd := exec.Command(tacrmm, cmdArgs...)
 | 
			
		||||
 | 
			
		||||
	cmdReader, err := cmd.StdoutPipe()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintln(os.Stderr, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cmdErrReader, oerr := cmd.StderrPipe()
 | 
			
		||||
	if oerr != nil {
 | 
			
		||||
		fmt.Fprintln(os.Stderr, oerr)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	scanner := bufio.NewScanner(cmdReader)
 | 
			
		||||
	escanner := bufio.NewScanner(cmdErrReader)
 | 
			
		||||
	go func() {
 | 
			
		||||
		for scanner.Scan() {
 | 
			
		||||
			fmt.Println(scanner.Text())
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	go func() {
 | 
			
		||||
		for escanner.Scan() {
 | 
			
		||||
			fmt.Println(escanner.Text())
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	err = cmd.Start()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintln(os.Stderr, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = cmd.Wait()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintln(os.Stderr, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 48 KiB  | 
@@ -1,43 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
    "FixedFileInfo": {
 | 
			
		||||
        "FileVersion": {
 | 
			
		||||
            "Major": 1,
 | 
			
		||||
            "Minor": 0,
 | 
			
		||||
            "Patch": 0,
 | 
			
		||||
            "Build": 0
 | 
			
		||||
        },
 | 
			
		||||
        "ProductVersion": {
 | 
			
		||||
            "Major": 1,
 | 
			
		||||
            "Minor": 0,
 | 
			
		||||
            "Patch": 0,
 | 
			
		||||
            "Build": 0
 | 
			
		||||
        },
 | 
			
		||||
        "FileFlagsMask": "3f",
 | 
			
		||||
        "FileFlags ": "00",
 | 
			
		||||
        "FileOS": "040004",
 | 
			
		||||
        "FileType": "01",
 | 
			
		||||
        "FileSubType": "00"
 | 
			
		||||
    },
 | 
			
		||||
    "StringFileInfo": {
 | 
			
		||||
        "Comments": "",
 | 
			
		||||
        "CompanyName": "Tactical Techs",
 | 
			
		||||
        "FileDescription": "Tactical RMM Installer",
 | 
			
		||||
        "FileVersion": "v1.0.0.0",
 | 
			
		||||
        "InternalName": "rmm.exe",
 | 
			
		||||
        "LegalCopyright": "Copyright (c) 2020 Tactical Techs",
 | 
			
		||||
        "LegalTrademarks": "",
 | 
			
		||||
        "OriginalFilename": "installer.go",
 | 
			
		||||
        "PrivateBuild": "",
 | 
			
		||||
        "ProductName": "Tactical RMM Installer",
 | 
			
		||||
        "ProductVersion": "v1.0.0.0",
 | 
			
		||||
        "SpecialBuild": ""
 | 
			
		||||
    },
 | 
			
		||||
    "VarFileInfo": {
 | 
			
		||||
        "Translation": {
 | 
			
		||||
            "LangID": "0409",
 | 
			
		||||
            "CharsetID": "04B0"
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    "IconPath": "",
 | 
			
		||||
    "ManifestPath": ""
 | 
			
		||||
}
 | 
			
		||||
@@ -10,6 +10,8 @@ $ping = pingchange
 | 
			
		||||
$auth = '"tokenchange"'
 | 
			
		||||
$downloadlink = 'downloadchange'
 | 
			
		||||
 | 
			
		||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
 | 
			
		||||
 | 
			
		||||
$serviceName = 'tacticalagent'
 | 
			
		||||
If (Get-Service $serviceName -ErrorAction SilentlyContinue) {
 | 
			
		||||
    write-host ('Tactical RMM Is Already Installed')
 | 
			
		||||
@@ -33,11 +35,20 @@ If (Get-Service $serviceName -ErrorAction SilentlyContinue) {
 | 
			
		||||
 | 
			
		||||
    Try
 | 
			
		||||
    {
 | 
			
		||||
        $DefenderStatus = Get-MpComputerStatus | select  AntivirusEnabled
 | 
			
		||||
        if ($DefenderStatus -match "True") {
 | 
			
		||||
            Add-MpPreference -ExclusionPath 'C:\Program Files\TacticalAgent\*'
 | 
			
		||||
            Add-MpPreference -ExclusionPath 'C:\Windows\Temp\winagent-v*.exe'
 | 
			
		||||
            Add-MpPreference -ExclusionPath 'C:\Program Files\Mesh Agent\*'
 | 
			
		||||
            Add-MpPreference -ExclusionPath 'C:\Windows\Temp\trmm*\*'
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    Catch {
 | 
			
		||||
        # pass
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    Try
 | 
			
		||||
    {  
 | 
			
		||||
        Invoke-WebRequest -Uri $downloadlink -OutFile $OutPath\$output
 | 
			
		||||
        Start-Process -FilePath $OutPath\$output -ArgumentList ('/VERYSILENT /SUPPRESSMSGBOXES') -Wait
 | 
			
		||||
        write-host ('Extracting...')
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										27
									
								
								api/tacticalrmm/core/migrations/0014_customfield.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								api/tacticalrmm/core/migrations/0014_customfield.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
			
		||||
# Generated by Django 3.1.7 on 2021-03-17 14:45
 | 
			
		||||
 | 
			
		||||
import django.contrib.postgres.fields
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('core', '0013_coresettings_alert_template'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='CustomField',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('order', models.PositiveIntegerField()),
 | 
			
		||||
                ('model', models.CharField(choices=[('client', 'Client'), ('site', 'Site'), ('agent', 'Agent')], max_length=25)),
 | 
			
		||||
                ('type', models.CharField(choices=[('text', 'Text'), ('number', 'Number'), ('single', 'Single'), ('multiple', 'Multiple'), ('checkbox', 'Checkbox'), ('datetime', 'DateTime')], default='text', max_length=25)),
 | 
			
		||||
                ('options', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=255, null=True), blank=True, default=list, null=True, size=None)),
 | 
			
		||||
                ('name', models.TextField(blank=True, null=True)),
 | 
			
		||||
                ('default_value', models.TextField(blank=True, null=True)),
 | 
			
		||||
                ('required', models.BooleanField(blank=True, default=False)),
 | 
			
		||||
            ],
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										18
									
								
								api/tacticalrmm/core/migrations/0015_auto_20210318_2034.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								api/tacticalrmm/core/migrations/0015_auto_20210318_2034.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
# Generated by Django 3.1.7 on 2021-03-18 20:34
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('core', '0014_customfield'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='customfield',
 | 
			
		||||
            name='order',
 | 
			
		||||
            field=models.PositiveIntegerField(default=0),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										17
									
								
								api/tacticalrmm/core/migrations/0016_auto_20210319_1536.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								api/tacticalrmm/core/migrations/0016_auto_20210319_1536.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
# Generated by Django 3.1.7 on 2021-03-19 15:36
 | 
			
		||||
 | 
			
		||||
from django.db import migrations
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('core', '0015_auto_20210318_2034'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterUniqueTogether(
 | 
			
		||||
            name='customfield',
 | 
			
		||||
            unique_together={('model', 'name')},
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										24
									
								
								api/tacticalrmm/core/migrations/0017_auto_20210329_1050.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								api/tacticalrmm/core/migrations/0017_auto_20210329_1050.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
			
		||||
# Generated by Django 3.1.7 on 2021-03-29 10:50
 | 
			
		||||
 | 
			
		||||
import django.contrib.postgres.fields
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('core', '0016_auto_20210319_1536'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='customfield',
 | 
			
		||||
            name='checkbox_value',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='customfield',
 | 
			
		||||
            name='default_values_multiple',
 | 
			
		||||
            field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=255, null=True), blank=True, default=list, null=True, size=None),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										23
									
								
								api/tacticalrmm/core/migrations/0018_auto_20210329_1709.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								api/tacticalrmm/core/migrations/0018_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 = [
 | 
			
		||||
        ('core', '0017_auto_20210329_1050'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.RenameField(
 | 
			
		||||
            model_name='customfield',
 | 
			
		||||
            old_name='checkbox_value',
 | 
			
		||||
            new_name='default_value_bool',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RenameField(
 | 
			
		||||
            model_name='customfield',
 | 
			
		||||
            old_name='default_value',
 | 
			
		||||
            new_name='default_value_string',
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -78,6 +78,7 @@ class CoreSettings(BaseAuditModel):
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def save(self, *args, **kwargs):
 | 
			
		||||
        from alerts.tasks import cache_agents_alert_template
 | 
			
		||||
        from automation.tasks import generate_all_agent_checks_task
 | 
			
		||||
 | 
			
		||||
        if not self.pk and CoreSettings.objects.exists():
 | 
			
		||||
@@ -105,6 +106,9 @@ class CoreSettings(BaseAuditModel):
 | 
			
		||||
                mon_type="workstation", create_tasks=True
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        if old_settings and old_settings.alert_template != self.alert_template:
 | 
			
		||||
            cache_agents_alert_template.delay()
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return "Global Site Settings"
 | 
			
		||||
 | 
			
		||||
@@ -212,3 +216,53 @@ class CoreSettings(BaseAuditModel):
 | 
			
		||||
        from .serializers import CoreSerializer
 | 
			
		||||
 | 
			
		||||
        return CoreSerializer(core).data
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FIELD_TYPE_CHOICES = (
 | 
			
		||||
    ("text", "Text"),
 | 
			
		||||
    ("number", "Number"),
 | 
			
		||||
    ("single", "Single"),
 | 
			
		||||
    ("multiple", "Multiple"),
 | 
			
		||||
    ("checkbox", "Checkbox"),
 | 
			
		||||
    ("datetime", "DateTime"),
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
MODEL_CHOICES = (("client", "Client"), ("site", "Site"), ("agent", "Agent"))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CustomField(models.Model):
 | 
			
		||||
 | 
			
		||||
    order = models.PositiveIntegerField(default=0)
 | 
			
		||||
    model = models.CharField(max_length=25, choices=MODEL_CHOICES)
 | 
			
		||||
    type = models.CharField(max_length=25, choices=FIELD_TYPE_CHOICES, default="text")
 | 
			
		||||
    options = ArrayField(
 | 
			
		||||
        models.CharField(max_length=255, null=True, blank=True),
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        default=list,
 | 
			
		||||
    )
 | 
			
		||||
    name = models.TextField(null=True, blank=True)
 | 
			
		||||
    required = models.BooleanField(blank=True, default=False)
 | 
			
		||||
    default_value_string = models.TextField(null=True, blank=True)
 | 
			
		||||
    default_value_bool = models.BooleanField(default=False)
 | 
			
		||||
    default_values_multiple = ArrayField(
 | 
			
		||||
        models.CharField(max_length=255, null=True, blank=True),
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        default=list,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        unique_together = (("model", "name"),)
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.name
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def default_value(self):
 | 
			
		||||
        if self.type == "multiple":
 | 
			
		||||
            return self.default_values_multiple
 | 
			
		||||
        elif self.type == "checkbox":
 | 
			
		||||
            return self.default_value_bool
 | 
			
		||||
        else:
 | 
			
		||||
            return self.default_value_string
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import pytz
 | 
			
		||||
from rest_framework import serializers
 | 
			
		||||
 | 
			
		||||
from .models import CoreSettings
 | 
			
		||||
from .models import CoreSettings, CustomField
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CoreSettingsSerializer(serializers.ModelSerializer):
 | 
			
		||||
@@ -21,3 +21,9 @@ class CoreSerializer(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = CoreSettings
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CustomFieldSerializer(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = CustomField
 | 
			
		||||
        fields = "__all__"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,39 @@
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
 | 
			
		||||
from model_bakery import baker, seq
 | 
			
		||||
from channels.db import database_sync_to_async
 | 
			
		||||
from channels.testing import WebsocketCommunicator
 | 
			
		||||
from model_bakery import baker
 | 
			
		||||
 | 
			
		||||
from core.models import CoreSettings
 | 
			
		||||
from core.tasks import core_maintenance_tasks
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
 | 
			
		||||
from .consumers import DashInfo
 | 
			
		||||
from .models import CoreSettings, CustomField
 | 
			
		||||
from .serializers import CustomFieldSerializer
 | 
			
		||||
from .tasks import core_maintenance_tasks
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestConsumers(TacticalTestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.setup_coresettings()
 | 
			
		||||
        self.authenticate()
 | 
			
		||||
 | 
			
		||||
    @database_sync_to_async
 | 
			
		||||
    def get_token(self):
 | 
			
		||||
        from rest_framework.authtoken.models import Token
 | 
			
		||||
 | 
			
		||||
        token = Token.objects.create(user=self.john)
 | 
			
		||||
        return token.key
 | 
			
		||||
 | 
			
		||||
    async def test_dash_info(self):
 | 
			
		||||
        key = self.get_token()
 | 
			
		||||
        communicator = WebsocketCommunicator(
 | 
			
		||||
            DashInfo.as_asgi(), f"/ws/dashinfo/?access_token={key}"
 | 
			
		||||
        )
 | 
			
		||||
        communicator.scope["user"] = self.john
 | 
			
		||||
        connected, _ = await communicator.connect()
 | 
			
		||||
        assert connected
 | 
			
		||||
        await communicator.disconnect()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestCoreTasks(TacticalTestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
@@ -42,7 +70,7 @@ class TestCoreTasks(TacticalTestCase):
 | 
			
		||||
        url = "/core/editsettings/"
 | 
			
		||||
 | 
			
		||||
        # setup
 | 
			
		||||
        policies = baker.make("Policy", _quantity=2)
 | 
			
		||||
        policies = baker.make("automation.Policy", _quantity=2)
 | 
			
		||||
        # test normal request
 | 
			
		||||
        data = {
 | 
			
		||||
            "smtp_from_email": "newexample@example.com",
 | 
			
		||||
@@ -59,14 +87,14 @@ class TestCoreTasks(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        # test adding policy
 | 
			
		||||
        data = {
 | 
			
		||||
            "workstation_policy": policies[0].id,
 | 
			
		||||
            "server_policy": policies[1].id,
 | 
			
		||||
            "workstation_policy": policies[0].id,  # type: ignore
 | 
			
		||||
            "server_policy": policies[1].id,  # type: ignore
 | 
			
		||||
        }
 | 
			
		||||
        r = self.client.patch(url, data)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(CoreSettings.objects.first().server_policy.id, policies[1].id)
 | 
			
		||||
        self.assertEqual(CoreSettings.objects.first().server_policy.id, policies[1].id)  # type: ignore
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            CoreSettings.objects.first().workstation_policy.id, policies[0].id
 | 
			
		||||
            CoreSettings.objects.first().workstation_policy.id, policies[0].id  # type: ignore
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(generate_all_agent_checks_task.call_count, 2)
 | 
			
		||||
@@ -116,7 +144,7 @@ class TestCoreTasks(TacticalTestCase):
 | 
			
		||||
        # test prune db with tables
 | 
			
		||||
        data = {
 | 
			
		||||
            "action": "prune_db",
 | 
			
		||||
            "prune_tables": ["audit_logs", "agent_outages", "pending_actions"],
 | 
			
		||||
            "prune_tables": ["audit_logs", "alerts", "pending_actions"],
 | 
			
		||||
        }
 | 
			
		||||
        r = self.client.post(url, data)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
@@ -128,3 +156,97 @@ class TestCoreTasks(TacticalTestCase):
 | 
			
		||||
        remove_orphaned_win_tasks.assert_called()
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
    def test_get_custom_fields(self):
 | 
			
		||||
        url = "/core/customfields/"
 | 
			
		||||
 | 
			
		||||
        # setup
 | 
			
		||||
        custom_fields = baker.make("core.CustomField", _quantity=2)
 | 
			
		||||
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        serializer = CustomFieldSerializer(custom_fields, many=True)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(len(r.data), 2)  # type: ignore
 | 
			
		||||
        self.assertEqual(r.data, serializer.data)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    def test_get_custom_fields_by_model(self):
 | 
			
		||||
        url = "/core/customfields/"
 | 
			
		||||
 | 
			
		||||
        # setup
 | 
			
		||||
        custom_fields = baker.make("core.CustomField", model="agent", _quantity=5)
 | 
			
		||||
        baker.make("core.CustomField", model="client", _quantity=5)
 | 
			
		||||
 | 
			
		||||
        # will error if request invalid
 | 
			
		||||
        r = self.client.patch(url, {"invalid": ""})
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
 | 
			
		||||
        data = {"model": "agent"}
 | 
			
		||||
        r = self.client.patch(url, data)
 | 
			
		||||
        serializer = CustomFieldSerializer(custom_fields, many=True)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(len(r.data), 5)  # type: ignore
 | 
			
		||||
        self.assertEqual(r.data, serializer.data)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("patch", url)
 | 
			
		||||
 | 
			
		||||
    def test_add_custom_field(self):
 | 
			
		||||
        url = "/core/customfields/"
 | 
			
		||||
 | 
			
		||||
        data = {"model": "client", "type": "text", "name": "Field"}
 | 
			
		||||
        r = self.client.patch(url, data)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
    def test_get_custom_field(self):
 | 
			
		||||
        # setup
 | 
			
		||||
        custom_field = baker.make("core.CustomField")
 | 
			
		||||
 | 
			
		||||
        # test not found
 | 
			
		||||
        r = self.client.get("/core/customfields/500/")
 | 
			
		||||
        self.assertEqual(r.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        url = f"/core/customfields/{custom_field.id}/"  # type: ignore
 | 
			
		||||
        r = self.client.get(url)
 | 
			
		||||
        serializer = CustomFieldSerializer(custom_field)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(r.data, serializer.data)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
 | 
			
		||||
    def test_update_custom_field(self):
 | 
			
		||||
        # setup
 | 
			
		||||
        custom_field = baker.make("core.CustomField")
 | 
			
		||||
 | 
			
		||||
        # test not found
 | 
			
		||||
        r = self.client.put("/core/customfields/500/")
 | 
			
		||||
        self.assertEqual(r.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        url = f"/core/customfields/{custom_field.id}/"  # type: ignore
 | 
			
		||||
        data = {"type": "single", "options": ["ione", "two", "three"]}
 | 
			
		||||
        r = self.client.put(url, data)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        new_field = CustomField.objects.get(pk=custom_field.id)  # type: ignore
 | 
			
		||||
        self.assertEqual(new_field.type, data["type"])
 | 
			
		||||
        self.assertEqual(new_field.options, data["options"])
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("put", url)
 | 
			
		||||
 | 
			
		||||
    def test_delete_custom_field(self):
 | 
			
		||||
        # setup
 | 
			
		||||
        custom_field = baker.make("core.CustomField")
 | 
			
		||||
 | 
			
		||||
        # test not found
 | 
			
		||||
        r = self.client.delete("/core/customfields/500/")
 | 
			
		||||
        self.assertEqual(r.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        url = f"/core/customfields/{custom_field.id}/"  # type: ignore
 | 
			
		||||
        r = self.client.delete(url)
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        self.assertFalse(CustomField.objects.filter(pk=custom_field.id).exists())  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("delete", url)
 | 
			
		||||
 
 | 
			
		||||
@@ -10,4 +10,6 @@ urlpatterns = [
 | 
			
		||||
    path("emailtest/", views.email_test),
 | 
			
		||||
    path("dashinfo/", views.dashboard_info),
 | 
			
		||||
    path("servermaintenance/", views.server_maintenance),
 | 
			
		||||
    path("customfields/", views.GetAddCustomFields.as_view()),
 | 
			
		||||
    path("customfields/<int:pk>/", views.GetUpdateDeleteCustomFields.as_view()),
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
import os
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from rest_framework import status
 | 
			
		||||
from rest_framework.decorators import api_view
 | 
			
		||||
from rest_framework.exceptions import ParseError
 | 
			
		||||
@@ -10,8 +11,8 @@ from rest_framework.views import APIView
 | 
			
		||||
 | 
			
		||||
from tacticalrmm.utils import notify_error
 | 
			
		||||
 | 
			
		||||
from .models import CoreSettings
 | 
			
		||||
from .serializers import CoreSettingsSerializer
 | 
			
		||||
from .models import CoreSettings, CustomField
 | 
			
		||||
from .serializers import CoreSettingsSerializer, CustomFieldSerializer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UploadMeshAgent(APIView):
 | 
			
		||||
@@ -63,6 +64,9 @@ def dashboard_info(request):
 | 
			
		||||
            "show_community_scripts": request.user.show_community_scripts,
 | 
			
		||||
            "dbl_click_action": request.user.agent_dblclick_action,
 | 
			
		||||
            "default_agent_tbl_tab": request.user.default_agent_tbl_tab,
 | 
			
		||||
            "client_tree_sort": request.user.client_tree_sort,
 | 
			
		||||
            "client_tree_splitter": request.user.client_tree_splitter,
 | 
			
		||||
            "loading_bar_color": request.user.loading_bar_color,
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
@@ -122,6 +126,56 @@ def server_maintenance(request):
 | 
			
		||||
            records_count += pendingactions.count()
 | 
			
		||||
            pendingactions.delete()
 | 
			
		||||
 | 
			
		||||
        if "alerts" in tables:
 | 
			
		||||
            from alerts.models import Alert
 | 
			
		||||
 | 
			
		||||
            alerts = Alert.objects.all()
 | 
			
		||||
            records_count += alerts.count()
 | 
			
		||||
            alerts.delete()
 | 
			
		||||
 | 
			
		||||
        return Response(f"{records_count} records were pruned from the database")
 | 
			
		||||
 | 
			
		||||
    return notify_error("The data is incorrect")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetAddCustomFields(APIView):
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
        fields = CustomField.objects.all()
 | 
			
		||||
        return Response(CustomFieldSerializer(fields, many=True).data)
 | 
			
		||||
 | 
			
		||||
    def patch(self, request):
 | 
			
		||||
        if "model" in request.data.keys():
 | 
			
		||||
            fields = CustomField.objects.filter(model=request.data["model"])
 | 
			
		||||
            return Response(CustomFieldSerializer(fields, many=True).data)
 | 
			
		||||
        else:
 | 
			
		||||
            return notify_error("The request was invalid")
 | 
			
		||||
 | 
			
		||||
    def post(self, request):
 | 
			
		||||
        serializer = CustomFieldSerializer(data=request.data, partial=True)
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        serializer.save()
 | 
			
		||||
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GetUpdateDeleteCustomFields(APIView):
 | 
			
		||||
    def get(self, request, pk):
 | 
			
		||||
        custom_field = get_object_or_404(CustomField, pk=pk)
 | 
			
		||||
 | 
			
		||||
        return Response(CustomFieldSerializer(custom_field).data)
 | 
			
		||||
 | 
			
		||||
    def put(self, request, pk):
 | 
			
		||||
        custom_field = get_object_or_404(CustomField, pk=pk)
 | 
			
		||||
 | 
			
		||||
        serializer = CustomFieldSerializer(
 | 
			
		||||
            instance=custom_field, data=request.data, partial=True
 | 
			
		||||
        )
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        serializer.save()
 | 
			
		||||
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 | 
			
		||||
    def delete(self, request, pk):
 | 
			
		||||
        get_object_or_404(CustomField, pk=pk).delete()
 | 
			
		||||
 | 
			
		||||
        return Response("ok")
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18
									
								
								api/tacticalrmm/logs/migrations/0012_auto_20210228_0943.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								api/tacticalrmm/logs/migrations/0012_auto_20210228_0943.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
# Generated by Django 3.1.7 on 2021-02-28 09:43
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('logs', '0011_auto_20201119_0854'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='pendingaction',
 | 
			
		||||
            name='action_type',
 | 
			
		||||
            field=models.CharField(blank=True, choices=[('schedreboot', 'Scheduled Reboot'), ('taskaction', 'Scheduled Task Action'), ('agentupdate', 'Agent Update'), ('chocoinstall', 'Chocolatey Software Install')], max_length=255, null=True),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -9,6 +9,7 @@ ACTION_TYPE_CHOICES = [
 | 
			
		||||
    ("schedreboot", "Scheduled Reboot"),
 | 
			
		||||
    ("taskaction", "Scheduled Task Action"),
 | 
			
		||||
    ("agentupdate", "Agent Update"),
 | 
			
		||||
    ("chocoinstall", "Chocolatey Software Install"),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
AUDIT_ACTION_TYPE_CHOICES = [
 | 
			
		||||
@@ -249,9 +250,12 @@ class PendingAction(models.Model):
 | 
			
		||||
        if self.action_type == "schedreboot":
 | 
			
		||||
            obj = dt.datetime.strptime(self.details["time"], "%Y-%m-%d %H:%M:%S")
 | 
			
		||||
            return dt.datetime.strftime(obj, "%B %d, %Y at %I:%M %p")
 | 
			
		||||
 | 
			
		||||
        elif self.action_type == "taskaction" or self.action_type == "agentupdate":
 | 
			
		||||
        elif self.action_type == "taskaction":
 | 
			
		||||
            return "Next agent check-in"
 | 
			
		||||
        elif self.action_type == "agentupdate":
 | 
			
		||||
            return "Next update cycle"
 | 
			
		||||
        elif self.action_type == "chocoinstall":
 | 
			
		||||
            return "ASAP"
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def description(self):
 | 
			
		||||
@@ -261,6 +265,9 @@ class PendingAction(models.Model):
 | 
			
		||||
        elif self.action_type == "agentupdate":
 | 
			
		||||
            return f"Agent update to {self.details['version']}"
 | 
			
		||||
 | 
			
		||||
        elif self.action_type == "chocoinstall":
 | 
			
		||||
            return f"{self.details['name']} software install"
 | 
			
		||||
 | 
			
		||||
        elif self.action_type == "taskaction":
 | 
			
		||||
            if self.details["action"] == "taskdelete":
 | 
			
		||||
                return "Device pending task deletion"
 | 
			
		||||
 
 | 
			
		||||
@@ -3,10 +3,9 @@ from unittest.mock import patch
 | 
			
		||||
 | 
			
		||||
from model_bakery import baker, seq
 | 
			
		||||
 | 
			
		||||
from logs.models import PendingAction
 | 
			
		||||
from tacticalrmm.test import TacticalTestCase
 | 
			
		||||
 | 
			
		||||
from .serializers import PendingActionSerializer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestAuditViews(TacticalTestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
@@ -177,63 +176,97 @@ class TestAuditViews(TacticalTestCase):
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("post", url)
 | 
			
		||||
 | 
			
		||||
    def test_agent_pending_actions(self):
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        pending_actions = baker.make(
 | 
			
		||||
    def test_get_pending_actions(self):
 | 
			
		||||
        url = "/logs/pendingactions/"
 | 
			
		||||
        agent1 = baker.make_recipe("agents.online_agent")
 | 
			
		||||
        agent2 = baker.make_recipe("agents.online_agent")
 | 
			
		||||
 | 
			
		||||
        baker.make(
 | 
			
		||||
            "logs.PendingAction",
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            _quantity=6,
 | 
			
		||||
            agent=agent1,
 | 
			
		||||
            action_type="chocoinstall",
 | 
			
		||||
            details={"name": "googlechrome", "output": None, "installed": False},
 | 
			
		||||
            _quantity=12,
 | 
			
		||||
        )
 | 
			
		||||
        baker.make(
 | 
			
		||||
            "logs.PendingAction",
 | 
			
		||||
            agent=agent2,
 | 
			
		||||
            action_type="chocoinstall",
 | 
			
		||||
            status="completed",
 | 
			
		||||
            details={"name": "adobereader", "output": None, "installed": False},
 | 
			
		||||
            _quantity=14,
 | 
			
		||||
        )
 | 
			
		||||
        url = f"/logs/{agent.pk}/pendingactions/"
 | 
			
		||||
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
        serializer = PendingActionSerializer(pending_actions, many=True)
 | 
			
		||||
        data = {"showCompleted": False}
 | 
			
		||||
        r = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(len(r.data["actions"]), 12)  # type: ignore
 | 
			
		||||
        self.assertEqual(r.data["completed_count"], 14)  # type: ignore
 | 
			
		||||
        self.assertEqual(r.data["total"], 26)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(len(resp.data), 6)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)
 | 
			
		||||
        PendingAction.objects.filter(action_type="chocoinstall").update(
 | 
			
		||||
            status="completed"
 | 
			
		||||
        )
 | 
			
		||||
        data = {"showCompleted": True}
 | 
			
		||||
        r = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(len(r.data["actions"]), 26)  # type: ignore
 | 
			
		||||
        self.assertEqual(r.data["completed_count"], 26)  # type: ignore
 | 
			
		||||
        self.assertEqual(r.data["total"], 26)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
        data = {"showCompleted": True, "agentPK": agent1.pk}
 | 
			
		||||
        r = self.client.patch(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        self.assertEqual(len(r.data["actions"]), 12)  # type: ignore
 | 
			
		||||
        self.assertEqual(r.data["completed_count"], 12)  # type: ignore
 | 
			
		||||
        self.assertEqual(r.data["total"], 12)  # type: ignore
 | 
			
		||||
 | 
			
		||||
    def test_all_pending_actions(self):
 | 
			
		||||
        url = "/logs/allpendingactions/"
 | 
			
		||||
        agent = baker.make_recipe("agents.agent")
 | 
			
		||||
        pending_actions = baker.make("logs.PendingAction", agent=agent, _quantity=6)
 | 
			
		||||
 | 
			
		||||
        resp = self.client.get(url, format="json")
 | 
			
		||||
        serializer = PendingActionSerializer(pending_actions, many=True)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertEqual(len(resp.data), 6)
 | 
			
		||||
        self.assertEqual(resp.data, serializer.data)
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("get", url)
 | 
			
		||||
        self.check_not_authenticated("patch", url)
 | 
			
		||||
 | 
			
		||||
    @patch("agents.models.Agent.nats_cmd")
 | 
			
		||||
    def test_cancel_pending_action(self, nats_cmd):
 | 
			
		||||
        url = "/logs/cancelpendingaction/"
 | 
			
		||||
        # TODO fix this TypeError: Object of type coroutine is not JSON serializable
 | 
			
		||||
        """ agent = baker.make("agents.Agent", version="1.1.1")
 | 
			
		||||
        pending_action = baker.make(
 | 
			
		||||
        nats_cmd.return_value = "ok"
 | 
			
		||||
        url = "/logs/pendingactions/"
 | 
			
		||||
        agent = baker.make_recipe("agents.online_agent")
 | 
			
		||||
        action = baker.make(
 | 
			
		||||
            "logs.PendingAction",
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            action_type="schedreboot",
 | 
			
		||||
            details={
 | 
			
		||||
                "time": "2021-01-13 18:20:00",
 | 
			
		||||
                "taskname": "TacticalRMM_SchedReboot_wYzCCDVXlc",
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        data = {"pk": pending_action.id}
 | 
			
		||||
        resp = self.client.delete(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        data = {"pk": action.pk}  # type: ignore
 | 
			
		||||
        r = self.client.delete(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 200)
 | 
			
		||||
        nats_data = {
 | 
			
		||||
            "func": "delschedtask",
 | 
			
		||||
            "schedtaskpayload": {"name": "TacticalRMM_SchedReboot_wYzCCDVXlc"},
 | 
			
		||||
        }
 | 
			
		||||
        nats_cmd.assert_called_with(nats_data, timeout=10)
 | 
			
		||||
 | 
			
		||||
        # try request again and it should fail since pending action doesn't exist
 | 
			
		||||
        resp = self.client.delete(url, data, format="json")
 | 
			
		||||
        self.assertEqual(resp.status_code, 404) """
 | 
			
		||||
        # try request again and it should 404 since pending action doesn't exist
 | 
			
		||||
        r = self.client.delete(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 404)
 | 
			
		||||
 | 
			
		||||
        nats_cmd.reset_mock()
 | 
			
		||||
 | 
			
		||||
        action2 = baker.make(
 | 
			
		||||
            "logs.PendingAction",
 | 
			
		||||
            agent=agent,
 | 
			
		||||
            action_type="schedreboot",
 | 
			
		||||
            details={
 | 
			
		||||
                "time": "2021-01-13 18:20:00",
 | 
			
		||||
                "taskname": "TacticalRMM_SchedReboot_wYzCCDVXlc",
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        data = {"pk": action2.pk}  # type: ignore
 | 
			
		||||
        nats_cmd.return_value = "error deleting sched task"
 | 
			
		||||
        r = self.client.delete(url, data, format="json")
 | 
			
		||||
        self.assertEqual(r.status_code, 400)
 | 
			
		||||
        self.assertEqual(r.data, "error deleting sched task")  # type: ignore
 | 
			
		||||
 | 
			
		||||
        self.check_not_authenticated("delete", url)
 | 
			
		||||
 
 | 
			
		||||
@@ -3,11 +3,9 @@ from django.urls import path
 | 
			
		||||
from . import views
 | 
			
		||||
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
    path("pendingactions/", views.PendingActions.as_view()),
 | 
			
		||||
    path("auditlogs/", views.GetAuditLogs.as_view()),
 | 
			
		||||
    path("auditlogs/optionsfilter/", views.FilterOptionsAuditLog.as_view()),
 | 
			
		||||
    path("<int:pk>/pendingactions/", views.agent_pending_actions),
 | 
			
		||||
    path("allpendingactions/", views.all_pending_actions),
 | 
			
		||||
    path("cancelpendingaction/", views.cancel_pending_action),
 | 
			
		||||
    path("debuglog/<mode>/<hostname>/<order>/", views.debug_log),
 | 
			
		||||
    path("downloadlog/", views.download_log),
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -106,24 +106,36 @@ class FilterOptionsAuditLog(APIView):
 | 
			
		||||
        return Response("error", status=status.HTTP_400_BAD_REQUEST)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view()
 | 
			
		||||
def agent_pending_actions(request, pk):
 | 
			
		||||
    action = PendingAction.objects.filter(agent__pk=pk)
 | 
			
		||||
    return Response(PendingActionSerializer(action, many=True).data)
 | 
			
		||||
class PendingActions(APIView):
 | 
			
		||||
    def patch(self, request):
 | 
			
		||||
        status_filter = "completed" if request.data["showCompleted"] else "pending"
 | 
			
		||||
        if "agentPK" in request.data.keys():
 | 
			
		||||
            actions = PendingAction.objects.filter(
 | 
			
		||||
                agent__pk=request.data["agentPK"], status=status_filter
 | 
			
		||||
            )
 | 
			
		||||
            total = PendingAction.objects.filter(
 | 
			
		||||
                agent__pk=request.data["agentPK"]
 | 
			
		||||
            ).count()
 | 
			
		||||
            completed = PendingAction.objects.filter(
 | 
			
		||||
                agent__pk=request.data["agentPK"], status="completed"
 | 
			
		||||
            ).count()
 | 
			
		||||
 | 
			
		||||
        else:
 | 
			
		||||
            actions = PendingAction.objects.filter(status=status_filter).select_related(
 | 
			
		||||
                "agent"
 | 
			
		||||
            )
 | 
			
		||||
            total = PendingAction.objects.count()
 | 
			
		||||
            completed = PendingAction.objects.filter(status="completed").count()
 | 
			
		||||
 | 
			
		||||
@api_view()
 | 
			
		||||
def all_pending_actions(request):
 | 
			
		||||
    actions = PendingAction.objects.all().select_related("agent")
 | 
			
		||||
    return Response(PendingActionSerializer(actions, many=True).data)
 | 
			
		||||
        ret = {
 | 
			
		||||
            "actions": PendingActionSerializer(actions, many=True).data,
 | 
			
		||||
            "completed_count": completed,
 | 
			
		||||
            "total": total,
 | 
			
		||||
        }
 | 
			
		||||
        return Response(ret)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_view(["DELETE"])
 | 
			
		||||
def cancel_pending_action(request):
 | 
			
		||||
    def delete(self, request):
 | 
			
		||||
        action = get_object_or_404(PendingAction, pk=request.data["pk"])
 | 
			
		||||
    if not action.agent.has_gotasks:
 | 
			
		||||
        return notify_error("Requires agent version 1.1.1 or greater")
 | 
			
		||||
 | 
			
		||||
        nats_data = {
 | 
			
		||||
            "func": "delschedtask",
 | 
			
		||||
            "schedtaskpayload": {"name": action.details["taskname"]},
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user