Compare commits
	
		
			270 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 | 
| @@ -1,7 +1,6 @@ | ||||
| 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: | ||||
|   | ||||
| @@ -136,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}" | ||||
| @@ -150,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 | ||||
| @@ -167,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,6 +1,7 @@ | ||||
| # To ensure app dependencies are ported from your virtual environment/host machine into your container, run 'pip freeze > requirements.txt' in the terminal to overwrite this file | ||||
| asyncio-nats-client | ||||
| celery | ||||
| channels | ||||
| Django | ||||
| django-cors-headers | ||||
| django-rest-knox | ||||
| @@ -30,3 +31,5 @@ mkdocs-material | ||||
| pymdown-extensions | ||||
| Pygments | ||||
| mypy | ||||
| pysnooper | ||||
| isort | ||||
|   | ||||
							
								
								
									
										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. | ||||
							
								
								
									
										2
									
								
								.github/workflows/deploy-docs.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/deploy-docs.yml
									
									
									
									
										vendored
									
									
								
							| @@ -2,7 +2,7 @@ name: Deploy Docs | ||||
| on: | ||||
|   push: | ||||
|     branches: | ||||
|       - develop | ||||
|       - master | ||||
|  | ||||
| defaults: | ||||
|   run: | ||||
|   | ||||
| @@ -8,7 +8,7 @@ | ||||
| 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.* | ||||
|   | ||||
| @@ -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,7 +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) | ||||
|     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,19 +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", | ||||
|             "agents_per_page": 1000, | ||||
|             "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) | ||||
|   | ||||
| @@ -14,7 +14,15 @@ 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): | ||||
| @@ -105,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) | ||||
| @@ -120,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() | ||||
| @@ -137,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"]) | ||||
| @@ -152,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 = "" | ||||
| @@ -184,23 +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"]) | ||||
|  | ||||
|         if "agents_per_page" in request.data.keys(): | ||||
|             user.agents_per_page = request.data["agents_per_page"] | ||||
|             user.save(update_fields=["agents_per_page"]) | ||||
|  | ||||
|         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) | ||||
|   | ||||
| @@ -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, 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 | ||||
| @@ -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 = [] | ||||
| @@ -271,6 +290,20 @@ 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, | ||||
| @@ -284,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, | ||||
| @@ -307,7 +343,7 @@ 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" | ||||
|                 ] | ||||
| @@ -461,9 +497,9 @@ class Agent(BaseAuditModel): | ||||
|             ) | ||||
|         ) | ||||
|  | ||||
|     # 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 | ||||
| @@ -563,9 +599,16 @@ 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): | ||||
| @@ -629,7 +672,11 @@ class Agent(BaseAuditModel): | ||||
|             except ErrTimeout: | ||||
|                 ret = "timeout" | ||||
|             else: | ||||
|                 ret = msgpack.loads(msg.data)  # type: ignore | ||||
|                 try: | ||||
|                     ret = msgpack.loads(msg.data)  # type: ignore | ||||
|                 except Exception as e: | ||||
|                     logger.error(e) | ||||
|                     ret = str(e) | ||||
|  | ||||
|             await nc.close() | ||||
|             return ret | ||||
| @@ -703,11 +750,11 @@ 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"):  # type: ignore | ||||
|         for action in self.pendingactions.filter(action_type="taskaction").exclude(status="completed"):  # type: ignore | ||||
|             if action.details["task_id"] == task_id: | ||||
|                 action.delete() | ||||
|  | ||||
|     def should_create_alert(self, alert_template): | ||||
|     def should_create_alert(self, alert_template=None): | ||||
|         return ( | ||||
|             self.overdue_dashboard_alert | ||||
|             or self.overdue_email_alert | ||||
| @@ -726,7 +773,6 @@ class Agent(BaseAuditModel): | ||||
|         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", | ||||
|             ( | ||||
| @@ -735,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", | ||||
|             ( | ||||
| @@ -751,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, | ||||
|         ) | ||||
|  | ||||
|  | ||||
| @@ -797,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( | ||||
| @@ -822,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", | ||||
|         ] | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -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) | ||||
|  | ||||
| @@ -200,20 +201,6 @@ def agent_outages_task() -> None: | ||||
|             Alert.handle_alert_failure(agent) | ||||
|  | ||||
|  | ||||
| @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)) | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def run_script_email_results_task( | ||||
|     agentpk: int, | ||||
| @@ -266,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) | ||||
|   | ||||
| @@ -12,7 +12,7 @@ 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 | ||||
|  | ||||
| @@ -54,66 +54,22 @@ class TestAgentsList(TacticalTestCase): | ||||
|             _quantity=7, | ||||
|         ) | ||||
|  | ||||
|         data = { | ||||
|             "pagination": { | ||||
|                 "rowsPerPage": 50, | ||||
|                 "rowsNumber": None, | ||||
|                 "sortBy": "hostname", | ||||
|                 "descending": False, | ||||
|                 "page": 1, | ||||
|             }, | ||||
|             "monType": "mixed", | ||||
|         } | ||||
|         # 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 mixed | ||||
|         # test client1 | ||||
|         data = {"clientPK": company1.pk}  # type: ignore | ||||
|         r = self.client.patch(url, data, format="json") | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         self.assertEqual(r.data["total"], 36)  # type: ignore | ||||
|         self.assertEqual(len(r.data["agents"]), 36)  # type: ignore | ||||
|         self.assertEqual(len(r.data), 25)  # type: ignore | ||||
|  | ||||
|         # test servers | ||||
|         data["monType"] = "server" | ||||
|         data["pagination"]["rowsPerPage"] = 6 | ||||
|         # test site3 | ||||
|         data = {"sitePK": site3.pk}  # type: ignore | ||||
|         r = self.client.patch(url, data, format="json") | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         self.assertEqual(r.data["total"], 19)  # type: ignore | ||||
|         self.assertEqual(len(r.data["agents"]), 6)  # type: ignore | ||||
|  | ||||
|         # test workstations | ||||
|         data["monType"] = "server" | ||||
|         data["pagination"]["rowsPerPage"] = 6 | ||||
|         r = self.client.patch(url, data, format="json") | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         self.assertEqual(r.data["total"], 19)  # type: ignore | ||||
|         self.assertEqual(len(r.data["agents"]), 6)  # type: ignore | ||||
|  | ||||
|         # test client1 mixed | ||||
|         data = { | ||||
|             "pagination": { | ||||
|                 "rowsPerPage": 3, | ||||
|                 "rowsNumber": None, | ||||
|                 "sortBy": "hostname", | ||||
|                 "descending": False, | ||||
|                 "page": 1, | ||||
|             }, | ||||
|             "monType": "mixed", | ||||
|             "clientPK": company1.pk,  # type: ignore | ||||
|         } | ||||
|  | ||||
|         r = self.client.patch(url, data, format="json") | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         self.assertEqual(r.data["total"], 25)  # type: ignore | ||||
|         self.assertEqual(len(r.data["agents"]), 3)  # type: ignore | ||||
|  | ||||
|         # test site3 workstations | ||||
|         del data["clientPK"] | ||||
|         data["monType"] = "workstation" | ||||
|         data["sitePK"] = site3.pk  # type: ignore | ||||
|  | ||||
|         r = self.client.patch(url, data, format="json") | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         self.assertEqual(r.data["total"], 7)  # type: ignore | ||||
|         self.assertEqual(len(r.data["agents"]), 3)  # type: ignore | ||||
|         self.assertEqual(len(r.data), 11)  # type: ignore | ||||
|  | ||||
|         self.check_not_authenticated("patch", url) | ||||
|  | ||||
| @@ -242,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/" | ||||
|  | ||||
| @@ -384,6 +335,7 @@ class TestAgentViews(TacticalTestCase): | ||||
|             "func": "schedtask", | ||||
|             "schedtaskpayload": { | ||||
|                 "type": "schedreboot", | ||||
|                 "deleteafter": True, | ||||
|                 "trigger": "once", | ||||
|                 "name": r.data["task_name"],  # type: ignore | ||||
|                 "year": 2025, | ||||
| @@ -411,9 +363,8 @@ class TestAgentViews(TacticalTestCase): | ||||
|         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 = { | ||||
| @@ -421,38 +372,29 @@ class TestAgentViews(TacticalTestCase): | ||||
|             "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"]) | ||||
| @@ -463,44 +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, 200) | ||||
|         self.assertEqual(RecoveryAction.objects.count(), 0) | ||||
|         nats_cmd.assert_called_with( | ||||
|             {"func": "recover", "payload": {"mode": "tacagent"}}, timeout=10 | ||||
|         ) | ||||
|         nats_cmd.reset_mock() | ||||
|  | ||||
|         # 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.assertIn("0.9.5", r.json()) | ||||
|         self.assertEqual(RecoveryAction.objects.count(), 0) | ||||
|         nats_cmd.reset_mock() | ||||
|  | ||||
|         self.check_not_authenticated("post", url) | ||||
|         # 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/" | ||||
| @@ -555,6 +527,35 @@ class TestAgentViews(TacticalTestCase): | ||||
|         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") | ||||
| @@ -842,7 +843,7 @@ class TestAgentViewsNew(TacticalTestCase): | ||||
|         self.authenticate() | ||||
|         self.setup_coresettings() | ||||
|  | ||||
|     def test_agent_counts(self): | ||||
|     """ def test_agent_counts(self): | ||||
|         url = "/agents/agent_counts/" | ||||
|  | ||||
|         # create some data | ||||
| @@ -869,7 +870,7 @@ class TestAgentViewsNew(TacticalTestCase): | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         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/" | ||||
|   | ||||
| @@ -27,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()), | ||||
| ] | ||||
|   | ||||
| @@ -5,8 +5,6 @@ import random | ||||
| import string | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.core.paginator import Paginator | ||||
| from django.db.models import Q | ||||
| from django.http import HttpResponse | ||||
| from django.shortcuts import get_object_or_404 | ||||
| from loguru import logger | ||||
| @@ -20,17 +18,13 @@ from core.models import CoreSettings | ||||
| from logs.models import AuditLog, PendingAction | ||||
| from scripts.models import Script | ||||
| from scripts.tasks import handle_bulk_command_task, handle_bulk_script_task | ||||
| from tacticalrmm.utils import ( | ||||
|     generate_installer_exe, | ||||
|     get_default_timezone, | ||||
|     notify_error, | ||||
|     reload_nats, | ||||
| ) | ||||
| 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, | ||||
| @@ -46,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], | ||||
| @@ -71,10 +65,9 @@ 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" | ||||
|     r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=5)) | ||||
|     if r == "pong": | ||||
|         status = "online" | ||||
|  | ||||
|     return Response({"name": agent.hostname, "status": status}) | ||||
|  | ||||
| @@ -82,8 +75,7 @@ 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)) | ||||
|     asyncio.run(agent.nats_cmd({"func": "uninstall"}, wait=False)) | ||||
|  | ||||
|     name = agent.hostname | ||||
|     agent.delete() | ||||
| @@ -91,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"]) | ||||
|  | ||||
| @@ -107,6 +99,29 @@ def edit_agent(request): | ||||
|         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") | ||||
|  | ||||
|  | ||||
| @@ -149,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") | ||||
| @@ -161,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) | ||||
|     ) | ||||
| @@ -179,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", | ||||
| @@ -200,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", | ||||
| @@ -228,59 +233,47 @@ def send_raw_cmd(request): | ||||
|  | ||||
| class AgentsTableList(APIView): | ||||
|     def patch(self, request): | ||||
|         pagination = request.data["pagination"] | ||||
|         monType = request.data["monType"] | ||||
|         client = Q() | ||||
|         site = Q() | ||||
|         mon_type = Q() | ||||
|  | ||||
|         if monType == "server": | ||||
|             mon_type = Q(monitoring_type="server") | ||||
|         elif monType == "workstation": | ||||
|             mon_type = Q(monitoring_type="workstation") | ||||
|  | ||||
|         if "clientPK" in request.data: | ||||
|             client = Q(site__client_id=request.data["clientPK"]) | ||||
|  | ||||
|         if "sitePK" in request.data: | ||||
|             site = Q(site_id=request.data["sitePK"]) | ||||
|  | ||||
|         queryset = ( | ||||
|             Agent.objects.select_related("site") | ||||
|             .prefetch_related("agentchecks") | ||||
|             .filter(mon_type) | ||||
|             .filter(client) | ||||
|             .filter(site) | ||||
|             .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", | ||||
|         if "sitePK" in request.data.keys(): | ||||
|             queryset = ( | ||||
|                 Agent.objects.select_related("site", "policy", "alert_template") | ||||
|                 .prefetch_related("agentchecks") | ||||
|                 .filter(site_id=request.data["sitePK"]) | ||||
|             ) | ||||
|             .order_by(pagination["sortBy"]) | ||||
|         ) | ||||
|         paginator = Paginator(queryset, pagination["rowsPerPage"]) | ||||
|         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", | ||||
|             "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()} | ||||
|         serializer = AgentTableSerializer( | ||||
|             paginator.get_page(pagination["page"]), many=True, context=ctx | ||||
|         ) | ||||
|  | ||||
|         ret = {"agents": serializer.data, "total": paginator.count} | ||||
|         return Response(ret) | ||||
|         serializer = AgentTableSerializer(queryset, many=True, context=ctx) | ||||
|         return Response(serializer.data) | ||||
|  | ||||
|  | ||||
| @api_view() | ||||
| @@ -310,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") | ||||
| @@ -322,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") | ||||
| @@ -338,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")), | ||||
| @@ -348,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) | ||||
| @@ -396,19 +382,19 @@ def install_agent(request): | ||||
|     ) | ||||
|  | ||||
|     if request.data["installMethod"] == "exe": | ||||
|         return generate_installer_exe( | ||||
|             file_name="rmm-installer.exe", | ||||
|             goarch="amd64" if arch == "64" else "386", | ||||
|             inno=inno, | ||||
|             api=request.data["api"], | ||||
|             client_id=client_id, | ||||
|             site_id=site_id, | ||||
|             atype=request.data["agenttype"], | ||||
|         from tacticalrmm.utils import generate_winagent_exe | ||||
|  | ||||
|         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"], | ||||
|             download_url=download_url, | ||||
|             arch=arch, | ||||
|             token=token, | ||||
|             api=request.data["api"], | ||||
|             file_name=request.data["fileName"], | ||||
|         ) | ||||
|  | ||||
|     elif request.data["installMethod"] == "manual": | ||||
| @@ -503,20 +489,12 @@ 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: | ||||
|         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") | ||||
|     # 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():  # type: ignore | ||||
|         return notify_error( | ||||
| @@ -583,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": | ||||
| @@ -696,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": | ||||
| @@ -765,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") | ||||
|   | ||||
| @@ -158,7 +158,7 @@ class Alert(models.Model): | ||||
|             email_alert = instance.overdue_email_alert | ||||
|             text_alert = instance.overdue_text_alert | ||||
|             dashboard_alert = instance.overdue_dashboard_alert | ||||
|             alert_template = instance.get_alert_template() | ||||
|             alert_template = instance.alert_template | ||||
|             maintenance_mode = instance.maintenance_mode | ||||
|             alert_severity = "error" | ||||
|             agent = instance | ||||
| @@ -194,7 +194,7 @@ class Alert(models.Model): | ||||
|             email_alert = instance.email_alert | ||||
|             text_alert = instance.text_alert | ||||
|             dashboard_alert = instance.dashboard_alert | ||||
|             alert_template = instance.agent.get_alert_template() | ||||
|             alert_template = instance.agent.alert_template | ||||
|             maintenance_mode = instance.agent.maintenance_mode | ||||
|             alert_severity = instance.alert_severity | ||||
|             agent = instance.agent | ||||
| @@ -227,7 +227,7 @@ class Alert(models.Model): | ||||
|             email_alert = instance.email_alert | ||||
|             text_alert = instance.text_alert | ||||
|             dashboard_alert = instance.dashboard_alert | ||||
|             alert_template = instance.agent.get_alert_template() | ||||
|             alert_template = instance.agent.alert_template | ||||
|             maintenance_mode = instance.agent.maintenance_mode | ||||
|             alert_severity = instance.alert_severity | ||||
|             agent = instance.agent | ||||
| @@ -336,7 +336,7 @@ class Alert(models.Model): | ||||
|             resolved_email_task = agent_recovery_email_task | ||||
|             resolved_text_task = agent_recovery_sms_task | ||||
|  | ||||
|             alert_template = instance.get_alert_template() | ||||
|             alert_template = instance.alert_template | ||||
|             alert = cls.objects.get(agent=instance, resolved=False) | ||||
|             maintenance_mode = instance.maintenance_mode | ||||
|             agent = instance | ||||
| @@ -354,7 +354,7 @@ class Alert(models.Model): | ||||
|             resolved_email_task = handle_resolved_check_email_alert_task | ||||
|             resolved_text_task = handle_resolved_check_sms_alert_task | ||||
|  | ||||
|             alert_template = instance.agent.get_alert_template() | ||||
|             alert_template = instance.agent.alert_template | ||||
|             alert = cls.objects.get(assigned_check=instance, resolved=False) | ||||
|             maintenance_mode = instance.agent.maintenance_mode | ||||
|             agent = instance.agent | ||||
| @@ -372,7 +372,7 @@ class Alert(models.Model): | ||||
|             resolved_email_task = handle_resolved_task_email_alert | ||||
|             resolved_text_task = handle_resolved_task_sms_alert | ||||
|  | ||||
|             alert_template = instance.agent.get_alert_template() | ||||
|             alert_template = instance.agent.alert_template | ||||
|             alert = cls.objects.get(assigned_task=instance, resolved=False) | ||||
|             maintenance_mode = instance.agent.maintenance_mode | ||||
|             agent = instance.agent | ||||
|   | ||||
| @@ -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" | ||||
|   | ||||
| @@ -5,6 +5,8 @@ from django.conf import settings | ||||
| from django.utils import timezone as djangotime | ||||
| from model_bakery import baker, seq | ||||
|  | ||||
| from alerts.tasks import cache_agents_alert_template | ||||
| from autotasks.models import AutomatedTask | ||||
| from core.models import CoreSettings | ||||
| from tacticalrmm.test import TacticalTestCase | ||||
|  | ||||
| @@ -395,8 +397,8 @@ class TestAlertTasks(TacticalTestCase): | ||||
|         alert_templates = baker.make("alerts.AlertTemplate", _quantity=6) | ||||
|  | ||||
|         # should be None | ||||
|         self.assertFalse(workstation.get_alert_template()) | ||||
|         self.assertFalse(server.get_alert_template()) | ||||
|         self.assertFalse(workstation.set_alert_template()) | ||||
|         self.assertFalse(server.set_alert_template()) | ||||
|  | ||||
|         # assign first Alert Template as to a policy and apply it as default | ||||
|         policy.alert_template = alert_templates[0]  # type: ignore | ||||
| @@ -405,15 +407,15 @@ class TestAlertTasks(TacticalTestCase): | ||||
|         core.server_policy = policy | ||||
|         core.save() | ||||
|  | ||||
|         self.assertEquals(server.get_alert_template().pk, alert_templates[0].pk)  # type: ignore | ||||
|         self.assertEquals(workstation.get_alert_template().pk, alert_templates[0].pk)  # type: ignore | ||||
|         self.assertEquals(server.set_alert_template().pk, alert_templates[0].pk)  # type: ignore | ||||
|         self.assertEquals(workstation.set_alert_template().pk, alert_templates[0].pk)  # type: ignore | ||||
|  | ||||
|         # assign second Alert Template to as default alert template | ||||
|         core.alert_template = alert_templates[1]  # type: ignore | ||||
|         core.save() | ||||
|  | ||||
|         self.assertEquals(workstation.get_alert_template().pk, alert_templates[1].pk)  # type: ignore | ||||
|         self.assertEquals(server.get_alert_template().pk, alert_templates[1].pk)  # type: ignore | ||||
|         self.assertEquals(workstation.set_alert_template().pk, alert_templates[1].pk)  # type: ignore | ||||
|         self.assertEquals(server.set_alert_template().pk, alert_templates[1].pk)  # type: ignore | ||||
|  | ||||
|         # assign third Alert Template to client | ||||
|         workstation.client.alert_template = alert_templates[2]  # type: ignore | ||||
| @@ -421,8 +423,8 @@ class TestAlertTasks(TacticalTestCase): | ||||
|         workstation.client.save() | ||||
|         server.client.save() | ||||
|  | ||||
|         self.assertEquals(workstation.get_alert_template().pk, alert_templates[2].pk)  # type: ignore | ||||
|         self.assertEquals(server.get_alert_template().pk, alert_templates[2].pk)  # type: ignore | ||||
|         self.assertEquals(workstation.set_alert_template().pk, alert_templates[2].pk)  # type: ignore | ||||
|         self.assertEquals(server.set_alert_template().pk, alert_templates[2].pk)  # type: ignore | ||||
|  | ||||
|         # apply policy to client and should override | ||||
|         workstation.client.workstation_policy = policy | ||||
| @@ -430,8 +432,8 @@ class TestAlertTasks(TacticalTestCase): | ||||
|         workstation.client.save() | ||||
|         server.client.save() | ||||
|  | ||||
|         self.assertEquals(workstation.get_alert_template().pk, alert_templates[0].pk)  # type: ignore | ||||
|         self.assertEquals(server.get_alert_template().pk, alert_templates[0].pk)  # type: ignore | ||||
|         self.assertEquals(workstation.set_alert_template().pk, alert_templates[0].pk)  # type: ignore | ||||
|         self.assertEquals(server.set_alert_template().pk, alert_templates[0].pk)  # type: ignore | ||||
|  | ||||
|         # assign fouth Alert Template to site | ||||
|         workstation.site.alert_template = alert_templates[3]  # type: ignore | ||||
| @@ -439,8 +441,8 @@ class TestAlertTasks(TacticalTestCase): | ||||
|         workstation.site.save() | ||||
|         server.site.save() | ||||
|  | ||||
|         self.assertEquals(workstation.get_alert_template().pk, alert_templates[3].pk)  # type: ignore | ||||
|         self.assertEquals(server.get_alert_template().pk, alert_templates[3].pk)  # type: ignore | ||||
|         self.assertEquals(workstation.set_alert_template().pk, alert_templates[3].pk)  # type: ignore | ||||
|         self.assertEquals(server.set_alert_template().pk, alert_templates[3].pk)  # type: ignore | ||||
|  | ||||
|         # apply policy to site | ||||
|         workstation.site.workstation_policy = policy | ||||
| @@ -448,8 +450,8 @@ class TestAlertTasks(TacticalTestCase): | ||||
|         workstation.site.save() | ||||
|         server.site.save() | ||||
|  | ||||
|         self.assertEquals(workstation.get_alert_template().pk, alert_templates[0].pk)  # type: ignore | ||||
|         self.assertEquals(server.get_alert_template().pk, alert_templates[0].pk)  # type: ignore | ||||
|         self.assertEquals(workstation.set_alert_template().pk, alert_templates[0].pk)  # type: ignore | ||||
|         self.assertEquals(server.set_alert_template().pk, alert_templates[0].pk)  # type: ignore | ||||
|  | ||||
|         # apply policy to agents | ||||
|         workstation.policy = policy | ||||
| @@ -457,35 +459,35 @@ class TestAlertTasks(TacticalTestCase): | ||||
|         workstation.save() | ||||
|         server.save() | ||||
|  | ||||
|         self.assertEquals(workstation.get_alert_template().pk, alert_templates[0].pk)  # type: ignore | ||||
|         self.assertEquals(server.get_alert_template().pk, alert_templates[0].pk)  # type: ignore | ||||
|         self.assertEquals(workstation.set_alert_template().pk, alert_templates[0].pk)  # type: ignore | ||||
|         self.assertEquals(server.set_alert_template().pk, alert_templates[0].pk)  # type: ignore | ||||
|  | ||||
|         # test disabling alert template | ||||
|         alert_templates[0].is_active = False  # type: ignore | ||||
|         alert_templates[0].save()  # type: ignore | ||||
|  | ||||
|         self.assertEquals(workstation.get_alert_template().pk, alert_templates[3].pk)  # type: ignore | ||||
|         self.assertEquals(server.get_alert_template().pk, alert_templates[3].pk)  # type: ignore | ||||
|         self.assertEquals(workstation.set_alert_template().pk, alert_templates[3].pk)  # type: ignore | ||||
|         self.assertEquals(server.set_alert_template().pk, alert_templates[3].pk)  # type: ignore | ||||
|  | ||||
|         # test policy exclusions | ||||
|         alert_templates[3].excluded_agents.set([workstation.pk])  # type: ignore | ||||
|  | ||||
|         self.assertEquals(workstation.get_alert_template().pk, alert_templates[2].pk)  # type: ignore | ||||
|         self.assertEquals(server.get_alert_template().pk, alert_templates[3].pk)  # type: ignore | ||||
|         self.assertEquals(workstation.set_alert_template().pk, alert_templates[2].pk)  # type: ignore | ||||
|         self.assertEquals(server.set_alert_template().pk, alert_templates[3].pk)  # type: ignore | ||||
|  | ||||
|         # test workstation exclusions | ||||
|         alert_templates[2].exclude_workstations = True  # type: ignore | ||||
|         alert_templates[2].save()  # type: ignore | ||||
|  | ||||
|         self.assertEquals(workstation.get_alert_template().pk, alert_templates[1].pk)  # type: ignore | ||||
|         self.assertEquals(server.get_alert_template().pk, alert_templates[3].pk)  # type: ignore | ||||
|         self.assertEquals(workstation.set_alert_template().pk, alert_templates[1].pk)  # type: ignore | ||||
|         self.assertEquals(server.set_alert_template().pk, alert_templates[3].pk)  # type: ignore | ||||
|  | ||||
|         # test server exclusions | ||||
|         alert_templates[3].exclude_servers = True  # type: ignore | ||||
|         alert_templates[3].save()  # type: ignore | ||||
|  | ||||
|         self.assertEquals(workstation.get_alert_template().pk, alert_templates[1].pk)  # type: ignore | ||||
|         self.assertEquals(server.get_alert_template().pk, alert_templates[2].pk)  # type: ignore | ||||
|         self.assertEquals(workstation.set_alert_template().pk, alert_templates[1].pk)  # type: ignore | ||||
|         self.assertEquals(server.set_alert_template().pk, alert_templates[2].pk)  # type: ignore | ||||
|  | ||||
|     @patch("agents.tasks.sleep") | ||||
|     @patch("core.models.CoreSettings.send_mail") | ||||
| @@ -504,6 +506,7 @@ class TestAlertTasks(TacticalTestCase): | ||||
|         send_email, | ||||
|         sleep, | ||||
|     ): | ||||
|         from agents.models import Agent | ||||
|         from agents.tasks import ( | ||||
|             agent_outage_email_task, | ||||
|             agent_outage_sms_task, | ||||
| @@ -564,6 +567,8 @@ class TestAlertTasks(TacticalTestCase): | ||||
|         agent_email_alert = baker.make_recipe( | ||||
|             "agents.overdue_agent", overdue_email_alert=True | ||||
|         ) | ||||
|  | ||||
|         cache_agents_alert_template() | ||||
|         agent_outages_task() | ||||
|  | ||||
|         # should have created 6 alerts | ||||
| @@ -663,6 +668,9 @@ class TestAlertTasks(TacticalTestCase): | ||||
|         alert_template_always_text.agent_text_on_resolved = True  # type: ignore | ||||
|         alert_template_always_text.save()  # type: ignore | ||||
|  | ||||
|         agent_template_text = Agent.objects.get(pk=agent_template_text.pk) | ||||
|         agent_template_email = Agent.objects.get(pk=agent_template_email.pk) | ||||
|  | ||||
|         # have the two agents checkin | ||||
|         url = "/api/v3/checkin/" | ||||
|  | ||||
| @@ -719,7 +727,8 @@ class TestAlertTasks(TacticalTestCase): | ||||
|         send_email, | ||||
|         sleep, | ||||
|     ): | ||||
|  | ||||
|         from alerts.tasks import cache_agents_alert_template | ||||
|         from checks.models import Check | ||||
|         from checks.tasks import ( | ||||
|             handle_check_email_alert_task, | ||||
|             handle_check_sms_alert_task, | ||||
| @@ -800,6 +809,14 @@ class TestAlertTasks(TacticalTestCase): | ||||
|             "checks.script_check", agent=agent_no_settings | ||||
|         ) | ||||
|  | ||||
|         # update alert template and pull new checks from DB | ||||
|         cache_agents_alert_template() | ||||
|         check_template_email = Check.objects.get(pk=check_template_email.pk) | ||||
|         check_template_dashboard_text = Check.objects.get( | ||||
|             pk=check_template_dashboard_text.pk | ||||
|         ) | ||||
|         check_template_blank = Check.objects.get(pk=check_template_blank.pk) | ||||
|  | ||||
|         # test agent with check that has alert settings | ||||
|         check_agent.alert_severity = "warning" | ||||
|         check_agent.status = "failing" | ||||
| @@ -905,11 +922,11 @@ class TestAlertTasks(TacticalTestCase): | ||||
|             Alert.objects.filter(assigned_check=check_template_email).count(), 1 | ||||
|         ) | ||||
|  | ||||
|         alert_template_email.check_periodic_alert_days = 1 | ||||
|         alert_template_email.save() | ||||
|         alert_template_email.check_periodic_alert_days = 1  # type: ignore | ||||
|         alert_template_email.save()  # type: ignore | ||||
|  | ||||
|         alert_template_dashboard_text.check_periodic_alert_days = 1 | ||||
|         alert_template_dashboard_text.save() | ||||
|         alert_template_dashboard_text.check_periodic_alert_days = 1  # type: ignore | ||||
|         alert_template_dashboard_text.save()  # type: ignore | ||||
|  | ||||
|         # set last email time for alert in the past | ||||
|         alert_email = Alert.objects.get(assigned_check=check_template_email) | ||||
| @@ -921,6 +938,13 @@ class TestAlertTasks(TacticalTestCase): | ||||
|         alert_sms.sms_sent = djangotime.now() - djangotime.timedelta(days=20) | ||||
|         alert_sms.save() | ||||
|  | ||||
|         # refresh checks to get alert template changes | ||||
|         check_template_email = Check.objects.get(pk=check_template_email.pk) | ||||
|         check_template_dashboard_text = Check.objects.get( | ||||
|             pk=check_template_dashboard_text.pk | ||||
|         ) | ||||
|         check_template_blank = Check.objects.get(pk=check_template_blank.pk) | ||||
|  | ||||
|         Alert.handle_alert_failure(check_template_email) | ||||
|         Alert.handle_alert_failure(check_template_dashboard_text) | ||||
|  | ||||
| @@ -939,11 +963,18 @@ class TestAlertTasks(TacticalTestCase): | ||||
|         resolved_email.assert_not_called() | ||||
|  | ||||
|         # test resolved notifications | ||||
|         alert_template_email.check_email_on_resolved = True | ||||
|         alert_template_email.save() | ||||
|         alert_template_email.check_email_on_resolved = True  # type: ignore | ||||
|         alert_template_email.save()  # type: ignore | ||||
|  | ||||
|         alert_template_dashboard_text.check_text_on_resolved = True | ||||
|         alert_template_dashboard_text.save() | ||||
|         alert_template_dashboard_text.check_text_on_resolved = True  # type: ignore | ||||
|         alert_template_dashboard_text.save()  # type: ignore | ||||
|  | ||||
|         # refresh checks to get alert template changes | ||||
|         check_template_email = Check.objects.get(pk=check_template_email.pk) | ||||
|         check_template_dashboard_text = Check.objects.get( | ||||
|             pk=check_template_dashboard_text.pk | ||||
|         ) | ||||
|         check_template_blank = Check.objects.get(pk=check_template_blank.pk) | ||||
|  | ||||
|         Alert.handle_alert_resolve(check_template_email) | ||||
|  | ||||
| @@ -980,7 +1011,8 @@ class TestAlertTasks(TacticalTestCase): | ||||
|         send_email, | ||||
|         sleep, | ||||
|     ): | ||||
|  | ||||
|         from alerts.tasks import cache_agents_alert_template | ||||
|         from autotasks.models import AutomatedTask | ||||
|         from autotasks.tasks import ( | ||||
|             handle_resolved_task_email_alert, | ||||
|             handle_resolved_task_sms_alert, | ||||
| @@ -1052,8 +1084,14 @@ class TestAlertTasks(TacticalTestCase): | ||||
|             "autotasks.AutomatedTask", agent=agent_no_settings, alert_severity="warning" | ||||
|         ) | ||||
|  | ||||
|         # update alert template and pull new checks from DB | ||||
|         cache_agents_alert_template() | ||||
|         task_template_email = AutomatedTask.objects.get(pk=task_template_email.pk)  # type: ignore | ||||
|         task_template_dashboard_text = AutomatedTask.objects.get(pk=task_template_dashboard_text.pk)  # type: ignore | ||||
|         task_template_blank = AutomatedTask.objects.get(pk=task_template_blank.pk)  # type: ignore | ||||
|  | ||||
|         # test agent with task that has alert settings | ||||
|         Alert.handle_alert_failure(task_agent) | ||||
|         Alert.handle_alert_failure(task_agent)  # type: ignore | ||||
|  | ||||
|         # alert should have been created and sms, email notifications sent | ||||
|         self.assertTrue(Alert.objects.filter(assigned_task=task_agent).exists()) | ||||
| @@ -1081,7 +1119,7 @@ class TestAlertTasks(TacticalTestCase): | ||||
|         send_sms.reset_mock() | ||||
|  | ||||
|         # test task with an agent that has an email always alert template | ||||
|         Alert.handle_alert_failure(task_template_email) | ||||
|         Alert.handle_alert_failure(task_template_email)  # type: ignore | ||||
|  | ||||
|         self.assertTrue(Alert.objects.filter(assigned_task=task_template_email)) | ||||
|         alertpk = Alert.objects.get(assigned_task=task_template_email).pk | ||||
| @@ -1099,7 +1137,7 @@ class TestAlertTasks(TacticalTestCase): | ||||
|         send_email.reset_mock() | ||||
|  | ||||
|         # test task with an agent that has an email always alert template | ||||
|         Alert.handle_alert_failure(task_template_dashboard_text) | ||||
|         Alert.handle_alert_failure(task_template_dashboard_text)  # type: ignore | ||||
|  | ||||
|         self.assertTrue( | ||||
|             Alert.objects.filter(assigned_task=task_template_dashboard_text).exists() | ||||
| @@ -1110,11 +1148,11 @@ class TestAlertTasks(TacticalTestCase): | ||||
|         outage_sms.assert_not_called | ||||
|  | ||||
|         # update task alert seveity to error | ||||
|         task_template_dashboard_text.alert_severity = "error" | ||||
|         task_template_dashboard_text.save() | ||||
|         task_template_dashboard_text.alert_severity = "error"  # type: ignore | ||||
|         task_template_dashboard_text.save()  # type: ignore | ||||
|  | ||||
|         # now should trigger alert | ||||
|         Alert.handle_alert_failure(task_template_dashboard_text) | ||||
|         Alert.handle_alert_failure(task_template_dashboard_text)  # type: ignore | ||||
|         outage_sms.assert_called_with(pk=alertpk, alert_interval=0) | ||||
|         outage_sms.reset_mock() | ||||
|  | ||||
| @@ -1130,21 +1168,21 @@ class TestAlertTasks(TacticalTestCase): | ||||
|         send_sms.reset_mock() | ||||
|  | ||||
|         # test task with an agent that has a blank alert template | ||||
|         Alert.handle_alert_failure(task_template_blank) | ||||
|         Alert.handle_alert_failure(task_template_blank)  # type: ignore | ||||
|  | ||||
|         self.assertFalse( | ||||
|             Alert.objects.filter(assigned_task=task_template_blank).exists() | ||||
|         ) | ||||
|  | ||||
|         # test task that has no template and no settings | ||||
|         Alert.handle_alert_failure(task_no_settings) | ||||
|         Alert.handle_alert_failure(task_no_settings)  # type: ignore | ||||
|  | ||||
|         self.assertFalse(Alert.objects.filter(assigned_task=task_no_settings).exists()) | ||||
|  | ||||
|         # test periodic notifications | ||||
|  | ||||
|         # make sure a failing task won't trigger another notification and only create a single alert | ||||
|         Alert.handle_alert_failure(task_template_email) | ||||
|         Alert.handle_alert_failure(task_template_email)  # type: ignore | ||||
|         send_email.assert_not_called() | ||||
|         send_sms.assert_not_called() | ||||
|  | ||||
| @@ -1152,11 +1190,11 @@ class TestAlertTasks(TacticalTestCase): | ||||
|             Alert.objects.filter(assigned_task=task_template_email).count(), 1 | ||||
|         ) | ||||
|  | ||||
|         alert_template_email.task_periodic_alert_days = 1 | ||||
|         alert_template_email.save() | ||||
|         alert_template_email.task_periodic_alert_days = 1  # type: ignore | ||||
|         alert_template_email.save()  # type: ignore | ||||
|  | ||||
|         alert_template_dashboard_text.task_periodic_alert_days = 1 | ||||
|         alert_template_dashboard_text.save() | ||||
|         alert_template_dashboard_text.task_periodic_alert_days = 1  # type: ignore | ||||
|         alert_template_dashboard_text.save()  # type: ignore | ||||
|  | ||||
|         # set last email time for alert in the past | ||||
|         alert_email = Alert.objects.get(assigned_task=task_template_email) | ||||
| @@ -1168,8 +1206,13 @@ class TestAlertTasks(TacticalTestCase): | ||||
|         alert_sms.sms_sent = djangotime.now() - djangotime.timedelta(days=20) | ||||
|         alert_sms.save() | ||||
|  | ||||
|         Alert.handle_alert_failure(task_template_email) | ||||
|         Alert.handle_alert_failure(task_template_dashboard_text) | ||||
|         # refresh automated tasks to get new alert templates | ||||
|         task_template_email = AutomatedTask.objects.get(pk=task_template_email.pk)  # type: ignore | ||||
|         task_template_dashboard_text = AutomatedTask.objects.get(pk=task_template_dashboard_text.pk)  # type: ignore | ||||
|         task_template_blank = AutomatedTask.objects.get(pk=task_template_blank.pk)  # type: ignore | ||||
|  | ||||
|         Alert.handle_alert_failure(task_template_email)  # type: ignore | ||||
|         Alert.handle_alert_failure(task_template_dashboard_text)  # type: ignore | ||||
|  | ||||
|         outage_email.assert_called_with(pk=alert_email.pk, alert_interval=1) | ||||
|         outage_sms.assert_called_with(pk=alert_sms.pk, alert_interval=1) | ||||
| @@ -1177,7 +1220,7 @@ class TestAlertTasks(TacticalTestCase): | ||||
|         outage_sms.reset_mock() | ||||
|  | ||||
|         # test resolving alerts | ||||
|         Alert.handle_alert_resolve(task_agent) | ||||
|         Alert.handle_alert_resolve(task_agent)  # type: ignore | ||||
|  | ||||
|         self.assertTrue(Alert.objects.get(assigned_task=task_agent).resolved) | ||||
|         self.assertTrue(Alert.objects.get(assigned_task=task_agent).resolved_on) | ||||
| @@ -1186,19 +1229,24 @@ class TestAlertTasks(TacticalTestCase): | ||||
|         resolved_email.assert_not_called() | ||||
|  | ||||
|         # test resolved notifications | ||||
|         alert_template_email.task_email_on_resolved = True | ||||
|         alert_template_email.save() | ||||
|         alert_template_email.task_email_on_resolved = True  # type: ignore | ||||
|         alert_template_email.save()  # type: ignore | ||||
|  | ||||
|         alert_template_dashboard_text.task_text_on_resolved = True | ||||
|         alert_template_dashboard_text.save() | ||||
|         alert_template_dashboard_text.task_text_on_resolved = True  # type: ignore | ||||
|         alert_template_dashboard_text.save()  # type: ignore | ||||
|  | ||||
|         Alert.handle_alert_resolve(task_template_email) | ||||
|         # refresh automated tasks to get new alert templates | ||||
|         task_template_email = AutomatedTask.objects.get(pk=task_template_email.pk)  # type: ignore | ||||
|         task_template_dashboard_text = AutomatedTask.objects.get(pk=task_template_dashboard_text.pk)  # type: ignore | ||||
|         task_template_blank = AutomatedTask.objects.get(pk=task_template_blank.pk)  # type: ignore | ||||
|  | ||||
|         Alert.handle_alert_resolve(task_template_email)  # type: ignore | ||||
|  | ||||
|         resolved_email.assert_called_with(pk=alert_email.pk) | ||||
|         resolved_sms.assert_not_called() | ||||
|         resolved_email.reset_mock() | ||||
|  | ||||
|         Alert.handle_alert_resolve(task_template_dashboard_text) | ||||
|         Alert.handle_alert_resolve(task_template_dashboard_text)  # type: ignore | ||||
|  | ||||
|         resolved_sms.assert_called_with(pk=alert_sms.pk) | ||||
|         resolved_email.assert_not_called() | ||||
| @@ -1276,6 +1324,8 @@ class TestAlertTasks(TacticalTestCase): | ||||
|         agent.client.alert_template = alert_template | ||||
|         agent.client.save() | ||||
|  | ||||
|         agent.set_alert_template() | ||||
|  | ||||
|         agent_outages_task() | ||||
|  | ||||
|         # this is what data should be | ||||
|   | ||||
| @@ -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) | ||||
| @@ -54,6 +90,45 @@ class TestAPIv3(TacticalTestCase): | ||||
|             {"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 | ||||
|  | ||||
| @@ -89,3 +164,42 @@ class TestAPIv3(TacticalTestCase): | ||||
|         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()), | ||||
| @@ -18,4 +19,5 @@ urlpatterns = [ | ||||
|     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()), | ||||
| ] | ||||
|   | ||||
| @@ -65,13 +65,6 @@ class CheckIn(APIView): | ||||
|         if Alert.objects.filter(agent=agent, resolved=False).exists(): | ||||
|             Alert.handle_alert_resolve(agent) | ||||
|  | ||||
|         recovery = agent.recoveryactions.filter(last_run=None).last()  # type: ignore | ||||
|         if recovery is not None: | ||||
|             recovery.last_run = djangotime.now() | ||||
|             recovery.save(update_fields=["last_run"]) | ||||
|             handle_agent_recovery_task.delay(pk=recovery.pk)  # type: ignore | ||||
|             return Response("ok") | ||||
|  | ||||
|         # get any pending actions | ||||
|         if agent.pendingactions.filter(status="pending").exists():  # type: ignore | ||||
|             agent.handle_pending_actions() | ||||
| @@ -267,14 +260,13 @@ class SupersededWinUpdate(APIView): | ||||
|         return Response("ok") | ||||
|  | ||||
|  | ||||
| class CheckRunner(APIView): | ||||
| 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,7 +325,10 @@ 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): | ||||
| @@ -513,3 +544,27 @@ class ChocoResult(APIView): | ||||
|         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 | ||||
|  | ||||
| @@ -52,19 +65,41 @@ class Policy(BaseAuditModel): | ||||
|     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)  # type: ignore | ||||
|         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() | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
| @@ -224,7 +223,7 @@ class AutomatedTask(BaseAuditModel): | ||||
|  | ||||
|         create_win_task_schedule.delay(task.pk) | ||||
|  | ||||
|     def should_create_alert(self, alert_template): | ||||
|     def should_create_alert(self, alert_template=None): | ||||
|         return ( | ||||
|             self.dashboard_alert | ||||
|             or self.email_alert | ||||
| @@ -243,7 +242,6 @@ class AutomatedTask(BaseAuditModel): | ||||
|         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" | ||||
| @@ -255,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" | ||||
| @@ -274,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 = ( | ||||
| @@ -288,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 | ||||
|  | ||||
|   | ||||
| @@ -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}") | ||||
|   | ||||
| @@ -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), | ||||
|         ), | ||||
|     ] | ||||
| @@ -93,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 | ||||
| @@ -181,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: | ||||
| @@ -259,7 +263,7 @@ class Check(BaseAuditModel): | ||||
|             "modified_time", | ||||
|         ] | ||||
|  | ||||
|     def should_create_alert(self, alert_template): | ||||
|     def should_create_alert(self, alert_template=None): | ||||
|  | ||||
|         return ( | ||||
|             self.dashboard_alert | ||||
| @@ -488,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" | ||||
| @@ -563,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, | ||||
| @@ -586,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): | ||||
| @@ -613,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" | ||||
|  | ||||
| @@ -628,12 +633,15 @@ class Check(BaseAuditModel): | ||||
|             if self.error_threshold: | ||||
|                 text += f" Error Threshold: {self.error_threshold}%" | ||||
|  | ||||
|             percent_used = [ | ||||
|                 d["percent"] for d in self.agent.disks if d["device"] == self.disk | ||||
|             ][0] | ||||
|             percent_free = 100 - percent_used | ||||
|             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}" | ||||
|                 body = subject + f" - Free: {percent_free}%, {text}" | ||||
|             except: | ||||
|                 body = subject + f" - Disk {self.disk} does not exist" | ||||
|  | ||||
|         elif self.check_type == "script": | ||||
|  | ||||
| @@ -662,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": | ||||
|  | ||||
| @@ -695,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: | ||||
| @@ -715,11 +713,15 @@ class Check(BaseAuditModel): | ||||
|             if self.error_threshold: | ||||
|                 text += f" Error Threshold: {self.error_threshold}%" | ||||
|  | ||||
|             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}" | ||||
|             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": | ||||
| @@ -737,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 | ||||
|  | ||||
|   | ||||
| @@ -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) | ||||
| @@ -1003,6 +997,12 @@ class TestCheckTasks(TacticalTestCase): | ||||
|                     "source": "source", | ||||
|                     "message": "a test message", | ||||
|                 }, | ||||
|                 { | ||||
|                     "eventType": "error", | ||||
|                     "eventID": 123, | ||||
|                     "source": "source", | ||||
|                     "message": "a test message", | ||||
|                 }, | ||||
|             ], | ||||
|         } | ||||
|  | ||||
| @@ -1107,3 +1107,61 @@ class TestCheckTasks(TacticalTestCase): | ||||
|         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") | ||||
|   | ||||
| @@ -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()), | ||||
|   | ||||
| @@ -6,22 +6,27 @@ import pytz | ||||
| from django.conf import settings | ||||
| 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 | ||||
|  | ||||
| from agents.models import Agent | ||||
| from core.models import CoreSettings | ||||
| from tacticalrmm.utils import generate_installer_exe, notify_error | ||||
| 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): | ||||
| @@ -29,45 +34,99 @@ 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 | ||||
|  | ||||
|                 serializer = ClientCustomFieldSerializer(data=custom_field) | ||||
|                 serializer.is_valid(raise_exception=True) | ||||
|                 serializer.save() | ||||
|  | ||||
|         return Response(f"{client} was added!") | ||||
|  | ||||
|  | ||||
| class GetUpdateDeleteClient(APIView): | ||||
| class GetUpdateClient(APIView): | ||||
|     def get(self, request, pk): | ||||
|         client = get_object_or_404(Client, pk=pk) | ||||
|         return Response(ClientSerializer(client).data) | ||||
|  | ||||
|     def put(self, request, pk): | ||||
|         client = get_object_or_404(Client, pk=pk) | ||||
|  | ||||
|         serializer = ClientSerializer(data=request.data, instance=client, partial=True) | ||||
|         serializer = ClientSerializer( | ||||
|             data=request.data["client"], instance=client, partial=True | ||||
|         ) | ||||
|         serializer.is_valid(raise_exception=True) | ||||
|         serializer.save() | ||||
|  | ||||
|         return Response("The Client was renamed") | ||||
|         # 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 | ||||
|  | ||||
|     def delete(self, request, pk): | ||||
|         client = get_object_or_404(Client, pk=pk) | ||||
|         agent_count = Agent.objects.filter(site__client=client).count() | ||||
|         if agent_count > 0: | ||||
|         agents = Agent.objects.filter(site__client=client) | ||||
|  | ||||
|         if not sitepk: | ||||
|             return notify_error( | ||||
|                 f"Cannot delete {client} while {agent_count} agents exist in it. Move the agents to another client first." | ||||
|                 "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!") | ||||
|  | ||||
| @@ -84,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"]: | ||||
|  | ||||
|                 custom_field = field | ||||
|                 custom_field["site"] = pk | ||||
|  | ||||
|                 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("Site was edited!") | ||||
|  | ||||
|  | ||||
| class GetUpdateDeleteSite(APIView): | ||||
|     def put(self, request, pk): | ||||
| class DeleteSite(APIView): | ||||
|     def delete(self, request, pk, sitepk): | ||||
|         from automation.tasks import generate_all_agent_checks_task | ||||
|  | ||||
|         site = get_object_or_404(Site, pk=pk) | ||||
|         serializer = SiteSerializer(instance=site, data=request.data, partial=True) | ||||
|         serializer.is_valid(raise_exception=True) | ||||
|         serializer.save() | ||||
|  | ||||
|         return Response("ok") | ||||
|  | ||||
|     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!") | ||||
|  | ||||
| @@ -173,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: | ||||
| @@ -180,28 +292,22 @@ class GenerateAgent(APIView): | ||||
|  | ||||
|         d = get_object_or_404(Deployment, uid=uid) | ||||
|  | ||||
|         inno = ( | ||||
|             f"winagent-v{settings.LATEST_AGENT_VER}.exe" | ||||
|             if d.arch == "64" | ||||
|             else f"winagent-v{settings.LATEST_AGENT_VER}-x86.exe" | ||||
|         ) | ||||
|         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}" | ||||
|  | ||||
|         return generate_installer_exe( | ||||
|             file_name=f"rmm-{client}-{site}-{d.mon_type}{ext}", | ||||
|             goarch="amd64" if d.arch == "64" else "386", | ||||
|             inno=inno, | ||||
|             api=f"https://{request.get_host()}", | ||||
|             client_id=d.client.pk, | ||||
|             site_id=d.site.pk, | ||||
|             atype=d.mon_type, | ||||
|         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"], | ||||
|             download_url=settings.DL_64 if d.arch == "64" else settings.DL_32, | ||||
|             arch=d.arch, | ||||
|             token=d.token_key, | ||||
|             api=f"https://{request.get_host()}", | ||||
|             file_name=file_name, | ||||
|         ) | ||||
|   | ||||
| @@ -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,5 +0,0 @@ | ||||
| module github.com/wh1te909/goinstaller | ||||
|  | ||||
| go 1.16 | ||||
|  | ||||
| require github.com/josephspurrier/goversioninfo v1.2.0 // indirect | ||||
| @@ -1,10 +0,0 @@ | ||||
| github.com/akavel/rsrc v0.8.0 h1:zjWn7ukO9Kc5Q62DOJCcxGpXC18RawVtYAGdz2aLlfw= | ||||
| github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= | ||||
| github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| github.com/josephspurrier/goversioninfo v1.2.0 h1:tpLHXAxLHKHg/dCU2AAYx08A4m+v9/CWg6+WUvTF4uQ= | ||||
| github.com/josephspurrier/goversioninfo v1.2.0/go.mod h1:AGP2a+Y/OVJZ+s6XM4IwFUpkETwvn0orYurY8qpw1+0= | ||||
| github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||||
| github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||||
| github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||
| gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
| @@ -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,186 +0,0 @@ | ||||
| //go:generate goversioninfo -icon=onit.ico -manifest=goversioninfo.exe.manifest -gofile=versioninfo.go | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"flag" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"net" | ||||
| 	"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 | ||||
| ) | ||||
|  | ||||
| var netTransport = &http.Transport{ | ||||
| 	Dial: (&net.Dialer{ | ||||
| 		Timeout: 5 * time.Second, | ||||
| 	}).Dial, | ||||
| 	TLSHandshakeTimeout: 5 * time.Second, | ||||
| } | ||||
|  | ||||
| var netClient = &http.Client{ | ||||
| 	Timeout:   time.Second * 900, | ||||
| 	Transport: netTransport, | ||||
| } | ||||
|  | ||||
| func downloadAgent(filepath string) (err error) { | ||||
|  | ||||
| 	out, err := os.Create(filepath) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer out.Close() | ||||
|  | ||||
| 	resp, err := netClient.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") | ||||
| 	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 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') | ||||
|   | ||||
							
								
								
									
										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) | ||||
| @@ -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,7 +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, | ||||
|             "agents_per_page": request.user.agents_per_page, | ||||
|             "client_tree_sort": request.user.client_tree_sort, | ||||
|             "client_tree_splitter": request.user.client_tree_splitter, | ||||
|             "loading_bar_color": request.user.loading_bar_color, | ||||
|         } | ||||
|     ) | ||||
|  | ||||
| @@ -133,3 +136,46 @@ def server_maintenance(request): | ||||
|         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") | ||||
|   | ||||
| @@ -250,8 +250,10 @@ 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" | ||||
|  | ||||
|   | ||||
| @@ -218,8 +218,8 @@ class TestAuditViews(TacticalTestCase): | ||||
|         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"], 26)  # type: ignore | ||||
|         self.assertEqual(r.data["total"], 26)  # type: ignore | ||||
|         self.assertEqual(r.data["completed_count"], 12)  # type: ignore | ||||
|         self.assertEqual(r.data["total"], 12)  # type: ignore | ||||
|  | ||||
|         self.check_not_authenticated("patch", url) | ||||
|  | ||||
|   | ||||
| @@ -113,16 +113,24 @@ class PendingActions(APIView): | ||||
|             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() | ||||
|  | ||||
|         ret = { | ||||
|             "actions": PendingActionSerializer(actions, many=True).data, | ||||
|             "completed_count": PendingAction.objects.filter(status="completed").count(), | ||||
|             "total": PendingAction.objects.count(), | ||||
|             "completed_count": completed, | ||||
|             "total": total, | ||||
|         } | ||||
|         return Response(ret) | ||||
|  | ||||
|   | ||||
| @@ -1,5 +0,0 @@ | ||||
| from django.apps import AppConfig | ||||
|  | ||||
|  | ||||
| class NatsapiConfig(AppConfig): | ||||
|     name = "natsapi" | ||||
| @@ -1,36 +0,0 @@ | ||||
| from django.conf import settings | ||||
| from model_bakery import baker | ||||
|  | ||||
| from tacticalrmm.test import TacticalTestCase | ||||
|  | ||||
|  | ||||
| class TestNatsAPIViews(TacticalTestCase): | ||||
|     def setUp(self): | ||||
|         self.authenticate() | ||||
|         self.setup_coresettings() | ||||
|  | ||||
|     def test_nats_agents(self): | ||||
|         baker.make_recipe( | ||||
|             "agents.online_agent", version=settings.LATEST_AGENT_VER, _quantity=14 | ||||
|         ) | ||||
|  | ||||
|         baker.make_recipe( | ||||
|             "agents.offline_agent", version=settings.LATEST_AGENT_VER, _quantity=6 | ||||
|         ) | ||||
|         baker.make_recipe( | ||||
|             "agents.overdue_agent", version=settings.LATEST_AGENT_VER, _quantity=5 | ||||
|         ) | ||||
|  | ||||
|         url = "/natsapi/online/agents/" | ||||
|         r = self.client.get(url) | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         self.assertEqual(len(r.json()["agent_ids"]), 14) | ||||
|  | ||||
|         url = "/natsapi/offline/agents/" | ||||
|         r = self.client.get(url) | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         self.assertEqual(len(r.json()["agent_ids"]), 11) | ||||
|  | ||||
|         url = "/natsapi/asdjaksdasd/agents/" | ||||
|         r = self.client.get(url) | ||||
|         self.assertEqual(r.status_code, 400) | ||||
| @@ -1,9 +0,0 @@ | ||||
| from django.urls import path | ||||
|  | ||||
| from . import views | ||||
|  | ||||
| urlpatterns = [ | ||||
|     path("natsinfo/", views.nats_info), | ||||
|     path("<str:stat>/agents/", views.NatsAgents.as_view()), | ||||
|     path("logcrash/", views.LogCrash.as_view()), | ||||
| ] | ||||
| @@ -1,60 +0,0 @@ | ||||
| from django.conf import settings | ||||
| from django.shortcuts import get_object_or_404 | ||||
| from django.utils import timezone as djangotime | ||||
| from loguru import logger | ||||
| from rest_framework.decorators import ( | ||||
|     api_view, | ||||
|     authentication_classes, | ||||
|     permission_classes, | ||||
| ) | ||||
| from rest_framework.response import Response | ||||
| from rest_framework.views import APIView | ||||
|  | ||||
| from agents.models import Agent | ||||
| from tacticalrmm.utils import notify_error | ||||
|  | ||||
| logger.configure(**settings.LOG_CONFIG) | ||||
|  | ||||
|  | ||||
| @api_view() | ||||
| @permission_classes([]) | ||||
| @authentication_classes([]) | ||||
| def nats_info(request): | ||||
|     return Response({"user": "tacticalrmm", "password": settings.SECRET_KEY}) | ||||
|  | ||||
|  | ||||
| class NatsAgents(APIView): | ||||
|     authentication_classes = []  # type: ignore | ||||
|     permission_classes = []  # type: ignore | ||||
|  | ||||
|     def get(self, request, stat: str): | ||||
|         if stat not in ["online", "offline"]: | ||||
|             return notify_error("invalid request") | ||||
|  | ||||
|         ret: list[str] = [] | ||||
|         agents = Agent.objects.only( | ||||
|             "pk", "agent_id", "version", "last_seen", "overdue_time", "offline_time" | ||||
|         ) | ||||
|         if stat == "online": | ||||
|             ret = [i.agent_id for i in agents if i.status == "online"] | ||||
|         else: | ||||
|             ret = [i.agent_id for i in agents if i.status != "online"] | ||||
|  | ||||
|         return Response({"agent_ids": ret}) | ||||
|  | ||||
|  | ||||
| class LogCrash(APIView): | ||||
|     authentication_classes = []  # type: ignore | ||||
|     permission_classes = []  # type: ignore | ||||
|  | ||||
|     def post(self, request): | ||||
|         agent = get_object_or_404(Agent, agent_id=request.data["agentid"]) | ||||
|         agent.last_seen = djangotime.now() | ||||
|         agent.save(update_fields=["last_seen"]) | ||||
|  | ||||
|         if hasattr(settings, "DEBUGTEST") and settings.DEBUGTEST: | ||||
|             logger.info( | ||||
|                 f"Detected crashed tacticalagent service on {agent.hostname} v{agent.version}, attempting recovery" | ||||
|             ) | ||||
|  | ||||
|         return Response("ok") | ||||
| @@ -1,22 +1,21 @@ | ||||
| amqp==5.0.5 | ||||
| asgiref==3.3.1 | ||||
| asgiref==3.3.4 | ||||
| asyncio-nats-client==0.11.4 | ||||
| billiard==3.6.3.0 | ||||
| celery==5.0.5 | ||||
| certifi==2020.12.5 | ||||
| cffi==1.14.5 | ||||
| channels==3.0.3 | ||||
| chardet==4.0.0 | ||||
| cryptography==3.4.6 | ||||
| decorator==4.4.2 | ||||
| Django==3.1.7 | ||||
| cryptography==3.4.7 | ||||
| daphne==3.0.2 | ||||
| Django==3.2.0 | ||||
| django-cors-headers==3.7.0 | ||||
| django-rest-knox==4.1.0 | ||||
| djangorestframework==3.12.2 | ||||
| djangorestframework==3.12.4 | ||||
| future==0.18.2 | ||||
| kombu==5.0.2 | ||||
| loguru==0.5.3 | ||||
| msgpack==1.0.2 | ||||
| packaging==20.8 | ||||
| packaging==20.9 | ||||
| psycopg2-binary==2.8.6 | ||||
| pycparser==2.20 | ||||
| pycryptodome==3.10.1 | ||||
| @@ -28,10 +27,10 @@ redis==3.5.3 | ||||
| requests==2.25.1 | ||||
| six==1.15.0 | ||||
| sqlparse==0.4.1 | ||||
| twilio==6.52.0 | ||||
| urllib3==1.26.3 | ||||
| twilio==6.56.0 | ||||
| urllib3==1.26.4 | ||||
| uWSGI==2.0.19.1 | ||||
| validators==0.18.2 | ||||
| vine==5.0.0 | ||||
| websockets==8.1 | ||||
| zipp==3.4.0 | ||||
| zipp==3.4.1 | ||||
|   | ||||
| @@ -1,212 +1,438 @@ | ||||
| [ | ||||
|     { | ||||
|         "filename": "ClearFirefoxCache.ps1", | ||||
|         "submittedBy": "https://github.com/Omnicef", | ||||
|         "name": "Clear Firefox Cache", | ||||
|         "description": "This script will clean up Mozilla Firefox for all users.", | ||||
|         "shell": "powershell" | ||||
|     }, | ||||
|     { | ||||
|         "filename": "ClearGoogleChromeCache.ps1", | ||||
|         "submittedBy": "https://github.com/Omnicef", | ||||
|         "name": "Clear Google Chrome Cache", | ||||
|         "description": "This script will clean up Google Chrome for all users.", | ||||
|         "shell": "powershell" | ||||
|     }, | ||||
|     { | ||||
|         "filename": "InstallAdobeReader.ps1", | ||||
|         "submittedBy": "https://github.com/Omnicef", | ||||
|         "name": "Install Adobe Reader DC", | ||||
|         "description": "Installs Adobe Reader DC.", | ||||
|         "shell": "powershell" | ||||
|     }, | ||||
|     { | ||||
|         "filename": "InstallDuplicati.ps1", | ||||
|         "submittedBy": "https://github.com/Omnicef", | ||||
|         "name": "Install Duplicati", | ||||
|         "description": "This script installs Duplicati 2.0.5.1 as a service.", | ||||
|         "shell": "powershell" | ||||
|     }, | ||||
|     { | ||||
|         "filename": "Reset-WindowsUpdate.ps1", | ||||
|         "submittedBy": "https://github.com/Omnicef", | ||||
|         "name": "Reset Windows Update", | ||||
|         "description": "This script will reset all of the Windows Updates components to DEFAULT SETTINGS.", | ||||
|         "shell": "powershell" | ||||
|     }, | ||||
|     { | ||||
|         "filename": "Start-Cleanup.ps1", | ||||
|         "submittedBy": "https://github.com/Omnicef", | ||||
|         "name": "Cleanup C: drive", | ||||
|         "description": "Cleans the C: drive's Window Temperary files, Windows SoftwareDistribution folder, the local users Temperary folder, IIS logs (if applicable) and empties the recycling bin. All deleted files will go into a log transcript in $env:TEMP. By default this script leaves files that are newer than 7 days old however this variable can be edited.", | ||||
|         "shell": "powershell" | ||||
|     }, | ||||
|     { | ||||
|         "filename": "WindowsDefenderFullScanBackground.ps1", | ||||
|         "submittedBy": "https://github.com/Omnicef", | ||||
|         "name": "Windows Defender Full Scan", | ||||
|         "description": "Runs a Windows Defender Full background scan.", | ||||
|         "shell": "powershell" | ||||
|     }, | ||||
|     { | ||||
|         "filename": "WindowsDefenderQuickScanBackground.ps1", | ||||
|         "submittedBy": "https://github.com/Omnicef", | ||||
|         "name": "Windows Defender Quick Scan", | ||||
|         "description": "Runs a Quick Scan using Windows Defender in the Background.", | ||||
|         "shell": "powershell" | ||||
|     }, | ||||
|     { | ||||
|         "filename": "speedtest.py", | ||||
|         "submittedBy": "https://github.com/wh1te909", | ||||
|         "name": "Speed Test", | ||||
|         "description": "Runs a Speed Test", | ||||
|         "shell": "python" | ||||
|     }, | ||||
|     { | ||||
|         "filename": "Rename-Installed-App.ps1", | ||||
|         "submittedBy": "https://github.com/bradhawkins85", | ||||
|         "name": "Rename Tactical RMM Agent", | ||||
|         "description": "Updates the DisplayName registry entry for the Tactical RMM windows agent to your desired name. This script takes 1 required argument: the name you wish to set.", | ||||
|         "shell": "powershell" | ||||
|     }, | ||||
|     { | ||||
|         "filename": "bitlocker_encrypted_drive_c.ps1", | ||||
|         "submittedBy": "https://github.com/ThatsNASt", | ||||
|         "name": "Check C Drive for Bitlocker Status", | ||||
|         "description": "Runs a check on drive C for Bitlocker status.", | ||||
|         "shell": "powershell" | ||||
|     }, | ||||
|     { | ||||
|         "filename": "bitlocker_create_status_report.ps1", | ||||
|         "submittedBy": "https://github.com/ThatsNASt", | ||||
|         "name": "Create Bitlocker Status Report", | ||||
|         "description": "Creates a Bitlocker status report.", | ||||
|         "shell": "powershell" | ||||
|     }, | ||||
|     { | ||||
|         "filename": "bitlocker_retrieve_status_report.ps1", | ||||
|         "submittedBy": "https://github.com/ThatsNASt", | ||||
|         "name": "Retreive Bitlocker Status Report", | ||||
|         "description": "Retreives a Bitlocker status report.", | ||||
|         "shell": "powershell" | ||||
|     }, | ||||
|     { | ||||
|         "filename": "bios_check.ps1", | ||||
|         "submittedBy": "https://github.com/ThatsNASt", | ||||
|         "name": "Check BIOS Information", | ||||
|         "description": "Retreives and reports on BIOS make, version, and date   .", | ||||
|         "shell": "powershell" | ||||
|     }, | ||||
|     { | ||||
|         "filename": "ResetHighPerformancePowerProfiletoDefaults.ps1", | ||||
|         "submittedBy": "https://github.com/azulskyknight", | ||||
|         "name": "Reset High Perf Power Profile", | ||||
|         "description": "Resets monitor, disk, standby, and hibernate timers in the default High Performance power profile to their default values. It also re-indexes the AC and DC power profiles into their default order.", | ||||
|         "shell": "powershell" | ||||
|     }, | ||||
|     { | ||||
|         "filename": "SetHighPerformancePowerProfile.ps1", | ||||
|         "submittedBy": "https://github.com/azulskyknight", | ||||
|         "name": "Set High Perf Power Profile", | ||||
|         "description": "Sets the High Performance Power profile to the active power profile. Use this to keep machines from falling asleep.", | ||||
|         "shell": "powershell" | ||||
|     }, | ||||
|     { | ||||
|         "filename": "Windows10Upgrade.ps1", | ||||
|         "submittedBy": "https://github.com/RVL-Solutions and https://github.com/darimm", | ||||
|         "name": "Windows 10 Upgrade", | ||||
|         "description": "Forces an upgrade to the latest release of Windows 10.", | ||||
|         "shell": "powershell" | ||||
|     }, | ||||
|     { | ||||
|         "filename": "DiskStatus.ps1", | ||||
|         "submittedBy": "https://github.com/dinger1986", | ||||
|         "name": "Check Disks", | ||||
|         "description": "Checks local disks for errors reported in event viewer within the last 24 hours", | ||||
|         "shell": "powershell" | ||||
|     }, | ||||
|     { | ||||
|         "filename": "DuplicatiStatus.ps1", | ||||
|         "submittedBy": "https://github.com/dinger1986", | ||||
|         "name": "Check Duplicati", | ||||
|         "description": "Checks Duplicati Backup is running properly over the last 24 hours", | ||||
|         "shell": "powershell" | ||||
|     }, | ||||
|     { | ||||
|         "filename": "EnableDefender.ps1", | ||||
|         "submittedBy": "https://github.com/dinger1986", | ||||
|         "name": "Enable Windows Defender", | ||||
|         "description": "Enables Windows Defender and sets preferences", | ||||
|         "shell": "powershell" | ||||
|     }, | ||||
|     { | ||||
|         "filename": "OpenSSHServerInstall.ps1", | ||||
|         "submittedBy": "https://github.com/dinger1986", | ||||
|         "name": "Install SSH", | ||||
|         "description": "Installs and enabled OpenSSH Server", | ||||
|         "shell": "powershell" | ||||
|     }, | ||||
|     { | ||||
|         "filename": "RDP_enable.bat", | ||||
|         "submittedBy": "https://github.com/dinger1986", | ||||
|         "name": "Enable RDP", | ||||
|         "description": "Enables RDP", | ||||
|         "shell": "cmd" | ||||
|     }, | ||||
|     { | ||||
|         "filename": "Speedtest.ps1", | ||||
|         "submittedBy": "https://github.com/dinger1986", | ||||
|         "name": "PS Speed Test", | ||||
|         "description": "Powershell speed test (win 10 or server2016+)", | ||||
|         "shell": "powershell" | ||||
|     }, | ||||
|     { | ||||
|         "filename": "SyncTime.bat", | ||||
|         "submittedBy": "https://github.com/dinger1986", | ||||
|         "name": "Sync DC Time", | ||||
|         "description": "Syncs time with domain controller", | ||||
|         "shell": "cmd" | ||||
|     }, | ||||
|     { | ||||
|         "filename": "WinDefenderClearLogs.ps1", | ||||
|         "submittedBy": "https://github.com/dinger1986", | ||||
|         "name": "Clear Defender Logs", | ||||
|         "description": "Clears Windows Defender Logs", | ||||
|         "shell": "powershell" | ||||
|     }, | ||||
|     { | ||||
|         "filename": "WinDefenderStatus.ps1", | ||||
|         "submittedBy": "https://github.com/dinger1986", | ||||
|         "name": "Defender Status", | ||||
|         "description": "This will check for Malware, Antispyware, that Windows Defender is Healthy, last scan etc within the last 24 hours", | ||||
|         "shell": "powershell" | ||||
|     }, | ||||
|     { | ||||
|         "filename": "disable_FastStartup.bat", | ||||
|         "submittedBy": "https://github.com/dinger1986", | ||||
|         "name": "Disable Fast Startup", | ||||
|         "description": "Disables Faststartup on Windows 10", | ||||
|         "shell": "cmd" | ||||
|     }, | ||||
|     { | ||||
|         "filename": "updatetacticalexclusion.ps1", | ||||
|         "submittedBy": "https://github.com/dinger1986", | ||||
|         "name": "TRMM Defender Exclusions", | ||||
|         "description": "Windows Defender Exclusions for Tactical RMM", | ||||
|         "shell": "powershell" | ||||
|     }, | ||||
|     { | ||||
|         "filename": "Display_Message_To_User.ps1", | ||||
|         "submittedBy": "https://github.com/bradhawkins85", | ||||
|         "name": "Display Message To User", | ||||
|         "description": "Displays a popup message to the currently logged on user", | ||||
|         "shell": "powershell" | ||||
|     }, | ||||
|     { | ||||
|         "filename": "VerifyAntivirus.ps1", | ||||
|         "submittedBy": "https://github.com/beejayzed", | ||||
|         "name": "Verify Antivirus Status", | ||||
|         "description": "Verify and display status for all installed Antiviruses", | ||||
|         "shell": "powershell" | ||||
|     } | ||||
| ] | ||||
|   { | ||||
|     "guid": "6820cb5e-5a7f-4d9b-8c22-d54677e3cc04", | ||||
|     "filename": "Win_Clear_Firefox_Cache.ps1", | ||||
|     "submittedBy": "https://github.com/Omnicef", | ||||
|     "name": "Clear Firefox Cache", | ||||
|     "description": "This script will clean up Mozilla Firefox for all users.", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Browsers" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "3ff6a386-11d1-4f9d-8cca-1b0563bb6443", | ||||
|     "filename": "Win_Clear_Google_Chrome_Cache.ps1", | ||||
|     "submittedBy": "https://github.com/Omnicef", | ||||
|     "name": "Clear Google Chrome Cache", | ||||
|     "description": "This script will clean up Google Chrome for all users.", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Browsers" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "be1de837-f677-4ac5-aa0c-37a0fc9991fc", | ||||
|     "filename": "Win_Install_Adobe_Reader.ps1", | ||||
|     "submittedBy": "https://github.com/Omnicef", | ||||
|     "name": "Install Adobe Reader DC", | ||||
|     "description": "Installs Adobe Reader DC.", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):3rd Party Software>Chocolatey" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "2ee134d5-76aa-4160-b334-a1efbc62079f", | ||||
|     "filename": "Win_Install_Duplicati.ps1", | ||||
|     "submittedBy": "https://github.com/Omnicef", | ||||
|     "name": "Install Duplicati", | ||||
|     "description": "This script installs Duplicati 2.0.5.1 as a service.", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):3rd Party Software" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "81cc5bcb-01bf-4b0c-89b9-0ac0f3fe0c04", | ||||
|     "filename": "Win_Reset_Windows_Update.ps1", | ||||
|     "submittedBy": "https://github.com/Omnicef", | ||||
|     "name": "Reset Windows Update", | ||||
|     "description": "This script will reset all of the Windows Updates components to DEFAULT SETTINGS.", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Updates" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "8db87ff0-a9b4-4d9d-bc55-377bbcb85b6d", | ||||
|     "filename": "Win_Start_Cleanup.ps1", | ||||
|     "submittedBy": "https://github.com/Omnicef", | ||||
|     "name": "Cleanup C: drive", | ||||
|     "description": "Cleans the C: drive's Window Temperary files, Windows SoftwareDistribution folder, the local users Temperary folder, IIS logs (if applicable) and empties the recycling bin. All deleted files will go into a log transcript in $env:TEMP. By default this script leaves files that are newer than 7 days old however this variable can be edited.", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Other" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "2f28e8c1-ae0f-4b46-a826-f513974526a3", | ||||
|     "filename": "Win_Defender_FullScan_Background.ps1", | ||||
|     "submittedBy": "https://github.com/Omnicef", | ||||
|     "name": "Windows Defender Full Scan", | ||||
|     "description": "Runs a Windows Defender Full background scan.", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Security>Antivirus" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "adf81ddb-3b77-415c-a89b-2ccc826b5aa7", | ||||
|     "filename": "Win_Defender_QuickScan_Background.ps1", | ||||
|     "submittedBy": "https://github.com/Omnicef", | ||||
|     "name": "Windows Defender Quick Scan", | ||||
|     "description": "Runs a Quick Scan using Windows Defender in the Background.", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Security>Antivirus" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "3c46290b-85db-4cd2-93a2-943c8c93b3b1", | ||||
|     "filename": "Speedtest.py", | ||||
|     "submittedBy": "https://github.com/wh1te909", | ||||
|     "name": "Speed Test", | ||||
|     "description": "Runs a Speed Test using Python", | ||||
|     "shell": "python", | ||||
|     "category": "TRMM (Win):Network" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "9d34f482-1f0c-4b2f-b65f-a9cf3c13ef5f", | ||||
|     "filename": "Win_Rename_Installed_App.ps1", | ||||
|     "submittedBy": "https://github.com/bradhawkins85", | ||||
|     "name": "Rename Tactical RMM Agent", | ||||
|     "description": "Updates the DisplayName registry entry for the Tactical RMM windows agent to your desired name. This script takes 1 required argument: the name you wish to set.", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):TacticalRMM Related" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "525ae965-1dcf-4c17-92b3-5da3cf6819f5", | ||||
|     "filename": "Win_Bitlocker_Encrypted_Drive_c.ps1", | ||||
|     "submittedBy": "https://github.com/ThatsNASt", | ||||
|     "name": "Check C Drive for Bitlocker Status", | ||||
|     "description": "Runs a check on drive C for Bitlocker status.", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Storage" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "2ea35fa2-c227-4d17-a40e-4d39f252e27a", | ||||
|     "filename": "Win_Bitlocker_Create_Status_Report.ps1", | ||||
|     "submittedBy": "https://github.com/ThatsNASt", | ||||
|     "name": "Create Bitlocker Status Report", | ||||
|     "description": "Creates a Bitlocker status report.", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Storage" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "9e5769c1-3873-4941-bf70-e851e0afbd6d", | ||||
|     "filename": "Win_Bitlocker_Retrieve_Status_Report.ps1", | ||||
|     "submittedBy": "https://github.com/ThatsNASt", | ||||
|     "name": "Retreive Bitlocker Status Report", | ||||
|     "description": "Retreives a Bitlocker status report.", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Storage" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "cfa14c28-4dfc-4d4e-95ee-a380652e058d", | ||||
|     "filename": "Win_Bios_Check.ps1", | ||||
|     "submittedBy": "https://github.com/ThatsNASt", | ||||
|     "name": "Check BIOS Information", | ||||
|     "description": "Retreives and reports on BIOS make, version, and date.", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Hardware" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "95a2ee6f-b89b-4551-856e-3081b041caa7", | ||||
|     "filename": "Win_Reset_High_Performance_Power_Profile_to_Defaults.ps1", | ||||
|     "submittedBy": "https://github.com/azulskyknight", | ||||
|     "name": "Reset High Perf Power Profile", | ||||
|     "description": "Resets monitor, disk, standby, and hibernate timers in the default High Performance power profile to their default values. It also re-indexes the AC and DC power profiles into their default order.", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Power" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "2cbd30b0-84dd-4388-a36d-2e2e980f1a3e", | ||||
|     "filename": "Win_Set_High_Performance_Power_Profile.ps1", | ||||
|     "submittedBy": "https://github.com/azulskyknight", | ||||
|     "name": "Set High Perf Power Profile", | ||||
|     "description": "Sets the High Performance Power profile to the active power profile. Use this to keep machines from falling asleep.", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Power" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "553236d3-81bc-49f4-af8a-0cff925a7f6d", | ||||
|     "filename": "Win_10_Upgrade.ps1", | ||||
|     "submittedBy": "https://github.com/RVL-Solutions and https://github.com/darimm", | ||||
|     "name": "Windows 10 Upgrade", | ||||
|     "description": "Forces an upgrade to the latest release of Windows 10.", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Updates" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "375323e5-cac6-4f35-a304-bb7cef35902d", | ||||
|     "filename": "Win_Disk_Status.ps1", | ||||
|     "submittedBy": "https://github.com/dinger1986", | ||||
|     "name": "Check Disk Hardware Health (using Event Viewer errors)", | ||||
|     "description": "Checks local disks for errors reported in event viewer within the last 24 hours", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Hardware" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "7c14beb4-d1c3-41aa-8e70-92a267d6e080", | ||||
|     "filename": "Win_Duplicati_Status.ps1", | ||||
|     "submittedBy": "https://github.com/dinger1986", | ||||
|     "name": "Check Duplicati", | ||||
|     "description": "Checks Duplicati Backup is running properly over the last 24 hours", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):3rd Party Software" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "da51111c-aff6-4d87-9d76-0608e1f67fe5", | ||||
|     "filename": "Win_Defender_Enable.ps1", | ||||
|     "submittedBy": "https://github.com/dinger1986", | ||||
|     "name": "Enable Windows Defender", | ||||
|     "description": "Enables Windows Defender and sets preferences", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Security>Antivirus" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "a223d03a-e22e-40e0-94f2-92dd8c481d14", | ||||
|     "filename": "Win_Open_SSH_Server_Install.ps1", | ||||
|     "submittedBy": "https://github.com/dinger1986", | ||||
|     "name": "Install SSH", | ||||
|     "description": "Installs and enabled OpenSSH Server", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Windows Features" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "2435297a-6263-4e90-8688-1847400d0e22", | ||||
|     "filename": "Win_RDP_enable.bat", | ||||
|     "submittedBy": "https://github.com/dinger1986", | ||||
|     "name": "Enable RDP", | ||||
|     "description": "Enables RDP", | ||||
|     "shell": "cmd", | ||||
|     "category": "TRMM (Win):Windows Features" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "24f19ead-fdfe-46b4-9dcb-4cd0e12a3940", | ||||
|     "filename": "Win_Speedtest.ps1", | ||||
|     "submittedBy": "https://github.com/dinger1986", | ||||
|     "name": "Speed Test Powershell", | ||||
|     "description": "Speed Test with Powershell(win 10 or server2016+)", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Network" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "a821975c-60df-4d58-8990-6cf8a55b4ee0", | ||||
|     "filename": "Win_Sync_Time.bat", | ||||
|     "submittedBy": "https://github.com/dinger1986", | ||||
|     "name": "Sync DC Time", | ||||
|     "description": "Syncs time with domain controller", | ||||
|     "shell": "cmd", | ||||
|     "category": "TRMM (Win):Active Directory" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "b720e320-7755-4c89-9992-e1a6c43699ed", | ||||
|     "filename": "Win_Defender_Clear_Logs.ps1", | ||||
|     "submittedBy": "https://github.com/dinger1986", | ||||
|     "name": "Clear Defender Logs", | ||||
|     "description": "Clears Windows Defender Logs", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Security>Antivirus" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "d980fda3-a068-47eb-8495-1aab07a24e64", | ||||
|     "filename": "Win_Defender_Status.ps1", | ||||
|     "submittedBy": "https://github.com/dinger1986", | ||||
|     "name": "Defender Status", | ||||
|     "description": "This will check for Malware, Antispyware, that Windows Defender is Healthy, last scan etc within the last 24 hours", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Security>Antivirus" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "9956e936-6fdb-4488-a9d8-8b274658037f", | ||||
|     "filename": "Win_Disable_Fast_Startup.bat", | ||||
|     "submittedBy": "https://github.com/dinger1986", | ||||
|     "name": "Disable Fast Startup", | ||||
|     "description": "Disables Faststartup on Windows 10", | ||||
|     "shell": "cmd", | ||||
|     "category": "TRMM (Win):Power" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "2472bbaf-1941-4722-8a58-d1dd0f528801", | ||||
|     "filename": "Win_Update_Tactical_Exclusion.ps1", | ||||
|     "submittedBy": "https://github.com/dinger1986", | ||||
|     "name": "TRMM Defender Exclusions", | ||||
|     "description": "Windows Defender Exclusions for Tactical RMM", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Security>Antivirus" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "b253dc76-41a0-48ca-9cea-bee4277402c4", | ||||
|     "filename": "Win_Display_Message_To_User.ps1", | ||||
|     "submittedBy": "https://github.com/bradhawkins85", | ||||
|     "name": "Display Message To User", | ||||
|     "description": "Displays a popup message to the currently logged on user", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Other" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "19224d21-bd39-44bc-b9cf-8f1ba3ca9c11", | ||||
|     "filename": "Win_Antivirus_Verify.ps1", | ||||
|     "submittedBy": "https://github.com/beejayzed", | ||||
|     "name": "Verify Antivirus Status", | ||||
|     "description": "Verify and display status for all installed Antiviruses", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Security>Antivirus" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "f88c5c52-c6fe-44db-b727-b7912a4279ed", | ||||
|     "filename": "Win_Create_All_User_Logon_Script.ps1", | ||||
|     "submittedBy": "https://github.com/nr-plaxon", | ||||
|     "name": "Create User Logon Script", | ||||
|     "description": "Creates a powershell script that runs at logon of any user on the machine in the security context of the user.", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Other" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "5615aa90-0272-427b-8acf-0ca019612501", | ||||
|     "filename": "Win_Chocolatey_Update_Installed.bat", | ||||
|     "submittedBy": "https://github.com/silversword411", | ||||
|     "name": "Chocolatey Update Installed Apps", | ||||
|     "description": "Update all apps that were installed using Chocolatey.", | ||||
|     "shell": "cmd", | ||||
|     "category": "TRMM (Win):3rd Party Software>Chocolatey" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "fff8024d-d72e-4457-84fa-6c780f69a16f", | ||||
|     "filename": "Win_AD_Check_And_Enable_AD_Recycle_Bin.ps1", | ||||
|     "submittedBy": "https://github.com/silversword411", | ||||
|     "name": "AD - Check and Enable AD Recycle Bin", | ||||
|     "description": "Only run on Domain Controllers, checks for Active Directory Recycle Bin and enables if not already enabled", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Active Directory" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "71090fc4-faa6-460b-adb0-95d7863544e1", | ||||
|     "filename": "Win_Check_Events_for_Bluescreens.ps1", | ||||
|     "submittedBy": "https://github.com/dinger1986", | ||||
|     "name": "Event Viewer - Check for Bluescreens", | ||||
|     "description": "This will check for Bluescreen events on your system", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Monitoring" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "5d905886-9eb1-4129-8b81-a013f842eb24", | ||||
|     "filename": "Win_Rename_Computer.ps1", | ||||
|     "submittedBy": "https://github.com/silversword411", | ||||
|     "name": "Rename Computer", | ||||
|     "description": "Rename computer. First parameter will be new PC name. 2nd parameter if yes will auto-reboot machine", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Other", | ||||
|     "default_timeout": 30 | ||||
|   }, | ||||
|   { | ||||
|     "guid": "f396dae2-c768-45c5-bd6c-176e56ed3614", | ||||
|     "filename": "Win_Finish_updates_and_restart.ps1", | ||||
|     "submittedBy": "https://github.com/tremor021", | ||||
|     "name": "Finish updates and restart", | ||||
|     "description": "Finish installing updates and restart PC", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Other" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "63f89be0-a9c9-4c61-9b55-bce0b28b90b2", | ||||
|     "filename": "Win_Finish_updates_and_shutdown.ps1", | ||||
|     "submittedBy": "https://github.com/tremor021", | ||||
|     "name": "Finish updates and shutdown", | ||||
|     "description": "Finish installing updates and shutdown PC", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Other" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "e09895d5-ca13-44a2-a38c-6e77c740f0e8", | ||||
|     "filename": "Win_ScreenConnectAIO.ps1", | ||||
|     "submittedBy": "https://github.com/bradhawkins85", | ||||
|     "name": "ScreenConnect AIO", | ||||
|     "description": "Install, Uninstall, Start and Stop ScreenConnect Access Agent", | ||||
|     "args": [ | ||||
|       "-serviceName {{client.ScreenConnectService}}", | ||||
|       "-url {{client.ScreenConnectInstaller}}", | ||||
|       "-action install" | ||||
|     ], | ||||
|     "default_timeout": "90", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):3rd Party Software" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "3abbb62a-3757-492c-8979-b4fc6174845d", | ||||
|     "filename": "Win_Disable_AutoRun.bat", | ||||
|     "submittedBy": "https://github.com/silversword411", | ||||
|     "name": "Disable Autorun", | ||||
|     "description": "Disable Autorun System Wide", | ||||
|     "shell": "cmd", | ||||
|     "category": "TRMM (Win):Other", | ||||
|     "default_timeout": "30" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "4a11877a-7555-494c-ac74-29d6df3c1989", | ||||
|     "filename": "Win_Disable_Cortana.bat", | ||||
|     "submittedBy": "https://github.com/silversword411", | ||||
|     "name": "Disable Cortana", | ||||
|     "description": "Disable Cortana System Wide", | ||||
|     "shell": "cmd", | ||||
|     "category": "TRMM (Win):Other", | ||||
|     "default_timeout": "30" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "28ef1387-dd4f-4bab-b042-26250914e370", | ||||
|     "filename": "Win_WOL_Enable_Status.ps1", | ||||
|     "submittedBy": "https://github.com/silversword411", | ||||
|     "name": "WoL - Enable function", | ||||
|     "description": "Wake on Lan enable on Dell, HP, Lenovo", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Network", | ||||
|     "default_timeout": "90" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "685d5432-0b84-46d5-98e8-3ec2054150fe", | ||||
|     "filename": "Win_WOL_Test_State.ps1", | ||||
|     "submittedBy": "https://github.com/silversword411", | ||||
|     "name": "WoL - Test State", | ||||
|     "description": "Wake on Lan test status", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Network", | ||||
|     "default_timeout": "90" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "6ce5682a-49db-4c0b-9417-609cf905ac43", | ||||
|     "filename": "Win_Win10_Change_Key_and_Activate.ps1", | ||||
|     "submittedBy": "https://github.com/silversword411", | ||||
|     "name": "Change Win10 Product Key and Activate", | ||||
|     "description": "Insert new product key and Activate. Requires 1 parameter the product key you want to use", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Other", | ||||
|     "default_timeout": "90" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "83f6c6ea-6120-4fd3-bec8-d3abc505dcdf", | ||||
|     "filename": "Win_TRMM_Start_Menu_Delete_Shortcut.ps1", | ||||
|     "submittedBy": "https://github.com/silversword411", | ||||
|     "name": "TRMM Delete Start Menu Shortcut for App", | ||||
|     "description": "Tactical RMM delete its application shortcut that's installed in the start menu", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):TacticalRMM Related", | ||||
|     "default_timeout": "10" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "60130fca-7636-446e-acd7-cc5d29d609c2", | ||||
|     "filename": "Win_Firewall_Check_Status.ps1", | ||||
|     "submittedBy": "https://github.com/dinger1986", | ||||
|     "name": "Windows Firewall - Check Status", | ||||
|     "description": "Windows Firewall - Check state, report status", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Network" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "93379675-c01c-433f-87df-a11597c959f0", | ||||
|     "filename": "Win_UAC_Check_Status.ps1", | ||||
|     "submittedBy": "https://github.com/dinger1986", | ||||
|     "name": "Windows UAC - Check Status", | ||||
|     "description": "Windows UAC - Report status", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Security" | ||||
|   }, | ||||
|   { | ||||
|     "guid": "7ea6a11a-05c0-4151-b5c1-cb8af029299f", | ||||
|     "filename": "Win_AzureAD_Check_Connection_Status.ps1", | ||||
|     "submittedBy": "https://github.com/dinger1986", | ||||
|     "name": "Azure AD - Check Status", | ||||
|     "description": "Azure AD - Check if joined or not", | ||||
|     "shell": "powershell", | ||||
|     "category": "TRMM (Win):Azure>AD" | ||||
|   } | ||||
| ] | ||||
| @@ -0,0 +1,18 @@ | ||||
| # Generated by Django 3.1.7 on 2021-03-31 01:08 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('scripts', '0005_auto_20201207_1606'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='script', | ||||
|             name='default_timeout', | ||||
|             field=models.PositiveIntegerField(default=90), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										19
									
								
								api/tacticalrmm/scripts/migrations/0007_script_args.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								api/tacticalrmm/scripts/migrations/0007_script_args.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| # Generated by Django 3.1.7 on 2021-04-01 14:52 | ||||
|  | ||||
| import django.contrib.postgres.fields | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('scripts', '0006_script_default_timeout'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='script', | ||||
|             name='args', | ||||
|             field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(blank=True, null=True), blank=True, default=list, null=True, size=None), | ||||
|         ), | ||||
|     ] | ||||
| @@ -1,5 +1,9 @@ | ||||
| import base64 | ||||
|  | ||||
| import re | ||||
| from loguru import logger | ||||
| from typing import Any, List, Union | ||||
| from django.conf import settings | ||||
| from django.contrib.postgres.fields import ArrayField | ||||
| from django.db import models | ||||
|  | ||||
| from logs.models import BaseAuditModel | ||||
| @@ -15,6 +19,8 @@ SCRIPT_TYPES = [ | ||||
|     ("builtin", "Built In"), | ||||
| ] | ||||
|  | ||||
| logger.configure(**settings.LOG_CONFIG) | ||||
|  | ||||
|  | ||||
| class Script(BaseAuditModel): | ||||
|     name = models.CharField(max_length=255) | ||||
| @@ -26,9 +32,16 @@ class Script(BaseAuditModel): | ||||
|     script_type = models.CharField( | ||||
|         max_length=100, choices=SCRIPT_TYPES, default="userdefined" | ||||
|     ) | ||||
|     args = ArrayField( | ||||
|         models.TextField(null=True, blank=True), | ||||
|         null=True, | ||||
|         blank=True, | ||||
|         default=list, | ||||
|     ) | ||||
|     favorite = models.BooleanField(default=False) | ||||
|     category = models.CharField(max_length=100, null=True, blank=True) | ||||
|     code_base64 = models.TextField(null=True, blank=True) | ||||
|     default_timeout = models.PositiveIntegerField(default=90) | ||||
|  | ||||
|     def __str__(self): | ||||
|         return self.name | ||||
| @@ -68,12 +81,27 @@ class Script(BaseAuditModel): | ||||
|                 s = cls.objects.filter(script_type="builtin").filter( | ||||
|                     name=script["name"] | ||||
|                 ) | ||||
|  | ||||
|                 category = ( | ||||
|                     script["category"] if "category" in script.keys() else "Community" | ||||
|                 ) | ||||
|  | ||||
|                 default_timeout = ( | ||||
|                     int(script["default_timeout"]) | ||||
|                     if "default_timeout" in script.keys() | ||||
|                     else 90 | ||||
|                 ) | ||||
|  | ||||
|                 args = script["args"] if "args" in script.keys() else [] | ||||
|  | ||||
|                 if s.exists(): | ||||
|                     i = s.first() | ||||
|                     i.name = script["name"] | ||||
|                     i.description = script["description"] | ||||
|                     i.category = "Community" | ||||
|                     i.category = category | ||||
|                     i.shell = script["shell"] | ||||
|                     i.default_timeout = default_timeout | ||||
|                     i.args = args | ||||
|  | ||||
|                     with open(os.path.join(scripts_dir, script["filename"]), "rb") as f: | ||||
|                         script_bytes = ( | ||||
| @@ -86,8 +114,10 @@ class Script(BaseAuditModel): | ||||
|                             "name", | ||||
|                             "description", | ||||
|                             "category", | ||||
|                             "default_timeout", | ||||
|                             "code_base64", | ||||
|                             "shell", | ||||
|                             "args", | ||||
|                         ] | ||||
|                     ) | ||||
|                 else: | ||||
| @@ -106,7 +136,9 @@ class Script(BaseAuditModel): | ||||
|                             filename=script["filename"], | ||||
|                             shell=script["shell"], | ||||
|                             script_type="builtin", | ||||
|                             category="Community", | ||||
|                             category=category, | ||||
|                             default_timeout=default_timeout, | ||||
|                             args=args, | ||||
|                         ).save() | ||||
|  | ||||
|     @staticmethod | ||||
| @@ -115,3 +147,108 @@ class Script(BaseAuditModel): | ||||
|         from .serializers import ScriptSerializer | ||||
|  | ||||
|         return ScriptSerializer(script).data | ||||
|  | ||||
|     @classmethod | ||||
|     def parse_script_args( | ||||
|         cls, agent, shell: str, args: List[str] = list() | ||||
|     ) -> Union[List[str], None]: | ||||
|         from core.models import CustomField | ||||
|  | ||||
|         if not list: | ||||
|             return [] | ||||
|  | ||||
|         temp_args = list() | ||||
|  | ||||
|         # pattern to match for injection | ||||
|         pattern = re.compile(".*\\{\\{(.*)\\}\\}.*") | ||||
|  | ||||
|         for arg in args: | ||||
|             match = pattern.match(arg) | ||||
|             if match: | ||||
|                 # only get the match between the () in regex | ||||
|                 string = match.group(1) | ||||
|  | ||||
|                 # split by period if exists. First should be model and second should be property | ||||
|                 temp = string.split(".") | ||||
|  | ||||
|                 # check for model and property | ||||
|                 if len(temp) != 2: | ||||
|                     # ignore arg since it is invalid | ||||
|                     continue | ||||
|  | ||||
|                 if temp[0] == "client": | ||||
|                     model = "client" | ||||
|                     obj = agent.client | ||||
|                 elif temp[0] == "site": | ||||
|                     model = "site" | ||||
|                     obj = agent.site | ||||
|                 elif temp[0] == "agent": | ||||
|                     model = "agent" | ||||
|                     obj = agent | ||||
|                 else: | ||||
|                     # ignore arg since it is invalid | ||||
|                     continue | ||||
|  | ||||
|                 if hasattr(obj, temp[1]): | ||||
|                     value = getattr(obj, temp[1]) | ||||
|  | ||||
|                 elif CustomField.objects.filter(model=model, name=temp[1]).exists(): | ||||
|  | ||||
|                     field = CustomField.objects.get(model=model, name=temp[1]) | ||||
|                     model_fields = getattr(field, f"{model}_fields") | ||||
|                     value = None | ||||
|                     if model_fields.filter(**{model: obj}).exists(): | ||||
|                         value = model_fields.get(**{model: obj}).value | ||||
|  | ||||
|                     if not value and field.default_value: | ||||
|                         value = field.default_value | ||||
|  | ||||
|                     # check if value exists and if not use defa | ||||
|                     if value and field.type == "multiple": | ||||
|                         value = format_shell_array(shell, value) | ||||
|                     elif value and field.type == "checkbox": | ||||
|                         value = format_shell_bool(shell, value) | ||||
|  | ||||
|                     if not value: | ||||
|                         continue | ||||
|  | ||||
|                 else: | ||||
|                     # ignore arg since property is invalid | ||||
|                     continue | ||||
|  | ||||
|                 # replace the value in the arg and push to array | ||||
|                 # log any unhashable type errors | ||||
|                 try: | ||||
|                     temp_args.append(re.sub("\\{\\{.*\\}\\}", value, arg))  # type: ignore | ||||
|                 except Exception as e: | ||||
|                     logger.error(e) | ||||
|                     continue | ||||
|  | ||||
|             else: | ||||
|                 temp_args.append(arg) | ||||
|  | ||||
|         return temp_args | ||||
|  | ||||
|  | ||||
| def format_shell_array(shell: str, value: Any) -> str: | ||||
|     if shell == "cmd": | ||||
|         return "array args are not supported with batch" | ||||
|     elif shell == "powershell": | ||||
|         temp_string = "" | ||||
|         for item in value: | ||||
|             temp_string += item + "," | ||||
|         return temp_string.strip(",") | ||||
|     else:  # python | ||||
|         temp_string = "" | ||||
|         for item in value: | ||||
|             temp_string += item + "," | ||||
|         return temp_string.strip(",") | ||||
|  | ||||
|  | ||||
| def format_shell_bool(shell: str, value: Any) -> str: | ||||
|     if shell == "cmd": | ||||
|         return "1" if value else "0" | ||||
|     elif shell == "powershell": | ||||
|         return "$True" if value else "$False" | ||||
|     else:  # python | ||||
|         return "True" if value else "False" | ||||
|   | ||||
| @@ -12,8 +12,10 @@ class ScriptTableSerializer(ModelSerializer): | ||||
|             "description", | ||||
|             "script_type", | ||||
|             "shell", | ||||
|             "args", | ||||
|             "category", | ||||
|             "favorite", | ||||
|             "default_timeout", | ||||
|         ] | ||||
|  | ||||
|  | ||||
| @@ -25,9 +27,11 @@ class ScriptSerializer(ModelSerializer): | ||||
|             "name", | ||||
|             "description", | ||||
|             "shell", | ||||
|             "args", | ||||
|             "category", | ||||
|             "favorite", | ||||
|             "code_base64", | ||||
|             "default_timeout", | ||||
|         ] | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -7,8 +7,6 @@ from tacticalrmm.celery import app | ||||
|  | ||||
| @app.task | ||||
| def handle_bulk_command_task(agentpks, cmd, shell, timeout) -> None: | ||||
|     agents = Agent.objects.filter(pk__in=agentpks) | ||||
|     agents_nats = [agent for agent in agents if agent.has_nats] | ||||
|     nats_data = { | ||||
|         "func": "rawcmd", | ||||
|         "timeout": timeout, | ||||
| @@ -17,15 +15,13 @@ def handle_bulk_command_task(agentpks, cmd, shell, timeout) -> None: | ||||
|             "shell": shell, | ||||
|         }, | ||||
|     } | ||||
|     for agent in agents_nats: | ||||
|     for agent in Agent.objects.filter(pk__in=agentpks): | ||||
|         asyncio.run(agent.nats_cmd(nats_data, wait=False)) | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def handle_bulk_script_task(scriptpk, agentpks, args, timeout) -> None: | ||||
|     script = Script.objects.get(pk=scriptpk) | ||||
|     agents = Agent.objects.filter(pk__in=agentpks) | ||||
|     agents_nats = [agent for agent in agents if agent.has_nats] | ||||
|     nats_data = { | ||||
|         "func": "runscript", | ||||
|         "timeout": timeout, | ||||
| @@ -35,5 +31,5 @@ def handle_bulk_script_task(scriptpk, agentpks, args, timeout) -> None: | ||||
|             "shell": script.shell, | ||||
|         }, | ||||
|     } | ||||
|     for agent in agents_nats: | ||||
|     for agent in Agent.objects.filter(pk__in=agentpks): | ||||
|         asyncio.run(agent.nats_cmd(nats_data, wait=False)) | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user