Compare commits
	
		
			108 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 6ae2da22c1 | ||
|  | cef1ab9512 | ||
|  | 94f02bfca3 | ||
|  | a941bb1744 | ||
|  | 6ff591427a | ||
|  | 809e172280 | ||
|  | 17aedae0a9 | ||
|  | ef817ccb3a | ||
|  | 0fb55b0bee | ||
|  | a1a6eddc31 | ||
|  | ff3d0b6b57 | ||
|  | dd64cef4c4 | ||
|  | 9796848079 | ||
|  | fea7eb4312 | ||
|  | c12cd0e755 | ||
|  | d86a72f858 | ||
|  | 50cd7f219a | ||
|  | 8252b3eccc | ||
|  | d0c6e3a158 | ||
|  | 1505fa547e | ||
|  | 9017bad884 | ||
|  | 2ac5e316a5 | ||
|  | 29f9113062 | ||
|  | 46349672d8 | ||
|  | 4787be2db0 | ||
|  | f0a8c5d732 | ||
|  | 9ad520bf7c | ||
|  | bd0cc51554 | ||
|  | 12f599f974 | ||
|  | 0118d5fb40 | ||
|  | 65cadb311a | ||
|  | dd75bd197d | ||
|  | 7e155bdb43 | ||
|  | 993b6fddf4 | ||
|  | 6ba51df6a7 | ||
|  | 1185ac58e1 | ||
|  | f835997f49 | ||
|  | a597dba775 | ||
|  | 3194e83a66 | ||
|  | 096c3cdd34 | ||
|  | 3a1ea42333 | ||
|  | 64877d4299 | ||
|  | e957dc5e2c | ||
|  | 578d5c5830 | ||
|  | 96284f9508 | ||
|  | 698b38dcba | ||
|  | 6db826befe | ||
|  | 1a3d412d73 | ||
|  | b8461c9dd8 | ||
|  | 699bd9de10 | ||
|  | 54b6866e21 | ||
|  | afd155e9c1 | ||
|  | 910a717230 | ||
|  | 70fbd33d61 | ||
|  | 2da0d5ee21 | ||
|  | 98f64e057a | ||
|  | 3d9d936c56 | ||
|  | 2b4cb59df8 | ||
|  | 9d80da52e3 | ||
|  | fd176d2c64 | ||
|  | 538b6de36b | ||
|  | f7eca8aee0 | ||
|  | a754d94c2c | ||
|  | 5e3493e6a9 | ||
|  | 619a14c26b | ||
|  | 7d9a8decf0 | ||
|  | d11e14ad89 | ||
|  | 69189cf2af | ||
|  | 6e7d2f19d2 | ||
|  | d99ebf5d6a | ||
|  | ef2d19e95b | ||
|  | e3a66f017e | ||
|  | 9e544ad471 | ||
|  | 5f19aa527a | ||
|  | bfd5bc5c26 | ||
|  | 2d0ec3accd | ||
|  | 0999d98225 | ||
|  | d8dd3e133f | ||
|  | 528470c37f | ||
|  | c03cd53853 | ||
|  | b57fc8a29c | ||
|  | a04ed5c3ca | ||
|  | 3ad1df14f6 | ||
|  | d8caf12fdc | ||
|  | 5ca9d30d5f | ||
|  | a7a71b4a46 | ||
|  | 638603ac6b | ||
|  | 1d70c15027 | ||
|  | 7a5f03d672 | ||
|  | 39e97c5589 | ||
|  | 1943d8367e | ||
|  | f91c5af9a1 | ||
|  | 2be71fc877 | ||
|  | f5f5b4a8db | ||
|  | ac9cfd09ea | ||
|  | 4cfc85dbfd | ||
|  | 1f3d2f47b1 | ||
|  | 653c482ff7 | ||
|  | 4b069cc2b0 | ||
|  | c89349a43a | ||
|  | 6e92d6c62c | ||
|  | 5d3d3e9076 | ||
|  | b440c772d6 | ||
|  | 2895560b30 | ||
|  | bedcecb2e1 | ||
|  | 656ac829a4 | ||
|  | 4d83debc0e | ||
|  | 4ff5d19979 | 
							
								
								
									
										59
									
								
								.github/workflows/ci-tests.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										59
									
								
								.github/workflows/ci-tests.yml
									
									
									
									
										vendored
									
									
								
							| @@ -10,23 +10,36 @@ on: | ||||
|  | ||||
| jobs: | ||||
|   test: | ||||
|     runs-on: self-hosted | ||||
|     runs-on: ubuntu-latest | ||||
|     name: Tests | ||||
|     strategy: | ||||
|       matrix: | ||||
|         python-version: ['3.10.4'] | ||||
|  | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|       - uses: actions/checkout@v3 | ||||
|  | ||||
|       - name: Setup virtual env and install requirements | ||||
|       - uses: harmon758/postgresql-action@v1 | ||||
|         with: | ||||
|           postgresql version: '14' | ||||
|           postgresql db: 'pipeline' | ||||
|           postgresql user: 'pipeline' | ||||
|           postgresql password: 'pipeline123456' | ||||
|        | ||||
|       - name: Setup Python ${{ matrix.python-version }} | ||||
|         uses: actions/setup-python@v3 | ||||
|         with: | ||||
|           python-version: ${{ matrix.python-version }} | ||||
|  | ||||
|       - name: Install redis | ||||
|         run: | | ||||
|           sudo apt update | ||||
|           sudo apt install -y redis | ||||
|           redis-server --version | ||||
|  | ||||
|       - name: Install requirements | ||||
|         working-directory: api/tacticalrmm | ||||
|         run: | | ||||
|           sudo -u postgres psql -c 'DROP DATABASE IF EXISTS pipeline' | ||||
|           sudo -u postgres psql -c 'DROP DATABASE IF EXISTS test_pipeline' | ||||
|           sudo -u postgres psql -c 'CREATE DATABASE pipeline' | ||||
|           sudo -u postgres psql -c "SET client_encoding = 'UTF8'" pipeline | ||||
|           pwd | ||||
|           rm -rf /actions-runner/_work/trmm-actions/trmm-actions/api/env | ||||
|           cd api | ||||
|           python3.10 -m venv env | ||||
|           source env/bin/activate | ||||
|           cd tacticalrmm | ||||
|           python --version | ||||
|           SETTINGS_FILE="tacticalrmm/settings.py" | ||||
|           SETUPTOOLS_VER=$(grep "^SETUPTOOLS_VER" "$SETTINGS_FILE" | awk -F'[= "]' '{print $5}') | ||||
| @@ -38,29 +51,23 @@ jobs: | ||||
|       - name: Run django tests | ||||
|         env: | ||||
|           GHACTIONS: "yes" | ||||
|         working-directory: api/tacticalrmm | ||||
|         run: | | ||||
|           cd api/tacticalrmm | ||||
|           source ../env/bin/activate | ||||
|           rm -f .coverage coverage.lcov | ||||
|           coverage run --concurrency=multiprocessing manage.py test -v 2 --parallel | ||||
|           coverage combine | ||||
|           coverage lcov | ||||
|           pytest | ||||
|           if [ $? -ne 0 ]; then | ||||
|               exit 1 | ||||
|           fi | ||||
|  | ||||
|       - name: Codestyle black | ||||
|         working-directory: api/tacticalrmm | ||||
|         run: | | ||||
|           cd api | ||||
|           source env/bin/activate | ||||
|           black --exclude migrations/ --check tacticalrmm | ||||
|           if [ $? -ne 0 ]; then | ||||
|               exit 1 | ||||
|           fi | ||||
|  | ||||
|       - name: Coveralls | ||||
|         uses: coverallsapp/github-action@master | ||||
|       - uses: codecov/codecov-action@v2 | ||||
|         with: | ||||
|           github-token: ${{ secrets.GITHUB_TOKEN }} | ||||
|           path-to-lcov: ./api/tacticalrmm/coverage.lcov | ||||
|           base-path: ./api/tacticalrmm | ||||
|           directory: ./api/tacticalrmm | ||||
|           files: ./api/tacticalrmm/coverage.xml | ||||
|           verbose: true | ||||
|   | ||||
							
								
								
									
										27
									
								
								.github/workflows/frontend-linting.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								.github/workflows/frontend-linting.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| name: Frontend Linting and Formatting | ||||
| on: | ||||
|   push: | ||||
|     branches: [develop] | ||||
|   pull_request: | ||||
|     branches: [develop] | ||||
|  | ||||
| defaults: | ||||
|   run: | ||||
|     working-directory: web | ||||
|  | ||||
| jobs: | ||||
|   lint: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/checkout@v3 | ||||
|  | ||||
|       - uses: actions/setup-node@v3 | ||||
|         with: | ||||
|           node-version: 16 | ||||
|       - run: npm install | ||||
|  | ||||
|       - name: Run Prettier formatting | ||||
|         run: npm run format | ||||
|  | ||||
|       - name: Run ESLint | ||||
|         run: npm run lint -- --max-warnings=0 | ||||
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -53,3 +53,5 @@ nats-api.conf | ||||
| ignore/ | ||||
| coverage.lcov | ||||
| daphne.sock.lock | ||||
| .pytest_cache | ||||
| coverage.xml | ||||
|   | ||||
							
								
								
									
										23
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| { | ||||
|   "recommendations": [ | ||||
|     // frontend | ||||
|     "dbaeumer.vscode-eslint", | ||||
|     "esbenp.prettier-vscode", | ||||
|     "editorconfig.editorconfig", | ||||
|     "vue.volar", | ||||
|     "wayou.vscode-todo-highlight", | ||||
|  | ||||
|     // python | ||||
|     "matangover.mypy", | ||||
|     "ms-python.python", | ||||
|  | ||||
|     // golang | ||||
|     "golang.go" | ||||
|   ], | ||||
|   "unwantedRecommendations": [ | ||||
|     "octref.vetur", | ||||
|     "hookyqr.beautify", | ||||
|     "dbaeumer.jshint", | ||||
|     "ms-vscode.vscode-typescript-tslint-plugin" | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										155
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										155
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							| @@ -1,86 +1,75 @@ | ||||
| { | ||||
|     "python.defaultInterpreterPath": "api/tacticalrmm/env/bin/python", | ||||
|     "python.languageServer": "Pylance", | ||||
|     "python.analysis.extraPaths": [ | ||||
|         "api/tacticalrmm", | ||||
|         "api/env", | ||||
|     ], | ||||
|     "python.analysis.diagnosticSeverityOverrides": { | ||||
|         "reportUnusedImport": "error", | ||||
|         "reportDuplicateImport": "error", | ||||
|         "reportGeneralTypeIssues": "none" | ||||
|     }, | ||||
|     "python.analysis.typeCheckingMode": "basic", | ||||
|     "mypy.runUsingActiveInterpreter": true, | ||||
|     "python.linting.enabled": true, | ||||
|     "python.linting.mypyEnabled": true, | ||||
|     "python.linting.mypyArgs": [ | ||||
|         "--ignore-missing-imports", | ||||
|         "--follow-imports=silent", | ||||
|         "--show-column-numbers", | ||||
|         "--strict" | ||||
|     ], | ||||
|     "python.formatting.provider": "black", | ||||
|     "editor.formatOnSave": true, | ||||
|     "vetur.format.defaultFormatter.js": "prettier", | ||||
|     "vetur.format.defaultFormatterOptions": { | ||||
|         "prettier": { | ||||
|             "semi": true, | ||||
|             "printWidth": 120, | ||||
|             "tabWidth": 2, | ||||
|             "useTabs": false, | ||||
|             "arrowParens": "avoid", | ||||
|         } | ||||
|     }, | ||||
|     "vetur.format.options.tabSize": 2, | ||||
|     "vetur.format.options.useTabs": false, | ||||
|   "python.defaultInterpreterPath": "api/tacticalrmm/env/bin/python", | ||||
|   "python.languageServer": "Pylance", | ||||
|   "python.analysis.extraPaths": ["api/tacticalrmm", "api/env"], | ||||
|   "python.analysis.diagnosticSeverityOverrides": { | ||||
|     "reportUnusedImport": "error", | ||||
|     "reportDuplicateImport": "error", | ||||
|     "reportGeneralTypeIssues": "none" | ||||
|   }, | ||||
|   "python.analysis.typeCheckingMode": "basic", | ||||
|   "python.linting.enabled": true, | ||||
|   "python.linting.mypyEnabled": true, | ||||
|   "python.linting.mypyArgs": [ | ||||
|     "--ignore-missing-imports", | ||||
|     "--follow-imports=silent", | ||||
|     "--show-column-numbers", | ||||
|     "--strict" | ||||
|   ], | ||||
|   "python.linting.ignorePatterns": [ | ||||
|     "**/site-packages/**/*.py", | ||||
|     ".vscode/*.py", | ||||
|     "**env/**" | ||||
|   ], | ||||
|   "python.formatting.provider": "black", | ||||
|   "mypy.targets": ["api/tacticalrmm"], | ||||
|   "mypy.runUsingActiveInterpreter": true, | ||||
|   "editor.bracketPairColorization.enabled": true, | ||||
|   "editor.guides.bracketPairs": true, | ||||
|   "editor.formatOnSave": true, | ||||
|   "[vue][javascript][typescript][javascriptreact]": { | ||||
|     "editor.defaultFormatter": "esbenp.prettier-vscode", | ||||
|     "editor.codeActionsOnSave": ["source.fixAll.eslint"], | ||||
|   }, | ||||
|   "eslint.validate": ["javascript", "javascriptreact", "typescript", "vue"], | ||||
|   "typescript.tsdk": "node_modules/typescript/lib", | ||||
|   "files.watcherExclude": { | ||||
|     "files.watcherExclude": { | ||||
|         "files.watcherExclude": { | ||||
|             "**/.git/objects/**": true, | ||||
|             "**/.git/subtree-cache/**": true, | ||||
|             "**/node_modules/": true, | ||||
|             "/node_modules/**": true, | ||||
|             "**/env/": true, | ||||
|             "/env/**": true, | ||||
|             "**/__pycache__": true, | ||||
|             "/__pycache__/**": true, | ||||
|             "**/.cache": true, | ||||
|             "**/.eggs": true, | ||||
|             "**/.ipynb_checkpoints": true, | ||||
|             "**/.mypy_cache": true, | ||||
|             "**/.pytest_cache": true, | ||||
|             "**/*.egg-info": true, | ||||
|             "**/*.feather": true, | ||||
|             "**/*.parquet*": true, | ||||
|             "**/*.pyc": true, | ||||
|             "**/*.zip": true | ||||
|         }, | ||||
|       "**/.git/objects/**": true, | ||||
|       "**/.git/subtree-cache/**": true, | ||||
|       "**/node_modules/": true, | ||||
|       "/node_modules/**": true, | ||||
|       "**/env/": true, | ||||
|       "/env/**": true, | ||||
|       "**/__pycache__": true, | ||||
|       "/__pycache__/**": true, | ||||
|       "**/.cache": true, | ||||
|       "**/.eggs": true, | ||||
|       "**/.ipynb_checkpoints": true, | ||||
|       "**/.mypy_cache": true, | ||||
|       "**/.pytest_cache": true, | ||||
|       "**/*.egg-info": true, | ||||
|       "**/*.feather": true, | ||||
|       "**/*.parquet*": true, | ||||
|       "**/*.pyc": true, | ||||
|       "**/*.zip": true | ||||
|     } | ||||
|   }, | ||||
|   "go.useLanguageServer": true, | ||||
|   "[go]": { | ||||
|     "editor.codeActionsOnSave": { | ||||
|       "source.organizeImports": false | ||||
|     }, | ||||
|     "go.useLanguageServer": true, | ||||
|     "[go]": { | ||||
|         "editor.formatOnSave": true, | ||||
|         "editor.codeActionsOnSave": { | ||||
|             "source.organizeImports": false, | ||||
|         }, | ||||
|         "editor.snippetSuggestions": "none", | ||||
|     }, | ||||
|     "[go.mod]": { | ||||
|         "editor.formatOnSave": true, | ||||
|         "editor.codeActionsOnSave": { | ||||
|             "source.organizeImports": true, | ||||
|         }, | ||||
|     }, | ||||
|     "gopls": { | ||||
|         "usePlaceholders": true, | ||||
|         "completeUnimported": true, | ||||
|         "staticcheck": true, | ||||
|     }, | ||||
|     "mypy.targets": [ | ||||
|         "api/tacticalrmm" | ||||
|     ], | ||||
|     "python.linting.ignorePatterns": [ | ||||
|         "**/site-packages/**/*.py", | ||||
|         ".vscode/*.py", | ||||
|         "**env/**" | ||||
|     ] | ||||
| } | ||||
|     "editor.snippetSuggestions": "none" | ||||
|   }, | ||||
|   "[go.mod]": { | ||||
|     "editor.codeActionsOnSave": { | ||||
|       "source.organizeImports": true | ||||
|     } | ||||
|   }, | ||||
|   "gopls": { | ||||
|     "usePlaceholders": true, | ||||
|     "completeUnimported": true, | ||||
|     "staticcheck": true | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,13 +1,13 @@ | ||||
| # Tactical RMM | ||||
|  | ||||
|  | ||||
| [](https://coveralls.io/github/amidaware/tacticalrmm?branch=develop) | ||||
| [](https://codecov.io/gh/amidaware/tacticalrmm) | ||||
| [](https://github.com/python/black) | ||||
|  | ||||
| Tactical RMM is a remote monitoring & management tool, built with Django and Vue.\ | ||||
| It uses an [agent](https://github.com/amidaware/rmmagent) written in golang and integrates with [MeshCentral](https://github.com/Ylianst/MeshCentral) | ||||
|  | ||||
| # [LIVE DEMO](https://rmm.tacticalrmm.io/) | ||||
| # [LIVE DEMO](https://demo.tacticalrmm.com/) | ||||
| Demo database resets every hour. A lot of features are disabled for obvious reasons due to the nature of this app. | ||||
|  | ||||
| ### [Discord Chat](https://discord.gg/upGTkWp) | ||||
|   | ||||
| @@ -20,6 +20,7 @@ omit = | ||||
|     */urls.py | ||||
|     */tests.py | ||||
|     */test.py | ||||
|     */tests/* | ||||
|     checks/utils.py | ||||
|     */asgi.py | ||||
|     */demo_views.py | ||||
|   | ||||
| @@ -1,22 +1,23 @@ | ||||
| import uuid | ||||
|  | ||||
| from accounts.models import User | ||||
| from django.core.management.base import BaseCommand | ||||
|  | ||||
| from accounts.models import User | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     help = "Creates the installer user" | ||||
|  | ||||
|     def handle(self, *args, **kwargs): | ||||
|     def handle(self, *args, **kwargs):  # type: ignore | ||||
|         self.stdout.write("Checking if installer user has been created...") | ||||
|         if User.objects.filter(is_installer_user=True).exists(): | ||||
|             self.stdout.write("Installer user already exists") | ||||
|             return | ||||
|  | ||||
|         User.objects.create_user(  # type: ignore | ||||
|         User.objects.create_user( | ||||
|             username=uuid.uuid4().hex, | ||||
|             is_installer_user=True, | ||||
|             password=User.objects.make_random_password(60),  # type: ignore | ||||
|             password=User.objects.make_random_password(60), | ||||
|             block_dashboard_login=True, | ||||
|         ) | ||||
|         self.stdout.write("Installer user has been created") | ||||
|   | ||||
| @@ -6,7 +6,7 @@ from knox.models import AuthToken | ||||
| class Command(BaseCommand): | ||||
|     help = "Deletes all knox web tokens" | ||||
|  | ||||
|     def handle(self, *args, **kwargs): | ||||
|     def handle(self, *args, **kwargs):  # type: ignore | ||||
|         # only delete web tokens, not any generated by the installer or deployments | ||||
|         dont_delete = djangotime.now() + djangotime.timedelta(hours=23) | ||||
|         tokens = AuthToken.objects.exclude(deploytokens__isnull=False).filter( | ||||
|   | ||||
| @@ -1,9 +1,10 @@ | ||||
| import subprocess | ||||
|  | ||||
| import pyotp | ||||
| from accounts.models import User | ||||
| from django.core.management.base import BaseCommand | ||||
|  | ||||
| from accounts.models import User | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     help = "Generates barcode for Authenticator and creates totp for user" | ||||
|   | ||||
| @@ -2,9 +2,10 @@ import os | ||||
| import subprocess | ||||
|  | ||||
| import pyotp | ||||
| from accounts.models import User | ||||
| from django.core.management.base import BaseCommand | ||||
|  | ||||
| from accounts.models import User | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     help = "Reset 2fa" | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| from accounts.models import User | ||||
| from django.core.management.base import BaseCommand | ||||
|  | ||||
| from accounts.models import User | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     help = "Reset password for user" | ||||
|   | ||||
| @@ -1,30 +1,17 @@ | ||||
| from django.contrib.auth.models import AbstractUser | ||||
| from django.db import models | ||||
| from django.db.models.fields import CharField, DateTimeField | ||||
| from django.core.cache import cache | ||||
| from logs.models import BaseAuditModel | ||||
|  | ||||
| from typing import Optional | ||||
|  | ||||
| from tacticalrmm.constants import ROLE_CACHE_PREFIX | ||||
| from django.contrib.auth.models import AbstractUser | ||||
| from django.core.cache import cache | ||||
| from django.db import models | ||||
| from django.db.models.fields import CharField, DateTimeField | ||||
|  | ||||
| AGENT_DBLCLICK_CHOICES = [ | ||||
|     ("editagent", "Edit Agent"), | ||||
|     ("takecontrol", "Take Control"), | ||||
|     ("remotebg", "Remote Background"), | ||||
|     ("urlaction", "URL Action"), | ||||
| ] | ||||
|  | ||||
| AGENT_TBL_TAB_CHOICES = [ | ||||
|     ("server", "Servers"), | ||||
|     ("workstation", "Workstations"), | ||||
|     ("mixed", "Mixed"), | ||||
| ] | ||||
|  | ||||
| CLIENT_TREE_SORT_CHOICES = [ | ||||
|     ("alphafail", "Move failing clients to the top"), | ||||
|     ("alpha", "Sort alphabetically"), | ||||
| ] | ||||
| from logs.models import BaseAuditModel | ||||
| from tacticalrmm.constants import ( | ||||
|     ROLE_CACHE_PREFIX, | ||||
|     AgentDblClick, | ||||
|     AgentTableTabs, | ||||
|     ClientTreeSort, | ||||
| ) | ||||
|  | ||||
|  | ||||
| class User(AbstractUser, BaseAuditModel): | ||||
| @@ -34,7 +21,7 @@ class User(AbstractUser, BaseAuditModel): | ||||
|     dark_mode = models.BooleanField(default=True) | ||||
|     show_community_scripts = models.BooleanField(default=True) | ||||
|     agent_dblclick_action = models.CharField( | ||||
|         max_length=50, choices=AGENT_DBLCLICK_CHOICES, default="editagent" | ||||
|         max_length=50, choices=AgentDblClick.choices, default=AgentDblClick.EDIT_AGENT | ||||
|     ) | ||||
|     url_action = models.ForeignKey( | ||||
|         "core.URLAction", | ||||
| @@ -44,11 +31,11 @@ class User(AbstractUser, BaseAuditModel): | ||||
|         on_delete=models.SET_NULL, | ||||
|     ) | ||||
|     default_agent_tbl_tab = models.CharField( | ||||
|         max_length=50, choices=AGENT_TBL_TAB_CHOICES, default="server" | ||||
|         max_length=50, choices=AgentTableTabs.choices, default=AgentTableTabs.SERVER | ||||
|     ) | ||||
|     agents_per_page = models.PositiveIntegerField(default=50)  # not currently used | ||||
|     client_tree_sort = models.CharField( | ||||
|         max_length=50, choices=CLIENT_TREE_SORT_CHOICES, default="alphafail" | ||||
|         max_length=50, choices=ClientTreeSort.choices, default=ClientTreeSort.ALPHA_FAIL | ||||
|     ) | ||||
|     client_tree_splitter = models.PositiveIntegerField(default=11) | ||||
|     loading_bar_color = models.CharField(max_length=255, default="red") | ||||
|   | ||||
| @@ -1,10 +1,11 @@ | ||||
| from unittest.mock import patch | ||||
|  | ||||
| from accounts.models import APIKey, User | ||||
| from accounts.serializers import APIKeySerializer | ||||
| from django.test import override_settings | ||||
| from model_bakery import baker, seq | ||||
|  | ||||
| from accounts.models import APIKey, User | ||||
| from accounts.serializers import APIKeySerializer | ||||
| from tacticalrmm.constants import AgentDblClick, AgentTableTabs, ClientTreeSort | ||||
| from tacticalrmm.test import TacticalTestCase | ||||
|  | ||||
|  | ||||
| @@ -69,17 +70,17 @@ class TestAccounts(TacticalTestCase): | ||||
|         self.assertEqual(r.status_code, 400) | ||||
|         self.assertIn("non_field_errors", r.data.keys()) | ||||
|  | ||||
|     @override_settings(DEBUG=True) | ||||
|     @patch("pyotp.TOTP.verify") | ||||
|     def test_debug_login_view(self, mock_verify): | ||||
|         url = "/login/" | ||||
|         mock_verify.return_value = True | ||||
|     # @override_settings(DEBUG=True) | ||||
|     # @patch("pyotp.TOTP.verify") | ||||
|     # def test_debug_login_view(self, mock_verify): | ||||
|     #     url = "/login/" | ||||
|     #     mock_verify.return_value = True | ||||
|  | ||||
|         data = {"username": "bob", "password": "hunter2", "twofactor": "sekret"} | ||||
|         r = self.client.post(url, data, format="json") | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         self.assertIn("expiry", r.data.keys()) | ||||
|         self.assertIn("token", r.data.keys()) | ||||
|     #     data = {"username": "bob", "password": "hunter2", "twofactor": "sekret"} | ||||
|     #     r = self.client.post(url, data, format="json") | ||||
|     #     self.assertEqual(r.status_code, 200) | ||||
|     #     self.assertIn("expiry", r.data.keys()) | ||||
|     #     self.assertIn("token", r.data.keys()) | ||||
|  | ||||
|  | ||||
| class TestGetAddUsers(TacticalTestCase): | ||||
| @@ -283,9 +284,9 @@ class TestUserAction(TacticalTestCase): | ||||
|         data = { | ||||
|             "dark_mode": True, | ||||
|             "show_community_scripts": True, | ||||
|             "agent_dblclick_action": "editagent", | ||||
|             "default_agent_tbl_tab": "mixed", | ||||
|             "client_tree_sort": "alpha", | ||||
|             "agent_dblclick_action": AgentDblClick.EDIT_AGENT, | ||||
|             "default_agent_tbl_tab": AgentTableTabs.MIXED, | ||||
|             "client_tree_sort": ClientTreeSort.ALPHA, | ||||
|             "client_tree_splitter": 14, | ||||
|             "loading_bar_color": "green", | ||||
|             "clear_search_when_switching": False, | ||||
| @@ -338,7 +339,7 @@ class TestAPIKeyViews(TacticalTestCase): | ||||
|         resp = self.client.put(url, data, format="json") | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|         apikey = APIKey.objects.get(pk=apikey.pk) | ||||
|         self.assertEquals(apikey.name, "New Name") | ||||
|         self.assertEqual(apikey.name, "New Name") | ||||
|  | ||||
|         self.check_not_authenticated("put", url) | ||||
|  | ||||
|   | ||||
| @@ -5,13 +5,13 @@ from django.db import IntegrityError | ||||
| from django.shortcuts import get_object_or_404 | ||||
| from ipware import get_client_ip | ||||
| from knox.views import LoginView as KnoxLoginView | ||||
| from logs.models import AuditLog | ||||
| from rest_framework.authtoken.serializers import AuthTokenSerializer | ||||
| from rest_framework.permissions import AllowAny, IsAuthenticated | ||||
| from rest_framework.response import Response | ||||
| from rest_framework.views import APIView | ||||
|  | ||||
| from tacticalrmm.utils import notify_error | ||||
| from logs.models import AuditLog | ||||
| from tacticalrmm.helpers import notify_error | ||||
|  | ||||
| from .models import APIKey, Role, User | ||||
| from .permissions import AccountsPerms, APIKeyPerms, RolesPerms | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| from django.core.management.base import BaseCommand | ||||
|  | ||||
| from agents.models import Agent | ||||
| from clients.models import Client, Site | ||||
| from django.core.management.base import BaseCommand | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| import asyncio | ||||
|  | ||||
| from agents.models import Agent | ||||
| from django.core.management.base import BaseCommand | ||||
| from django.utils import timezone as djangotime | ||||
| from packaging import version as pyver | ||||
|  | ||||
| from agents.models import Agent | ||||
| from tacticalrmm.constants import AGENT_DEFER | ||||
| from tacticalrmm.utils import reload_nats | ||||
|  | ||||
|   | ||||
| @@ -1,11 +1,12 @@ | ||||
| # import datetime as dt | ||||
| import random | ||||
|  | ||||
| from agents.models import Agent | ||||
| from core.tasks import cache_db_fields_task, handle_resolved_stuff | ||||
| from django.core.management.base import BaseCommand | ||||
| from django.utils import timezone as djangotime | ||||
|  | ||||
| from agents.models import Agent | ||||
| from core.tasks import cache_db_fields_task, handle_resolved_stuff | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     help = "stuff for demo site in cron" | ||||
| @@ -23,14 +24,6 @@ class Command(BaseCommand): | ||||
|             rand = now - djangotime.timedelta(minutes=random.randint(10, 20)) | ||||
|             random_dates.append(rand) | ||||
|  | ||||
|         """ for _ in range(5): | ||||
|             rand = djangotime.now() - djangotime.timedelta(hours=random.randint(1, 10)) | ||||
|             random_dates.append(rand) | ||||
|  | ||||
|         for _ in range(5): | ||||
|             rand = djangotime.now() - djangotime.timedelta(days=random.randint(40, 90)) | ||||
|             random_dates.append(rand) """ | ||||
|  | ||||
|         agents = Agent.objects.only("last_seen") | ||||
|         for agent in agents: | ||||
|             agent.last_seen = random.choice(random_dates) | ||||
|   | ||||
| @@ -3,30 +3,48 @@ import json | ||||
| import random | ||||
| import string | ||||
|  | ||||
| from accounts.models import User | ||||
| from agents.models import Agent, AgentHistory | ||||
| from automation.models import Policy | ||||
| from autotasks.models import AutomatedTask, TaskResult | ||||
| from checks.models import Check, CheckResult, CheckHistory | ||||
| from clients.models import Client, Site | ||||
| from django.conf import settings | ||||
| from django.core.management import call_command | ||||
| from django.core.management.base import BaseCommand | ||||
| from django.utils import timezone as djangotime | ||||
|  | ||||
| from accounts.models import User | ||||
| from agents.models import Agent, AgentHistory | ||||
| from automation.models import Policy | ||||
| from autotasks.models import AutomatedTask, TaskResult | ||||
| from checks.models import Check, CheckHistory, CheckResult | ||||
| from clients.models import Client, Site | ||||
| from logs.models import AuditLog, PendingAction | ||||
| from scripts.models import Script | ||||
| from software.models import InstalledSoftware | ||||
| from winupdate.models import WinUpdate, WinUpdatePolicy | ||||
|  | ||||
| from tacticalrmm.constants import ( | ||||
|     CheckStatus, | ||||
|     CheckType, | ||||
|     EvtLogFailWhen, | ||||
|     EvtLogNames, | ||||
|     EvtLogTypes, | ||||
|     PAAction, | ||||
|     ScriptShell, | ||||
| ) | ||||
| from tacticalrmm.demo_data import ( | ||||
|     check_network_loc_aware_ps1, | ||||
|     check_storage_pool_health_ps1, | ||||
|     clear_print_spool_bat, | ||||
|     disks, | ||||
|     disks_linux_deb, | ||||
|     disks_linux_pi, | ||||
|     ping_fail_output, | ||||
|     ping_success_output, | ||||
|     restart_nla_ps1, | ||||
|     show_temp_dir_py, | ||||
|     spooler_stdout, | ||||
|     temp_dir_stdout, | ||||
|     wmi_deb, | ||||
|     wmi_pi, | ||||
| ) | ||||
| from winupdate.models import WinUpdate, WinUpdatePolicy | ||||
|  | ||||
| AGENTS_TO_GENERATE = 250 | ||||
| AGENTS_TO_GENERATE = 20 | ||||
|  | ||||
| SVCS = settings.BASE_DIR.joinpath("tacticalrmm/test_data/winsvcs.json") | ||||
| WMI_1 = settings.BASE_DIR.joinpath("tacticalrmm/test_data/wmi1.json") | ||||
| @@ -43,19 +61,19 @@ EVT_LOG_FAIL = settings.BASE_DIR.joinpath( | ||||
| class Command(BaseCommand): | ||||
|     help = "populate database with fake agents" | ||||
|  | ||||
|     def rand_string(self, length): | ||||
|     def rand_string(self, length: int) -> str: | ||||
|         chars = string.ascii_letters | ||||
|         return "".join(random.choice(chars) for _ in range(length)) | ||||
|  | ||||
|     def handle(self, *args, **kwargs): | ||||
|     def handle(self, *args, **kwargs) -> None: | ||||
|  | ||||
|         user = User.objects.first() | ||||
|         if user: | ||||
|             user.totp_key = "ABSA234234" | ||||
|             user.save(update_fields=["totp_key"]) | ||||
|  | ||||
|         Client.objects.all().delete() | ||||
|         Agent.objects.all().delete() | ||||
|         Client.objects.all().delete() | ||||
|         Check.objects.all().delete() | ||||
|         Script.objects.all().delete() | ||||
|         AutomatedTask.objects.all().delete() | ||||
| @@ -65,6 +83,9 @@ class Command(BaseCommand): | ||||
|         PendingAction.objects.all().delete() | ||||
|  | ||||
|         call_command("load_community_scripts") | ||||
|         call_command("initial_db_setup") | ||||
|         call_command("load_chocos") | ||||
|         call_command("create_installer_user") | ||||
|  | ||||
|         # policies | ||||
|         check_policy = Policy() | ||||
| @@ -95,27 +116,27 @@ class Command(BaseCommand): | ||||
|         update_policy.email_if_fail = True | ||||
|         update_policy.save() | ||||
|  | ||||
|         clients = [ | ||||
|         clients = ( | ||||
|             "Company 1", | ||||
|             "Company 2", | ||||
|             "Company 3", | ||||
|             "Company 1", | ||||
|             "Company 4", | ||||
|             "Company 5", | ||||
|             "Company 6", | ||||
|         ] | ||||
|         sites1 = ["HQ1", "LA Office 1", "NY Office 1"] | ||||
|         sites2 = ["HQ2", "LA Office 2", "NY Office 2"] | ||||
|         sites3 = ["HQ3", "LA Office 3", "NY Office 3"] | ||||
|         sites4 = ["HQ4", "LA Office 4", "NY Office 4"] | ||||
|         sites5 = ["HQ5", "LA Office 5", "NY Office 5"] | ||||
|         sites6 = ["HQ6", "LA Office 6", "NY Office 6"] | ||||
|         ) | ||||
|         sites1 = ("HQ1", "LA Office 1", "NY Office 1") | ||||
|         sites2 = ("HQ2", "LA Office 2", "NY Office 2") | ||||
|         sites3 = ("HQ3", "LA Office 3", "NY Office 3") | ||||
|         sites4 = ("HQ4", "LA Office 4", "NY Office 4") | ||||
|         sites5 = ("HQ5", "LA Office 5", "NY Office 5") | ||||
|         sites6 = ("HQ6", "LA Office 6", "NY Office 6") | ||||
|  | ||||
|         client1 = Client(name="Company 1") | ||||
|         client2 = Client(name="Company 2") | ||||
|         client3 = Client(name="Company 3") | ||||
|         client4 = Client(name="Company 4") | ||||
|         client5 = Client(name="Company 5") | ||||
|         client6 = Client(name="Company 6") | ||||
|         client1 = Client(name=clients[0]) | ||||
|         client2 = Client(name=clients[1]) | ||||
|         client3 = Client(name=clients[2]) | ||||
|         client4 = Client(name=clients[3]) | ||||
|         client5 = Client(name=clients[4]) | ||||
|         client6 = Client(name=clients[5]) | ||||
|  | ||||
|         client1.save() | ||||
|         client2.save() | ||||
| @@ -142,7 +163,7 @@ class Command(BaseCommand): | ||||
|         for site in sites6: | ||||
|             Site(client=client6, name=site).save() | ||||
|  | ||||
|         hostnames = [ | ||||
|         hostnames = ( | ||||
|             "DC-1", | ||||
|             "DC-2", | ||||
|             "FSV-1", | ||||
| @@ -150,26 +171,30 @@ class Command(BaseCommand): | ||||
|             "WSUS", | ||||
|             "DESKTOP-12345", | ||||
|             "LAPTOP-55443", | ||||
|         ] | ||||
|         descriptions = ["Bob's computer", "Primary DC", "File Server", "Karen's Laptop"] | ||||
|         modes = ["server", "workstation"] | ||||
|         op_systems_servers = [ | ||||
|         ) | ||||
|         descriptions = ("Bob's computer", "Primary DC", "File Server", "Karen's Laptop") | ||||
|         modes = ("server", "workstation") | ||||
|         op_systems_servers = ( | ||||
|             "Microsoft Windows Server 2016 Standard, 64bit (build 14393)", | ||||
|             "Microsoft Windows Server 2012 R2 Standard, 64bit (build 9600)", | ||||
|             "Microsoft Windows Server 2019 Standard, 64bit (build 17763)", | ||||
|         ] | ||||
|         ) | ||||
|  | ||||
|         op_systems_workstations = [ | ||||
|         op_systems_workstations = ( | ||||
|             "Microsoft Windows 8.1 Pro, 64bit (build 9600)", | ||||
|             "Microsoft Windows 10 Pro for Workstations, 64bit (build 18363)", | ||||
|             "Microsoft Windows 10 Pro, 64bit (build 18363)", | ||||
|         ] | ||||
|         ) | ||||
|  | ||||
|         public_ips = ["65.234.22.4", "74.123.43.5", "44.21.134.45"] | ||||
|         linux_deb_os = "Debian 11.2 x86_64 5.10.0-11-amd64" | ||||
|         linux_pi_os = "Raspbian 11.2 armv7l 5.10.92-v7+" | ||||
|  | ||||
|         total_rams = [4, 8, 16, 32, 64, 128] | ||||
|         public_ips = ("65.234.22.4", "74.123.43.5", "44.21.134.45") | ||||
|  | ||||
|         total_rams = (4, 8, 16, 32, 64, 128) | ||||
|  | ||||
|         now = dt.datetime.now() | ||||
|         django_now = djangotime.now() | ||||
|  | ||||
|         boot_times = [] | ||||
|  | ||||
| @@ -181,7 +206,7 @@ class Command(BaseCommand): | ||||
|             rand_days = now - dt.timedelta(days=random.randint(2, 50)) | ||||
|             boot_times.append(str(rand_days.timestamp())) | ||||
|  | ||||
|         user_names = ["None", "Karen", "Steve", "jsmith", "jdoe"] | ||||
|         user_names = ("None", "Karen", "Steve", "jsmith", "jdoe") | ||||
|  | ||||
|         with open(SVCS) as f: | ||||
|             services = json.load(f) | ||||
| @@ -196,10 +221,7 @@ class Command(BaseCommand): | ||||
|         with open(WMI_3) as f: | ||||
|             wmi3 = json.load(f) | ||||
|  | ||||
|         wmi_details = [] | ||||
|         wmi_details.append(wmi1) | ||||
|         wmi_details.append(wmi2) | ||||
|         wmi_details.append(wmi3) | ||||
|         wmi_details = [i for i in (wmi1, wmi2, wmi3)] | ||||
|  | ||||
|         # software | ||||
|         with open(SW_1) as f: | ||||
| @@ -208,9 +230,7 @@ class Command(BaseCommand): | ||||
|         with open(SW_2) as f: | ||||
|             software2 = json.load(f) | ||||
|  | ||||
|         softwares = [] | ||||
|         softwares.append(software1) | ||||
|         softwares.append(software2) | ||||
|         softwares = [i for i in (software1, software2)] | ||||
|  | ||||
|         # windows updates | ||||
|         with open(WIN_UPDATES) as f: | ||||
| @@ -226,74 +246,97 @@ class Command(BaseCommand): | ||||
|         clear_spool.name = "Clear Print Spooler" | ||||
|         clear_spool.description = "clears the print spooler. Fuck printers" | ||||
|         clear_spool.filename = "clear_print_spool.bat" | ||||
|         clear_spool.shell = "cmd" | ||||
|         clear_spool.shell = ScriptShell.CMD | ||||
|         clear_spool.script_body = clear_print_spool_bat | ||||
|         clear_spool.save() | ||||
|  | ||||
|         check_net_aware = Script() | ||||
|         check_net_aware.name = "Check Network Location Awareness" | ||||
|         check_net_aware.description = "Check's network location awareness on domain computers, should always be domain profile and not public or private. Sometimes happens when computer restarts before domain available. This script will return 0 if check passes or 1 if it fails." | ||||
|         check_net_aware.filename = "check_network_loc_aware.ps1" | ||||
|         check_net_aware.shell = "powershell" | ||||
|         check_net_aware.shell = ScriptShell.POWERSHELL | ||||
|         check_net_aware.script_body = check_network_loc_aware_ps1 | ||||
|         check_net_aware.save() | ||||
|  | ||||
|         check_pool_health = Script() | ||||
|         check_pool_health.name = "Check storage spool health" | ||||
|         check_pool_health.description = "loops through all storage pools and will fail if any of them are not healthy" | ||||
|         check_pool_health.filename = "check_storage_pool_health.ps1" | ||||
|         check_pool_health.shell = "powershell" | ||||
|         check_pool_health.shell = ScriptShell.POWERSHELL | ||||
|         check_pool_health.script_body = check_storage_pool_health_ps1 | ||||
|         check_pool_health.save() | ||||
|  | ||||
|         restart_nla = Script() | ||||
|         restart_nla.name = "Restart NLA Service" | ||||
|         restart_nla.description = "restarts the Network Location Awareness windows service to fix the nic profile. Run this after the check network service fails" | ||||
|         restart_nla.filename = "restart_nla.ps1" | ||||
|         restart_nla.shell = "powershell" | ||||
|         restart_nla.shell = ScriptShell.POWERSHELL | ||||
|         restart_nla.script_body = restart_nla_ps1 | ||||
|         restart_nla.save() | ||||
|  | ||||
|         show_tmp_dir_script = Script() | ||||
|         show_tmp_dir_script.name = "Check temp dir" | ||||
|         show_tmp_dir_script.description = "shows files in temp dir using python" | ||||
|         show_tmp_dir_script.filename = "show_temp_dir.py" | ||||
|         show_tmp_dir_script.shell = "python" | ||||
|         show_tmp_dir_script.shell = ScriptShell.PYTHON | ||||
|         show_tmp_dir_script.script_body = show_temp_dir_py | ||||
|         show_tmp_dir_script.save() | ||||
|  | ||||
|         for count_agents in range(AGENTS_TO_GENERATE): | ||||
|  | ||||
|             client = random.choice(clients) | ||||
|  | ||||
|             if client == "Company 1": | ||||
|             if client == clients[0]: | ||||
|                 site = random.choice(sites1) | ||||
|             elif client == "Company 2": | ||||
|             elif client == clients[1]: | ||||
|                 site = random.choice(sites2) | ||||
|             elif client == "Company 3": | ||||
|             elif client == clients[2]: | ||||
|                 site = random.choice(sites3) | ||||
|             elif client == "Company 4": | ||||
|             elif client == clients[3]: | ||||
|                 site = random.choice(sites4) | ||||
|             elif client == "Company 5": | ||||
|             elif client == clients[4]: | ||||
|                 site = random.choice(sites5) | ||||
|             elif client == "Company 6": | ||||
|             elif client == clients[5]: | ||||
|                 site = random.choice(sites6) | ||||
|             else: | ||||
|                 site = None | ||||
|  | ||||
|             agent = Agent() | ||||
|  | ||||
|             mode = random.choice(modes) | ||||
|             if mode == "server": | ||||
|                 agent.operating_system = random.choice(op_systems_servers) | ||||
|             plat_pick = random.randint(1, 15) | ||||
|             if plat_pick in (7, 11): | ||||
|                 agent.plat = "linux" | ||||
|                 mode = "server" | ||||
|                 # pi arm | ||||
|                 if plat_pick == 7: | ||||
|                     agent.goarch = "arm" | ||||
|                     agent.wmi_detail = wmi_pi | ||||
|                     agent.disks = disks_linux_pi | ||||
|                     agent.operating_system = linux_pi_os | ||||
|                 else: | ||||
|                     agent.goarch = "amd64" | ||||
|                     agent.wmi_detail = wmi_deb | ||||
|                     agent.disks = disks_linux_deb | ||||
|                     agent.operating_system = linux_deb_os | ||||
|             else: | ||||
|                 agent.operating_system = random.choice(op_systems_workstations) | ||||
|                 agent.plat = "windows" | ||||
|                 agent.goarch = "amd64" | ||||
|                 mode = random.choice(modes) | ||||
|                 agent.wmi_detail = random.choice(wmi_details) | ||||
|                 agent.services = services | ||||
|                 agent.disks = random.choice(disks) | ||||
|                 if mode == "server": | ||||
|                     agent.operating_system = random.choice(op_systems_servers) | ||||
|                 else: | ||||
|                     agent.operating_system = random.choice(op_systems_workstations) | ||||
|  | ||||
|             agent.hostname = random.choice(hostnames) | ||||
|             agent.version = settings.LATEST_AGENT_VER | ||||
|             agent.site = Site.objects.get(name=site) | ||||
|             agent.agent_id = self.rand_string(25) | ||||
|             agent.agent_id = self.rand_string(40) | ||||
|             agent.description = random.choice(descriptions) | ||||
|             agent.monitoring_type = mode | ||||
|             agent.public_ip = random.choice(public_ips) | ||||
|             agent.last_seen = djangotime.now() | ||||
|             agent.plat = "windows" | ||||
|             agent.plat_release = "windows-2019Server" | ||||
|             agent.last_seen = django_now | ||||
|  | ||||
|             agent.total_ram = random.choice(total_rams) | ||||
|             agent.boot_time = random.choice(boot_times) | ||||
|             agent.logged_in_username = random.choice(user_names) | ||||
| @@ -303,35 +346,31 @@ class Command(BaseCommand): | ||||
|             agent.overdue_email_alert = random.choice([True, False]) | ||||
|             agent.overdue_text_alert = random.choice([True, False]) | ||||
|             agent.needs_reboot = random.choice([True, False]) | ||||
|             agent.wmi_detail = random.choice(wmi_details) | ||||
|             agent.services = services | ||||
|             agent.disks = random.choice(disks) | ||||
|  | ||||
|             agent.save() | ||||
|  | ||||
|             InstalledSoftware(agent=agent, software=random.choice(softwares)).save() | ||||
|             if agent.plat == "windows": | ||||
|                 InstalledSoftware(agent=agent, software=random.choice(softwares)).save() | ||||
|  | ||||
|             if mode == "workstation": | ||||
|                 WinUpdatePolicy(agent=agent, run_time_days=[5, 6]).save() | ||||
|             else: | ||||
|                 WinUpdatePolicy(agent=agent).save() | ||||
|  | ||||
|             # windows updates load | ||||
|             guids = [] | ||||
|             for k in windows_updates.keys(): | ||||
|                 guids.append(k) | ||||
|  | ||||
|             for i in guids: | ||||
|                 WinUpdate( | ||||
|                     agent=agent, | ||||
|                     guid=i, | ||||
|                     kb=windows_updates[i]["KBs"][0], | ||||
|                     title=windows_updates[i]["Title"], | ||||
|                     installed=windows_updates[i]["Installed"], | ||||
|                     downloaded=windows_updates[i]["Downloaded"], | ||||
|                     description=windows_updates[i]["Description"], | ||||
|                     severity=windows_updates[i]["Severity"], | ||||
|                 ).save() | ||||
|             if agent.plat == "windows": | ||||
|                 # windows updates load | ||||
|                 guids = [i for i in windows_updates.keys()] | ||||
|                 for i in guids: | ||||
|                     WinUpdate( | ||||
|                         agent=agent, | ||||
|                         guid=i, | ||||
|                         kb=windows_updates[i]["KBs"][0], | ||||
|                         title=windows_updates[i]["Title"], | ||||
|                         installed=windows_updates[i]["Installed"], | ||||
|                         downloaded=windows_updates[i]["Downloaded"], | ||||
|                         description=windows_updates[i]["Description"], | ||||
|                         severity=windows_updates[i]["Severity"], | ||||
|                     ).save() | ||||
|  | ||||
|             # agent histories | ||||
|             hist = AgentHistory() | ||||
| @@ -355,50 +394,57 @@ class Command(BaseCommand): | ||||
|             } | ||||
|             hist1.save() | ||||
|  | ||||
|             # disk space check | ||||
|             check1 = Check() | ||||
|             check_result1 = CheckResult(assigned_check=check1, agent=agent) | ||||
|             check1.agent = agent | ||||
|             check1.check_type = "diskspace" | ||||
|             check_result1.status = "passing" | ||||
|             check_result1.last_run = djangotime.now() | ||||
|             check_result1.more_info = "Total: 498.7GB, Free: 287.4GB" | ||||
|             check_result1.save() | ||||
|             if agent.plat == "windows": | ||||
|                 # disk space check | ||||
|                 check1 = Check() | ||||
|                 check1.agent = agent | ||||
|                 check1.check_type = CheckType.DISK_SPACE | ||||
|                 check1.warning_threshold = 25 | ||||
|                 check1.error_threshold = 10 | ||||
|                 check1.disk = "C:" | ||||
|                 check1.email_alert = random.choice([True, False]) | ||||
|                 check1.text_alert = random.choice([True, False]) | ||||
|                 check1.save() | ||||
|  | ||||
|             check1.warning_threshold = 25 | ||||
|             check1.error_threshold = 10 | ||||
|             check1.disk = "C:" | ||||
|             check1.email_alert = random.choice([True, False]) | ||||
|             check1.text_alert = random.choice([True, False]) | ||||
|             check1.save() | ||||
|                 check_result1 = CheckResult() | ||||
|                 check_result1.agent = agent | ||||
|                 check_result1.assigned_check = check1 | ||||
|                 check_result1.status = CheckStatus.PASSING | ||||
|                 check_result1.last_run = django_now | ||||
|                 check_result1.more_info = "Total: 498.7GB, Free: 287.4GB" | ||||
|                 check_result1.save() | ||||
|  | ||||
|             for i in range(30): | ||||
|                 check1_history = CheckHistory() | ||||
|                 check1_history.check_id = check1.pk | ||||
|                 check1_history.agent_id = agent.agent_id | ||||
|                 check1_history.x = djangotime.now() - djangotime.timedelta( | ||||
|                     minutes=i * 2 | ||||
|                 ) | ||||
|                 check1_history.y = random.randint(13, 40) | ||||
|                 check1_history.save() | ||||
|                 for i in range(30): | ||||
|                     check1_history = CheckHistory() | ||||
|                     check1_history.check_id = check1.pk | ||||
|                     check1_history.agent_id = agent.agent_id | ||||
|                     check1_history.x = django_now - djangotime.timedelta(minutes=i * 2) | ||||
|                     check1_history.y = random.randint(13, 40) | ||||
|                     check1_history.save() | ||||
|  | ||||
|             # ping check | ||||
|             check2 = Check() | ||||
|             check_result2 = CheckResult(assigned_check=check2, agent=agent) | ||||
|             check_result2 = CheckResult() | ||||
|  | ||||
|             check2.agent = agent | ||||
|             check2.check_type = "ping" | ||||
|             check_result2.last_run = djangotime.now() | ||||
|             check2.check_type = CheckType.PING | ||||
|  | ||||
|             check2.email_alert = random.choice([True, False]) | ||||
|             check2.text_alert = random.choice([True, False]) | ||||
|  | ||||
|             check_result2.agent = agent | ||||
|             check_result2.assigned_check = check2 | ||||
|             check_result2.last_run = django_now | ||||
|  | ||||
|             if site in sites5: | ||||
|                 check2.name = "Synology NAS" | ||||
|                 check_result2.status = "failing" | ||||
|                 check2.alert_severity = "error" | ||||
|                 check_result2.status = CheckStatus.FAILING | ||||
|                 check2.ip = "172.17.14.26" | ||||
|                 check_result2.more_info = ping_fail_output | ||||
|             else: | ||||
|                 check2.name = "Google" | ||||
|                 check_result2.status = "passing" | ||||
|                 check_result2.status = CheckStatus.PASSING | ||||
|                 check2.ip = "8.8.8.8" | ||||
|                 check_result2.more_info = ping_success_output | ||||
|  | ||||
| @@ -409,9 +455,7 @@ class Command(BaseCommand): | ||||
|                 check2_history = CheckHistory() | ||||
|                 check2_history.check_id = check2.pk | ||||
|                 check2_history.agent_id = agent.agent_id | ||||
|                 check2_history.x = djangotime.now() - djangotime.timedelta( | ||||
|                     minutes=i * 2 | ||||
|                 ) | ||||
|                 check2_history.x = django_now - djangotime.timedelta(minutes=i * 2) | ||||
|                 if site in sites5: | ||||
|                     check2_history.y = 1 | ||||
|                     check2_history.results = ping_fail_output | ||||
| @@ -422,13 +466,19 @@ class Command(BaseCommand): | ||||
|  | ||||
|             # cpu load check | ||||
|             check3 = Check() | ||||
|             check_result3 = CheckResult(assigned_check=check3, agent=agent) | ||||
|             check3.agent = agent | ||||
|             check3.check_type = "cpuload" | ||||
|             check_result3.status = "passing" | ||||
|             check_result3.last_run = djangotime.now() | ||||
|             check3.check_type = CheckType.CPU_LOAD | ||||
|             check3.warning_threshold = 70 | ||||
|             check3.error_threshold = 90 | ||||
|             check3.email_alert = random.choice([True, False]) | ||||
|             check3.text_alert = random.choice([True, False]) | ||||
|             check3.save() | ||||
|  | ||||
|             check_result3 = CheckResult() | ||||
|             check_result3.agent = agent | ||||
|             check_result3.assigned_check = check3 | ||||
|             check_result3.status = CheckStatus.PASSING | ||||
|             check_result3.last_run = django_now | ||||
|             check_result3.history = [ | ||||
|                 15, | ||||
|                 23, | ||||
| @@ -445,68 +495,69 @@ class Command(BaseCommand): | ||||
|                 13, | ||||
|                 34, | ||||
|             ] | ||||
|             check3.email_alert = random.choice([True, False]) | ||||
|             check3.text_alert = random.choice([True, False]) | ||||
|             check3.save() | ||||
|             check_result3.save() | ||||
|  | ||||
|             for i in range(30): | ||||
|                 check3_history = CheckHistory() | ||||
|                 check3_history.check_id = check3.pk | ||||
|                 check3_history.agent_id = agent.agent_id | ||||
|                 check3_history.x = djangotime.now() - djangotime.timedelta( | ||||
|                     minutes=i * 2 | ||||
|                 ) | ||||
|                 check3_history.x = django_now - djangotime.timedelta(minutes=i * 2) | ||||
|                 check3_history.y = random.randint(2, 79) | ||||
|                 check3_history.save() | ||||
|  | ||||
|             # memory check | ||||
|             check4 = Check() | ||||
|             check_result4 = CheckResult(assigned_check=check4, agent=agent) | ||||
|             check4.agent = agent | ||||
|             check4.check_type = "memory" | ||||
|             check_result4.status = "passing" | ||||
|             check4.check_type = CheckType.MEMORY | ||||
|             check4.warning_threshold = 70 | ||||
|             check4.error_threshold = 85 | ||||
|             check_result4.history = [34, 34, 35, 36, 34, 34, 34, 34, 34, 34] | ||||
|             check4.email_alert = random.choice([True, False]) | ||||
|             check4.text_alert = random.choice([True, False]) | ||||
|             check4.save() | ||||
|  | ||||
|             check_result4 = CheckResult() | ||||
|             check_result4.agent = agent | ||||
|             check_result4.assigned_check = check4 | ||||
|             check_result4.status = CheckStatus.PASSING | ||||
|             check_result4.last_run = django_now | ||||
|             check_result4.history = [34, 34, 35, 36, 34, 34, 34, 34, 34, 34] | ||||
|             check_result4.save() | ||||
|  | ||||
|             for i in range(30): | ||||
|                 check4_history = CheckHistory() | ||||
|                 check4_history.check_id = check4.pk | ||||
|                 check4_history.agent_id = agent.agent_id | ||||
|                 check4_history.x = djangotime.now() - djangotime.timedelta( | ||||
|                     minutes=i * 2 | ||||
|                 ) | ||||
|                 check4_history.x = django_now - djangotime.timedelta(minutes=i * 2) | ||||
|                 check4_history.y = random.randint(2, 79) | ||||
|                 check4_history.save() | ||||
|  | ||||
|             # script check storage pool | ||||
|             check5 = Check() | ||||
|             check_result5 = CheckResult(assigned_check=check5, agent=agent) | ||||
|  | ||||
|             check5.agent = agent | ||||
|             check5.check_type = "script" | ||||
|             check_result5.status = "passing" | ||||
|             check_result5.last_run = djangotime.now() | ||||
|             check5.check_type = CheckType.SCRIPT | ||||
|  | ||||
|             check5.email_alert = random.choice([True, False]) | ||||
|             check5.text_alert = random.choice([True, False]) | ||||
|             check5.timeout = 120 | ||||
|             check_result5.retcode = 0 | ||||
|             check_result5.execution_time = "4.0000" | ||||
|  | ||||
|             check5.script = check_pool_health | ||||
|             check5.save() | ||||
|  | ||||
|             check_result5 = CheckResult() | ||||
|             check_result5.agent = agent | ||||
|             check_result5.assigned_check = check5 | ||||
|             check_result5.status = CheckStatus.PASSING | ||||
|             check_result5.last_run = django_now | ||||
|             check_result5.retcode = 0 | ||||
|             check_result5.execution_time = "4.0000" | ||||
|             check_result5.save() | ||||
|  | ||||
|             for i in range(30): | ||||
|                 check5_history = CheckHistory() | ||||
|                 check5_history.check_id = check5.pk | ||||
|                 check5_history.agent_id = agent.agent_id | ||||
|                 check5_history.x = djangotime.now() - djangotime.timedelta( | ||||
|                     minutes=i * 2 | ||||
|                 ) | ||||
|                 check5_history.x = django_now - djangotime.timedelta(minutes=i * 2) | ||||
|                 if i == 10 or i == 18: | ||||
|                     check5_history.y = 1 | ||||
|                 else: | ||||
| @@ -514,32 +565,34 @@ class Command(BaseCommand): | ||||
|                 check5_history.save() | ||||
|  | ||||
|             check6 = Check() | ||||
|             check_result6 = CheckResult(assigned_check=check6, agent=agent) | ||||
|  | ||||
|             check6.agent = agent | ||||
|             check6.check_type = "script" | ||||
|             check_result6.status = "passing" | ||||
|             check_result6.last_run = djangotime.now() | ||||
|             check6.check_type = CheckType.SCRIPT | ||||
|             check6.email_alert = random.choice([True, False]) | ||||
|             check6.text_alert = random.choice([True, False]) | ||||
|             check6.timeout = 120 | ||||
|             check_result6.retcode = 0 | ||||
|             check_result6.execution_time = "4.0000" | ||||
|             check6.script = check_net_aware | ||||
|             check6.save() | ||||
|  | ||||
|             check_result6 = CheckResult() | ||||
|             check_result6.agent = agent | ||||
|             check_result6.assigned_check = check6 | ||||
|             check_result6.status = CheckStatus.PASSING | ||||
|             check_result6.last_run = django_now | ||||
|             check_result6.retcode = 0 | ||||
|             check_result6.execution_time = "4.0000" | ||||
|             check_result6.save() | ||||
|  | ||||
|             for i in range(30): | ||||
|                 check6_history = CheckHistory() | ||||
|                 check6_history.check_id = check6.pk | ||||
|                 check6_history.agent_id = agent.agent_id | ||||
|                 check6_history.x = djangotime.now() - djangotime.timedelta( | ||||
|                     minutes=i * 2 | ||||
|                 ) | ||||
|                 check6_history.x = django_now - djangotime.timedelta(minutes=i * 2) | ||||
|                 check6_history.y = 0 | ||||
|                 check6_history.save() | ||||
|  | ||||
|             nla_task = AutomatedTask() | ||||
|             nla_task_result = TaskResult(task=nla_task, agent=agent) | ||||
|  | ||||
|             nla_task.agent = agent | ||||
|             actions = [ | ||||
|                 { | ||||
| @@ -554,16 +607,20 @@ class Command(BaseCommand): | ||||
|             nla_task.assigned_check = check6 | ||||
|             nla_task.name = "Restart NLA" | ||||
|             nla_task.task_type = "checkfailure" | ||||
|             nla_task.save() | ||||
|  | ||||
|             nla_task_result = TaskResult() | ||||
|             nla_task_result.task = nla_task | ||||
|             nla_task_result.agent = agent | ||||
|             nla_task_result.execution_time = "1.8443" | ||||
|             nla_task_result.last_run = djangotime.now() | ||||
|             nla_task_result.last_run = django_now | ||||
|             nla_task_result.stdout = "no stdout" | ||||
|             nla_task_result.retcode = 0 | ||||
|             nla_task_result.sync_status = "synced" | ||||
|             nla_task.save() | ||||
|             nla_task_result.save() | ||||
|  | ||||
|             spool_task = AutomatedTask() | ||||
|             spool_task_result = TaskResult(task=spool_task, agent=agent) | ||||
|  | ||||
|             spool_task.agent = agent | ||||
|             actions = [ | ||||
|                 { | ||||
| @@ -577,24 +634,25 @@ class Command(BaseCommand): | ||||
|             spool_task.actions = actions | ||||
|             spool_task.name = "Clear the print spooler" | ||||
|             spool_task.task_type = "daily" | ||||
|             spool_task.run_time_date = djangotime.now() + djangotime.timedelta( | ||||
|                 minutes=10 | ||||
|             ) | ||||
|             spool_task.expire_date = djangotime.now() + djangotime.timedelta(days=753) | ||||
|             spool_task.run_time_date = django_now + djangotime.timedelta(minutes=10) | ||||
|             spool_task.expire_date = django_now + djangotime.timedelta(days=753) | ||||
|             spool_task.daily_interval = 1 | ||||
|             spool_task.weekly_interval = 1 | ||||
|             spool_task.task_repetition_duration = "2h" | ||||
|             spool_task.task_repetition_interval = "25m" | ||||
|             spool_task.random_task_delay = "3m" | ||||
|             spool_task_result.last_run = djangotime.now() | ||||
|             spool_task.save() | ||||
|  | ||||
|             spool_task_result = TaskResult() | ||||
|             spool_task_result.task = spool_task | ||||
|             spool_task_result.agent = agent | ||||
|             spool_task_result.last_run = django_now | ||||
|             spool_task_result.retcode = 0 | ||||
|             spool_task_result.stdout = spooler_stdout | ||||
|             spool_task_result.sync_status = "synced" | ||||
|             spool_task.save() | ||||
|             spool_task_result.save() | ||||
|  | ||||
|             tmp_dir_task = AutomatedTask() | ||||
|             tmp_dir_task_result = TaskResult(task=tmp_dir_task, agent=agent) | ||||
|             tmp_dir_task.agent = agent | ||||
|             tmp_dir_task.name = "show temp dir files" | ||||
|             actions = [ | ||||
| @@ -608,138 +666,147 @@ class Command(BaseCommand): | ||||
|             ] | ||||
|             tmp_dir_task.actions = actions | ||||
|             tmp_dir_task.task_type = "manual" | ||||
|             tmp_dir_task_result.last_run = djangotime.now() | ||||
|             tmp_dir_task.save() | ||||
|  | ||||
|             tmp_dir_task_result = TaskResult() | ||||
|             tmp_dir_task_result.task = tmp_dir_task | ||||
|             tmp_dir_task_result.agent = agent | ||||
|             tmp_dir_task_result.last_run = django_now | ||||
|             tmp_dir_task_result.stdout = temp_dir_stdout | ||||
|             tmp_dir_task_result.retcode = 0 | ||||
|             tmp_dir_task_result.sync_status = "synced" | ||||
|             tmp_dir_task.save() | ||||
|             tmp_dir_task_result.save() | ||||
|  | ||||
|             check7 = Check() | ||||
|             check_result7 = CheckResult(assigned_check=check7, agent=agent) | ||||
|  | ||||
|             check7.agent = agent | ||||
|             check7.check_type = "script" | ||||
|             check_result7.status = "passing" | ||||
|             check_result7.last_run = djangotime.now() | ||||
|             check7.check_type = CheckType.SCRIPT | ||||
|  | ||||
|             check7.email_alert = random.choice([True, False]) | ||||
|             check7.text_alert = random.choice([True, False]) | ||||
|             check7.timeout = 120 | ||||
|  | ||||
|             check7.script = clear_spool | ||||
|  | ||||
|             check7.save() | ||||
|  | ||||
|             check_result7 = CheckResult() | ||||
|             check_result7.assigned_check = check7 | ||||
|             check_result7.agent = agent | ||||
|             check_result7.status = CheckStatus.PASSING | ||||
|             check_result7.last_run = django_now | ||||
|             check_result7.retcode = 0 | ||||
|             check_result7.execution_time = "3.1337" | ||||
|             check7.script = clear_spool | ||||
|             check_result7.stdout = spooler_stdout | ||||
|             check7.save() | ||||
|             check_result7.save() | ||||
|  | ||||
|             for i in range(30): | ||||
|                 check7_history = CheckHistory() | ||||
|                 check7_history.check_id = check7.pk | ||||
|                 check7_history.agent_id = agent.agent_id | ||||
|                 check7_history.x = djangotime.now() - djangotime.timedelta( | ||||
|                     minutes=i * 2 | ||||
|                 ) | ||||
|                 check7_history.x = django_now - djangotime.timedelta(minutes=i * 2) | ||||
|                 check7_history.y = 0 | ||||
|                 check7_history.save() | ||||
|  | ||||
|             check8 = Check() | ||||
|             check_result8 = CheckResult(assigned_check=check8, agent=agent) | ||||
|             check8.agent = agent | ||||
|             check8.check_type = "winsvc" | ||||
|             check_result8.status = "passing" | ||||
|             check_result8.last_run = djangotime.now() | ||||
|             check8.email_alert = random.choice([True, False]) | ||||
|             check8.text_alert = random.choice([True, False]) | ||||
|             check_result8.more_info = "Status RUNNING" | ||||
|             check8.fails_b4_alert = 4 | ||||
|             check8.svc_name = "Spooler" | ||||
|             check8.svc_display_name = "Print Spooler" | ||||
|             check8.pass_if_start_pending = False | ||||
|             check8.restart_if_stopped = True | ||||
|             check8.save() | ||||
|             check_result8.save() | ||||
|             if agent.plat == "windows": | ||||
|                 check8 = Check() | ||||
|                 check8.agent = agent | ||||
|                 check8.check_type = CheckType.WINSVC | ||||
|                 check8.email_alert = random.choice([True, False]) | ||||
|                 check8.text_alert = random.choice([True, False]) | ||||
|                 check8.fails_b4_alert = 4 | ||||
|                 check8.svc_name = "Spooler" | ||||
|                 check8.svc_display_name = "Print Spooler" | ||||
|                 check8.pass_if_start_pending = False | ||||
|                 check8.restart_if_stopped = True | ||||
|                 check8.save() | ||||
|  | ||||
|             for i in range(30): | ||||
|                 check8_history = CheckHistory() | ||||
|                 check8_history.check_id = check8.pk | ||||
|                 check8_history.agent_id = agent.agent_id | ||||
|                 check8_history.x = djangotime.now() - djangotime.timedelta( | ||||
|                     minutes=i * 2 | ||||
|                 ) | ||||
|                 if i == 10 or i == 18: | ||||
|                     check8_history.y = 1 | ||||
|                     check8_history.results = "Status STOPPED" | ||||
|                 check_result8 = CheckResult() | ||||
|                 check_result8.assigned_check = check8 | ||||
|                 check_result8.agent = agent | ||||
|                 check_result8.status = CheckStatus.PASSING | ||||
|                 check_result8.last_run = django_now | ||||
|                 check_result8.more_info = "Status RUNNING" | ||||
|                 check_result8.save() | ||||
|  | ||||
|                 for i in range(30): | ||||
|                     check8_history = CheckHistory() | ||||
|                     check8_history.check_id = check8.pk | ||||
|                     check8_history.agent_id = agent.agent_id | ||||
|                     check8_history.x = django_now - djangotime.timedelta(minutes=i * 2) | ||||
|                     if i == 10 or i == 18: | ||||
|                         check8_history.y = 1 | ||||
|                         check8_history.results = "Status STOPPED" | ||||
|                     else: | ||||
|                         check8_history.y = 0 | ||||
|                         check8_history.results = "Status RUNNING" | ||||
|                     check8_history.save() | ||||
|  | ||||
|                 check9 = Check() | ||||
|                 check9.agent = agent | ||||
|                 check9.check_type = CheckType.EVENT_LOG | ||||
|                 check9.name = "unexpected shutdown" | ||||
|                 check9.email_alert = random.choice([True, False]) | ||||
|                 check9.text_alert = random.choice([True, False]) | ||||
|                 check9.fails_b4_alert = 2 | ||||
|                 check9.log_name = EvtLogNames.APPLICATION | ||||
|                 check9.event_id = 1001 | ||||
|                 check9.event_type = EvtLogTypes.INFO | ||||
|                 check9.fail_when = EvtLogFailWhen.CONTAINS | ||||
|                 check9.search_last_days = 30 | ||||
|  | ||||
|                 check_result9 = CheckResult() | ||||
|                 check_result9.agent = agent | ||||
|                 check_result9.assigned_check = check9 | ||||
|  | ||||
|                 check_result9.last_run = django_now | ||||
|                 if site in sites5: | ||||
|                     check_result9.extra_details = eventlog_check_fail_data | ||||
|                     check_result9.status = CheckStatus.FAILING | ||||
|                 else: | ||||
|                     check8_history.y = 0 | ||||
|                     check8_history.results = "Status RUNNING" | ||||
|                 check8_history.save() | ||||
|                     check_result9.extra_details = {"log": []} | ||||
|                     check_result9.status = CheckStatus.PASSING | ||||
|  | ||||
|             check9 = Check() | ||||
|             check_result9 = CheckResult(assigned_check=check9, agent=agent) | ||||
|             check9.agent = agent | ||||
|             check9.check_type = "eventlog" | ||||
|             check9.name = "unexpected shutdown" | ||||
|                 check9.save() | ||||
|                 check_result9.save() | ||||
|  | ||||
|             check_result9.last_run = djangotime.now() | ||||
|             check9.email_alert = random.choice([True, False]) | ||||
|             check9.text_alert = random.choice([True, False]) | ||||
|             check9.fails_b4_alert = 2 | ||||
|                 for i in range(30): | ||||
|                     check9_history = CheckHistory() | ||||
|                     check9_history.check_id = check9.pk | ||||
|                     check9_history.agent_id = agent.agent_id | ||||
|                     check9_history.x = django_now - djangotime.timedelta(minutes=i * 2) | ||||
|                     if i == 10 or i == 18: | ||||
|                         check9_history.y = 1 | ||||
|                         check9_history.results = "Events Found: 16" | ||||
|                     else: | ||||
|                         check9_history.y = 0 | ||||
|                         check9_history.results = "Events Found: 0" | ||||
|                     check9_history.save() | ||||
|  | ||||
|             if site in sites5: | ||||
|                 check_result9.extra_details = eventlog_check_fail_data | ||||
|                 check_result9.status = "failing" | ||||
|             else: | ||||
|                 check_result9.extra_details = {"log": []} | ||||
|                 check_result9.status = "passing" | ||||
|                 pick = random.randint(1, 10) | ||||
|  | ||||
|             check9.log_name = "Application" | ||||
|             check9.event_id = 1001 | ||||
|             check9.event_type = "INFO" | ||||
|             check9.fail_when = "contains" | ||||
|             check9.search_last_days = 30 | ||||
|                 if pick == 5 or pick == 3: | ||||
|  | ||||
|             check9.save() | ||||
|             check_result9.save() | ||||
|                     reboot_time = django_now + djangotime.timedelta( | ||||
|                         minutes=random.randint(1000, 500000) | ||||
|                     ) | ||||
|                     date_obj = dt.datetime.strftime(reboot_time, "%Y-%m-%d %H:%M") | ||||
|  | ||||
|             for i in range(30): | ||||
|                 check9_history = CheckHistory() | ||||
|                 check9_history.check_id = check9.pk | ||||
|                 check9_history.agent_id = agent.agent_id | ||||
|                 check9_history.x = djangotime.now() - djangotime.timedelta( | ||||
|                     minutes=i * 2 | ||||
|                 ) | ||||
|                 if i == 10 or i == 18: | ||||
|                     check9_history.y = 1 | ||||
|                     check9_history.results = "Events Found: 16" | ||||
|                 else: | ||||
|                     check9_history.y = 0 | ||||
|                     check9_history.results = "Events Found: 0" | ||||
|                 check9_history.save() | ||||
|                     obj = dt.datetime.strptime(date_obj, "%Y-%m-%d %H:%M") | ||||
|  | ||||
|             pick = random.randint(1, 10) | ||||
|                     task_name = "TacticalRMM_SchedReboot_" + "".join( | ||||
|                         random.choice(string.ascii_letters) for _ in range(10) | ||||
|                     ) | ||||
|  | ||||
|             if pick == 5 or pick == 3: | ||||
|  | ||||
|                 reboot_time = djangotime.now() + djangotime.timedelta( | ||||
|                     minutes=random.randint(1000, 500000) | ||||
|                 ) | ||||
|                 date_obj = dt.datetime.strftime(reboot_time, "%Y-%m-%d %H:%M") | ||||
|  | ||||
|                 obj = dt.datetime.strptime(date_obj, "%Y-%m-%d %H:%M") | ||||
|  | ||||
|                 task_name = "TacticalRMM_SchedReboot_" + "".join( | ||||
|                     random.choice(string.ascii_letters) for _ in range(10) | ||||
|                 ) | ||||
|  | ||||
|                 sched_reboot = PendingAction() | ||||
|                 sched_reboot.agent = agent | ||||
|                 sched_reboot.action_type = "schedreboot" | ||||
|                 sched_reboot.details = { | ||||
|                     "time": str(obj), | ||||
|                     "taskname": task_name, | ||||
|                 } | ||||
|                 sched_reboot.save() | ||||
|                     sched_reboot = PendingAction() | ||||
|                     sched_reboot.agent = agent | ||||
|                     sched_reboot.action_type = PAAction.SCHED_REBOOT | ||||
|                     sched_reboot.details = { | ||||
|                         "time": str(obj), | ||||
|                         "taskname": task_name, | ||||
|                     } | ||||
|                     sched_reboot.save() | ||||
|  | ||||
|             self.stdout.write(self.style.SUCCESS(f"Added agent # {count_agents + 1}")) | ||||
|  | ||||
|         call_command("load_demo_scripts") | ||||
|         self.stdout.write("done") | ||||
|   | ||||
| @@ -1,7 +1,8 @@ | ||||
| from agents.models import Agent | ||||
| from django.conf import settings | ||||
| from django.core.management.base import BaseCommand | ||||
|  | ||||
| from agents.models import Agent | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     help = "Shows online agents that are not on the latest version" | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| from agents.models import Agent | ||||
| from agents.tasks import send_agent_update_task | ||||
| from core.utils import get_core_settings | ||||
| from django.conf import settings | ||||
| from django.core.management.base import BaseCommand | ||||
| from packaging import version as pyver | ||||
|  | ||||
| from agents.models import Agent | ||||
| from agents.tasks import send_agent_update_task | ||||
| from core.utils import get_core_settings | ||||
| from tacticalrmm.constants import AGENT_DEFER | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| # Generated by Django 4.0.3 on 2022-04-07 17:28 | ||||
|  | ||||
| from django.db import migrations, models | ||||
| import django.db.models.deletion | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|   | ||||
| @@ -0,0 +1,17 @@ | ||||
| # Generated by Django 4.0.4 on 2022-04-25 06:51 | ||||
|  | ||||
| from django.db import migrations | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('agents', '0049_agent_agents_agen_monitor_df8816_idx'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.RemoveField( | ||||
|             model_name='agent', | ||||
|             name='plat_release', | ||||
|         ), | ||||
|     ] | ||||
| @@ -2,29 +2,29 @@ import asyncio | ||||
| import re | ||||
| from collections import Counter | ||||
| from distutils.version import LooseVersion | ||||
| from typing import Any, Optional, List, Dict, Union, Sequence, cast, TYPE_CHECKING | ||||
| from django.core.cache import cache | ||||
| from packaging import version as pyver | ||||
| from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Union, cast | ||||
|  | ||||
| import msgpack | ||||
| import nats | ||||
| import validators | ||||
| from asgiref.sync import sync_to_async | ||||
| from core.models import TZ_CHOICES | ||||
| from django.conf import settings | ||||
| from django.contrib.postgres.fields import ArrayField | ||||
| from django.core.cache import cache | ||||
| from django.db import models | ||||
| from django.utils import timezone as djangotime | ||||
| from logs.models import BaseAuditModel, DebugLog | ||||
| from nats.errors import TimeoutError | ||||
| from packaging import version as pyver | ||||
|  | ||||
| from core.utils import get_core_settings | ||||
| from core.models import TZ_CHOICES | ||||
| from core.utils import get_core_settings, send_command_with_mesh | ||||
| from logs.models import BaseAuditModel, DebugLog | ||||
| from tacticalrmm.constants import ONLINE_AGENTS, CheckStatus, CheckType, DebugLogType | ||||
| from tacticalrmm.models import PermissionQuerySet | ||||
| from tacticalrmm.constants import ONLINE_AGENTS | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|     from alerts.models import Alert, AlertTemplate | ||||
|     from automation.models import Policy | ||||
|     from alerts.models import AlertTemplate, Alert | ||||
|     from autotasks.models import AutomatedTask | ||||
|     from checks.models import Check | ||||
|     from clients.models import Client | ||||
| @@ -46,7 +46,6 @@ class Agent(BaseAuditModel): | ||||
|     operating_system = models.CharField(null=True, blank=True, max_length=255) | ||||
|     plat = models.CharField(max_length=255, default="windows") | ||||
|     goarch = models.CharField(max_length=255, null=True, blank=True) | ||||
|     plat_release = models.CharField(max_length=255, null=True, blank=True) | ||||
|     hostname = models.CharField(max_length=255) | ||||
|     agent_id = models.CharField(max_length=200, unique=True) | ||||
|     last_seen = models.DateTimeField(null=True, blank=True) | ||||
| @@ -168,16 +167,22 @@ class Agent(BaseAuditModel): | ||||
|             if ( | ||||
|                 not hasattr(check.check_result, "status") | ||||
|                 or isinstance(check.check_result, CheckResult) | ||||
|                 and check.check_result.status == "passing" | ||||
|                 and check.check_result.status == CheckStatus.PASSING | ||||
|             ): | ||||
|                 passing += 1 | ||||
|             elif ( | ||||
|                 isinstance(check.check_result, CheckResult) | ||||
|                 and check.check_result.status == "failing" | ||||
|                 and check.check_result.status == CheckStatus.FAILING | ||||
|             ): | ||||
|                 alert_severity = ( | ||||
|                     check.check_result.alert_severity | ||||
|                     if check.check_type in ["memory", "cpuload", "diskspace", "script"] | ||||
|                     if check.check_type | ||||
|                     in [ | ||||
|                         CheckType.MEMORY, | ||||
|                         CheckType.CPU_LOAD, | ||||
|                         CheckType.DISK_SPACE, | ||||
|                         CheckType.SCRIPT, | ||||
|                     ] | ||||
|                     else check.alert_severity | ||||
|                 ) | ||||
|                 if alert_severity == "error": | ||||
| @@ -718,7 +723,7 @@ class Agent(BaseAuditModel): | ||||
|             return tasks | ||||
|  | ||||
|     def _do_nats_debug(self, agent: "Agent", message: str) -> None: | ||||
|         DebugLog.error(agent=agent, log_type="agent_issues", message=message) | ||||
|         DebugLog.error(agent=agent, log_type=DebugLogType.AGENT_ISSUES, message=message) | ||||
|  | ||||
|     async def nats_cmd( | ||||
|         self, data: Dict[Any, Any], timeout: int = 30, wait: bool = True | ||||
| @@ -759,6 +764,38 @@ class Agent(BaseAuditModel): | ||||
|             await nc.flush() | ||||
|             await nc.close() | ||||
|  | ||||
|     def recover(self, mode: str, mesh_uri: str, wait: bool = True) -> tuple[str, bool]: | ||||
|         """ | ||||
|         Return type: tuple(message: str, error: bool) | ||||
|         """ | ||||
|         if mode == "tacagent": | ||||
|             if self.is_posix: | ||||
|                 cmd = "systemctl restart tacticalagent.service" | ||||
|                 shell = 3 | ||||
|             else: | ||||
|                 cmd = "net stop tacticalrmm & taskkill /F /IM tacticalrmm.exe & net start tacticalrmm" | ||||
|                 shell = 1 | ||||
|  | ||||
|             asyncio.run( | ||||
|                 send_command_with_mesh(cmd, mesh_uri, self.mesh_node_id, shell, 0) | ||||
|             ) | ||||
|             return ("ok", False) | ||||
|  | ||||
|         elif mode == "mesh": | ||||
|             data = {"func": "recover", "payload": {"mode": mode}} | ||||
|             if wait: | ||||
|                 r = asyncio.run(self.nats_cmd(data, timeout=20)) | ||||
|                 if r == "ok": | ||||
|                     return ("ok", False) | ||||
|                 else: | ||||
|                     return (str(r), True) | ||||
|             else: | ||||
|                 asyncio.run(self.nats_cmd(data, timeout=20, wait=False)) | ||||
|  | ||||
|             return ("ok", False) | ||||
|  | ||||
|         return ("invalid", True) | ||||
|  | ||||
|     @staticmethod | ||||
|     def serialize(agent: "Agent") -> Dict[str, Any]: | ||||
|         # serializes the agent and returns json | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import pytz | ||||
| from rest_framework import serializers | ||||
|  | ||||
| from winupdate.serializers import WinUpdatePolicySerializer | ||||
|  | ||||
| from .models import Agent, AgentCustomField, AgentHistory, Note | ||||
|   | ||||
| @@ -4,16 +4,17 @@ import random | ||||
| from time import sleep | ||||
| from typing import Optional | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.utils import timezone as djangotime | ||||
| from packaging import version as pyver | ||||
|  | ||||
| from agents.models import Agent | ||||
| from agents.utils import get_agent_url | ||||
| from core.utils import get_core_settings | ||||
| from django.conf import settings | ||||
| from django.utils import timezone as djangotime | ||||
| from logs.models import DebugLog, PendingAction | ||||
| from packaging import version as pyver | ||||
| from scripts.models import Script | ||||
|  | ||||
| from tacticalrmm.celery import app | ||||
| from tacticalrmm.constants import CheckStatus, DebugLogType, PAAction, PAStatus | ||||
|  | ||||
|  | ||||
| def agent_update(agent_id: str, force: bool = False) -> str: | ||||
| @@ -27,7 +28,7 @@ def agent_update(agent_id: str, force: bool = False) -> str: | ||||
|     if agent.arch is None: | ||||
|         DebugLog.warning( | ||||
|             agent=agent, | ||||
|             log_type="agent_issues", | ||||
|             log_type=DebugLogType.AGENT_ISSUES, | ||||
|             message=f"Unable to determine arch on {agent.hostname}({agent.agent_id}). Skipping agent update.", | ||||
|         ) | ||||
|         return "noarch" | ||||
| @@ -38,15 +39,15 @@ def agent_update(agent_id: str, force: bool = False) -> str: | ||||
|  | ||||
|     if not force: | ||||
|         if agent.pendingactions.filter( | ||||
|             action_type="agentupdate", status="pending" | ||||
|             action_type=PAAction.AGENT_UPDATE, status=PAStatus.PENDING | ||||
|         ).exists(): | ||||
|             agent.pendingactions.filter( | ||||
|                 action_type="agentupdate", status="pending" | ||||
|                 action_type=PAAction.AGENT_UPDATE, status=PAStatus.PENDING | ||||
|             ).delete() | ||||
|  | ||||
|         PendingAction.objects.create( | ||||
|             agent=agent, | ||||
|             action_type="agentupdate", | ||||
|             action_type=PAAction.AGENT_UPDATE, | ||||
|             details={ | ||||
|                 "url": url, | ||||
|                 "version": version, | ||||
| @@ -68,7 +69,7 @@ def agent_update(agent_id: str, force: bool = False) -> str: | ||||
|  | ||||
| @app.task | ||||
| def force_code_sign(agent_ids: list[str]) -> None: | ||||
|     chunks = (agent_ids[i : i + 50] for i in range(0, len(agent_ids), 50)) | ||||
|     chunks = (agent_ids[i : i + 70] for i in range(0, len(agent_ids), 70)) | ||||
|     for chunk in chunks: | ||||
|         for agent_id in chunk: | ||||
|             agent_update(agent_id=agent_id, force=True) | ||||
| @@ -77,7 +78,7 @@ def force_code_sign(agent_ids: list[str]) -> None: | ||||
|  | ||||
| @app.task | ||||
| def send_agent_update_task(agent_ids: list[str]) -> None: | ||||
|     chunks = (agent_ids[i : i + 50] for i in range(0, len(agent_ids), 50)) | ||||
|     chunks = (agent_ids[i : i + 70] for i in range(0, len(agent_ids), 70)) | ||||
|     for chunk in chunks: | ||||
|         for agent_id in chunk: | ||||
|             agent_update(agent_id) | ||||
| @@ -97,7 +98,7 @@ def auto_self_agent_update_task() -> None: | ||||
|         if pyver.parse(i.version) < pyver.parse(settings.LATEST_AGENT_VER) | ||||
|     ] | ||||
|  | ||||
|     chunks = (agent_ids[i : i + 30] for i in range(0, len(agent_ids), 30)) | ||||
|     chunks = (agent_ids[i : i + 70] for i in range(0, len(agent_ids), 70)) | ||||
|     for chunk in chunks: | ||||
|         for agent_id in chunk: | ||||
|             agent_update(agent_id) | ||||
| @@ -235,7 +236,7 @@ def run_script_email_results_task( | ||||
|     if r == "timeout": | ||||
|         DebugLog.error( | ||||
|             agent=agent, | ||||
|             log_type="scripting", | ||||
|             log_type=DebugLogType.SCRIPTING, | ||||
|             message=f"{agent.hostname}({agent.pk}) timed out running script.", | ||||
|         ) | ||||
|         return | ||||
| @@ -289,7 +290,7 @@ def clear_faults_task(older_than_days: int) -> None: | ||||
|         for check in agent.get_checks_with_policies(): | ||||
|             # reset check status | ||||
|             if check.check_result: | ||||
|                 check.check_result.status = "passing" | ||||
|                 check.check_result.status = CheckStatus.PASSING | ||||
|                 check.check_result.save(update_fields=["status"]) | ||||
|             if check.alert.filter(agent=agent, resolved=False).exists(): | ||||
|                 alert = Alert.create_or_return_check_alert(check, agent=agent) | ||||
|   | ||||
							
								
								
									
										0
									
								
								api/tacticalrmm/agents/tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								api/tacticalrmm/agents/tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -1,28 +1,28 @@ | ||||
| import json | ||||
| import os | ||||
| from itertools import cycle | ||||
| from unittest.mock import patch | ||||
| import pytz | ||||
| from typing import TYPE_CHECKING | ||||
| from unittest.mock import patch | ||||
| 
 | ||||
| import pytz | ||||
| from django.conf import settings | ||||
| from django.test import modify_settings | ||||
| from django.utils import timezone as djangotime | ||||
| from logs.models import PendingAction | ||||
| from model_bakery import baker | ||||
| from packaging import version as pyver | ||||
| from winupdate.models import WinUpdatePolicy | ||||
| from winupdate.serializers import WinUpdatePolicySerializer | ||||
| 
 | ||||
| from tacticalrmm.test import TacticalTestCase | ||||
| 
 | ||||
| from .models import Agent, AgentCustomField, AgentHistory, Note | ||||
| from .serializers import ( | ||||
| from agents.models import Agent, AgentCustomField, AgentHistory, Note | ||||
| from agents.serializers import ( | ||||
|     AgentHistorySerializer, | ||||
|     AgentHostnameSerializer, | ||||
|     AgentNoteSerializer, | ||||
|     AgentSerializer, | ||||
| ) | ||||
| from .tasks import auto_self_agent_update_task | ||||
| from agents.tasks import auto_self_agent_update_task | ||||
| from logs.models import PendingAction | ||||
| from tacticalrmm.constants import EvtLogNames, PAAction, PAStatus | ||||
| from tacticalrmm.test import TacticalTestCase | ||||
| from winupdate.models import WinUpdatePolicy | ||||
| from winupdate.serializers import WinUpdatePolicySerializer | ||||
| 
 | ||||
| if TYPE_CHECKING: | ||||
|     from clients.models import Client, Site | ||||
| @@ -30,11 +30,6 @@ if TYPE_CHECKING: | ||||
| base_url = "/agents" | ||||
| 
 | ||||
| 
 | ||||
| @modify_settings( | ||||
|     MIDDLEWARE={ | ||||
|         "remove": "tacticalrmm.middleware.LinuxMiddleware", | ||||
|     } | ||||
| ) | ||||
| class TestAgentsList(TacticalTestCase): | ||||
|     def setUp(self) -> None: | ||||
|         self.authenticate() | ||||
| @@ -100,11 +95,6 @@ class TestAgentsList(TacticalTestCase): | ||||
|         self.check_not_authenticated("get", url) | ||||
| 
 | ||||
| 
 | ||||
| @modify_settings( | ||||
|     MIDDLEWARE={ | ||||
|         "remove": "tacticalrmm.middleware.LinuxMiddleware", | ||||
|     } | ||||
| ) | ||||
| class TestAgentViews(TacticalTestCase): | ||||
|     def setUp(self): | ||||
|         self.authenticate() | ||||
| @@ -245,7 +235,10 @@ class TestAgentViews(TacticalTestCase): | ||||
| 
 | ||||
|     def test_get_agent_versions(self): | ||||
|         url = "/agents/versions/" | ||||
|         r = self.client.get(url) | ||||
| 
 | ||||
|         with self.assertNumQueries(1): | ||||
|             r = self.client.get(url) | ||||
| 
 | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         assert any(i["hostname"] == self.agent.hostname for i in r.json()["agents"]) | ||||
| 
 | ||||
| @@ -371,7 +364,7 @@ class TestAgentViews(TacticalTestCase): | ||||
|                 "func": "eventlog", | ||||
|                 "timeout": 30, | ||||
|                 "payload": { | ||||
|                     "logname": "Application", | ||||
|                     "logname": EvtLogNames.APPLICATION, | ||||
|                     "days": str(22), | ||||
|                 }, | ||||
|             }, | ||||
| @@ -386,7 +379,7 @@ class TestAgentViews(TacticalTestCase): | ||||
|                 "func": "eventlog", | ||||
|                 "timeout": 180, | ||||
|                 "payload": { | ||||
|                     "logname": "Security", | ||||
|                     "logname": EvtLogNames.SECURITY, | ||||
|                     "days": str(6), | ||||
|                 }, | ||||
|             }, | ||||
| @@ -576,10 +569,9 @@ class TestAgentViews(TacticalTestCase): | ||||
|     @patch("agents.tasks.run_script_email_results_task.delay") | ||||
|     @patch("agents.models.Agent.run_script") | ||||
|     def test_run_script(self, run_script, email_task): | ||||
|         from agents.models import AgentCustomField, AgentHistory, Note | ||||
|         from clients.models import ClientCustomField, SiteCustomField | ||||
| 
 | ||||
|         from .models import AgentCustomField, AgentHistory, Note | ||||
| 
 | ||||
|         run_script.return_value = "ok" | ||||
|         url = f"/agents/{self.agent.agent_id}/runscript/" | ||||
|         script = baker.make_recipe("scripts.script") | ||||
| @@ -883,11 +875,6 @@ class TestAgentViews(TacticalTestCase): | ||||
|         self.assertEqual(r.data, data)  # type:ignore | ||||
| 
 | ||||
| 
 | ||||
| @modify_settings( | ||||
|     MIDDLEWARE={ | ||||
|         "remove": "tacticalrmm.middleware.LinuxMiddleware", | ||||
|     } | ||||
| ) | ||||
| class TestAgentViewsNew(TacticalTestCase): | ||||
|     def setUp(self): | ||||
|         self.authenticate() | ||||
| @@ -922,11 +909,6 @@ class TestAgentViewsNew(TacticalTestCase): | ||||
|         self.check_not_authenticated("post", url) | ||||
| 
 | ||||
| 
 | ||||
| @modify_settings( | ||||
|     MIDDLEWARE={ | ||||
|         "remove": "tacticalrmm.middleware.LinuxMiddleware", | ||||
|     } | ||||
| ) | ||||
| class TestAgentPermissions(TacticalTestCase): | ||||
|     def setUp(self): | ||||
|         self.setup_client() | ||||
| @@ -1400,11 +1382,6 @@ class TestAgentPermissions(TacticalTestCase): | ||||
|         self.check_authorized_superuser("get", unauthorized_url) | ||||
| 
 | ||||
| 
 | ||||
| @modify_settings( | ||||
|     MIDDLEWARE={ | ||||
|         "remove": "tacticalrmm.middleware.LinuxMiddleware", | ||||
|     } | ||||
| ) | ||||
| class TestAgentTasks(TacticalTestCase): | ||||
|     def setUp(self): | ||||
|         self.authenticate() | ||||
| @@ -1443,8 +1420,8 @@ class TestAgentTasks(TacticalTestCase): | ||||
|         r = agent_update(agent64_nosign.agent_id) | ||||
|         self.assertEqual(r, "created") | ||||
|         action = PendingAction.objects.get(agent__agent_id=agent64_nosign.agent_id) | ||||
|         self.assertEqual(action.action_type, "agentupdate") | ||||
|         self.assertEqual(action.status, "pending") | ||||
|         self.assertEqual(action.action_type, PAAction.AGENT_UPDATE) | ||||
|         self.assertEqual(action.status, PAStatus.PENDING) | ||||
|         self.assertEqual( | ||||
|             action.details["url"], | ||||
|             f"https://github.com/amidaware/rmmagent/releases/download/v{settings.LATEST_AGENT_VER}/winagent-v{settings.LATEST_AGENT_VER}.exe", | ||||
| @@ -1489,8 +1466,8 @@ class TestAgentTasks(TacticalTestCase): | ||||
|             wait=False, | ||||
|         ) | ||||
|         action = PendingAction.objects.get(agent__pk=agent64_sign.pk) | ||||
|         self.assertEqual(action.action_type, "agentupdate") | ||||
|         self.assertEqual(action.status, "pending") | ||||
|         self.assertEqual(action.action_type, PAAction.AGENT_UPDATE) | ||||
|         self.assertEqual(action.status, PAStatus.PENDING) | ||||
| 
 | ||||
|         # test __with__ code signing (32 bit) | ||||
|         agent32_sign = baker.make_recipe( | ||||
| @@ -1515,8 +1492,8 @@ class TestAgentTasks(TacticalTestCase): | ||||
|             wait=False, | ||||
|         ) | ||||
|         action = PendingAction.objects.get(agent__pk=agent32_sign.pk) | ||||
|         self.assertEqual(action.action_type, "agentupdate") | ||||
|         self.assertEqual(action.status, "pending") """ | ||||
|         self.assertEqual(action.action_type, PAAction.AGENT_UPDATE) | ||||
|         self.assertEqual(action.status, PAStatus.PENDING) """ | ||||
| 
 | ||||
|     @patch("agents.tasks.agent_update") | ||||
|     @patch("agents.tasks.sleep", return_value=None) | ||||
| @@ -1547,7 +1524,7 @@ class TestAgentTasks(TacticalTestCase): | ||||
|         self.assertEqual(agent_update.call_count, 33) | ||||
| 
 | ||||
|     def test_agent_history_prune_task(self): | ||||
|         from .tasks import prune_agent_history | ||||
|         from agents.tasks import prune_agent_history | ||||
| 
 | ||||
|         # setup data | ||||
|         agent = baker.make_recipe("agents.agent") | ||||
							
								
								
									
										62
									
								
								api/tacticalrmm/agents/tests/test_recovery.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								api/tacticalrmm/agents/tests/test_recovery.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | ||||
| from typing import TYPE_CHECKING | ||||
| from unittest.mock import patch | ||||
|  | ||||
| from model_bakery import baker | ||||
|  | ||||
| from tacticalrmm.test import TacticalTestCase | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|     from clients.models import Client, Site | ||||
|  | ||||
|  | ||||
| class TestRecovery(TacticalTestCase): | ||||
|     def setUp(self) -> None: | ||||
|         self.authenticate() | ||||
|         self.setup_coresettings() | ||||
|         self.client1: "Client" = baker.make("clients.Client") | ||||
|         self.site1: "Site" = baker.make("clients.Site", client=self.client1) | ||||
|  | ||||
|     @patch("agents.models.Agent.recover") | ||||
|     @patch("agents.views.get_mesh_ws_url") | ||||
|     def test_recover(self, get_mesh_ws_url, recover) -> None: | ||||
|         get_mesh_ws_url.return_value = "https://mesh.example.com" | ||||
|  | ||||
|         agent = baker.make_recipe( | ||||
|             "agents.online_agent", | ||||
|             site=self.site1, | ||||
|             monitoring_type="server", | ||||
|             plat="windows", | ||||
|         ) | ||||
|  | ||||
|         url = f"/agents/{agent.agent_id}/recover/" | ||||
|  | ||||
|         # test successfull tacticalagent recovery | ||||
|         data = {"mode": "tacagent"} | ||||
|         r = self.client.post(url, data, format="json") | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         recover.assert_called_with("tacagent", "https://mesh.example.com", wait=False) | ||||
|         get_mesh_ws_url.assert_called_once() | ||||
|  | ||||
|         # reset mocks | ||||
|         recover.reset_mock() | ||||
|         get_mesh_ws_url.reset_mock() | ||||
|  | ||||
|         # test successfull mesh agent recovery | ||||
|         data = {"mode": "mesh"} | ||||
|         recover.return_value = ("ok", False) | ||||
|         r = self.client.post(url, data, format="json") | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         get_mesh_ws_url.assert_not_called() | ||||
|         recover.assert_called_with("mesh", "") | ||||
|  | ||||
|         # reset mocks | ||||
|         recover.reset_mock() | ||||
|         get_mesh_ws_url.reset_mock() | ||||
|  | ||||
|         # test failed mesh agent recovery | ||||
|         data = {"mode": "mesh"} | ||||
|         recover.return_value = ("Unable to contact the agent", True) | ||||
|         r = self.client.post(url, data, format="json") | ||||
|         self.assertEqual(r.status_code, 400) | ||||
|  | ||||
|         self.check_not_authenticated("post", url) | ||||
| @@ -1,6 +1,7 @@ | ||||
| from django.urls import path | ||||
|  | ||||
| from autotasks.views import GetAddAutoTasks | ||||
| from checks.views import GetAddChecks | ||||
| from django.urls import path | ||||
| from logs.views import PendingActions | ||||
|  | ||||
| from . import views | ||||
|   | ||||
| @@ -2,11 +2,11 @@ import asyncio | ||||
| import tempfile | ||||
| import urllib.parse | ||||
|  | ||||
| from core.models import CodeSignToken | ||||
| from core.utils import get_mesh_device_id, get_mesh_ws_url, get_core_settings | ||||
| from django.conf import settings | ||||
| from django.http import FileResponse | ||||
|  | ||||
| from core.models import CodeSignToken | ||||
| from core.utils import get_core_settings, get_mesh_device_id, get_mesh_ws_url | ||||
| from tacticalrmm.constants import MeshAgentIdent | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -4,40 +4,36 @@ import os | ||||
| import random | ||||
| import string | ||||
| import time | ||||
| from meshctrl.utils import get_login_token | ||||
|  | ||||
| from core.models import CodeSignToken | ||||
| from core.utils import ( | ||||
|     get_mesh_ws_url, | ||||
|     remove_mesh_agent, | ||||
|     send_command_with_mesh, | ||||
|     get_core_settings, | ||||
| ) | ||||
| from django.conf import settings | ||||
| from django.db.models import Q, Prefetch, Exists, Count, OuterRef | ||||
| from django.db.models import Count, Exists, OuterRef, Prefetch, Q | ||||
| from django.http import HttpResponse | ||||
| from django.shortcuts import get_object_or_404 | ||||
| from django.utils import timezone as djangotime | ||||
| from logs.models import AuditLog, DebugLog, PendingAction | ||||
| from meshctrl.utils import get_login_token | ||||
| from packaging import version as pyver | ||||
| from rest_framework.decorators import api_view, permission_classes | ||||
| from rest_framework.exceptions import PermissionDenied | ||||
| from rest_framework.permissions import IsAuthenticated | ||||
| from rest_framework.response import Response | ||||
| from rest_framework.views import APIView | ||||
|  | ||||
| from core.models import CodeSignToken | ||||
| from core.utils import get_core_settings, get_mesh_ws_url, remove_mesh_agent | ||||
| from logs.models import AuditLog, DebugLog, PendingAction | ||||
| from scripts.models import Script | ||||
| from scripts.tasks import handle_bulk_command_task, handle_bulk_script_task | ||||
| from winupdate.serializers import WinUpdatePolicySerializer | ||||
| from winupdate.tasks import bulk_check_for_updates_task, bulk_install_updates_task | ||||
| from winupdate.models import WinUpdate | ||||
|  | ||||
| from tacticalrmm.constants import AGENT_DEFER | ||||
| from tacticalrmm.constants import AGENT_DEFER, EvtLogNames, PAAction, PAStatus | ||||
| from tacticalrmm.helpers import notify_error | ||||
| from tacticalrmm.permissions import ( | ||||
|     _has_perm_on_agent, | ||||
|     _has_perm_on_client, | ||||
|     _has_perm_on_site, | ||||
| ) | ||||
| from tacticalrmm.utils import get_default_timezone, notify_error, reload_nats | ||||
| from tacticalrmm.utils import get_default_timezone, reload_nats | ||||
| from winupdate.models import WinUpdate | ||||
| from winupdate.serializers import WinUpdatePolicySerializer | ||||
| from winupdate.tasks import bulk_check_for_updates_task, bulk_install_updates_task | ||||
|  | ||||
| from .models import Agent, AgentCustomField, AgentHistory, Note | ||||
| from .permissions import ( | ||||
| @@ -119,7 +115,8 @@ class GetAgents(APIView): | ||||
|                 ) | ||||
|                 .annotate( | ||||
|                     pending_actions_count=Count( | ||||
|                         "pendingactions", filter=Q(pendingactions__status="pending") | ||||
|                         "pendingactions", | ||||
|                         filter=Q(pendingactions__status=PAStatus.PENDING), | ||||
|                     ) | ||||
|                 ) | ||||
|                 .annotate( | ||||
| @@ -136,10 +133,10 @@ class GetAgents(APIView): | ||||
|         else: | ||||
|             agents = ( | ||||
|                 Agent.objects.filter_by_role(request.user)  # type: ignore | ||||
|                 .select_related("site") | ||||
|                 .defer(*AGENT_DEFER) | ||||
|                 .select_related("site__client") | ||||
|                 .filter(monitoring_type_filter) | ||||
|                 .filter(client_site_filter) | ||||
|                 .only("agent_id", "hostname", "site") | ||||
|             ) | ||||
|             serializer = AgentHostnameSerializer(agents, many=True) | ||||
|  | ||||
| @@ -297,9 +294,9 @@ class AgentMeshCentral(APIView): | ||||
| @permission_classes([IsAuthenticated, AgentPerms]) | ||||
| def get_agent_versions(request): | ||||
|     agents = ( | ||||
|         Agent.objects.filter_by_role(request.user)  # type: ignore | ||||
|         .prefetch_related("site") | ||||
|         .only("pk", "hostname") | ||||
|         Agent.objects.defer(*AGENT_DEFER) | ||||
|         .filter_by_role(request.user)  # type: ignore | ||||
|         .select_related("site__client") | ||||
|     ) | ||||
|     return Response( | ||||
|         { | ||||
| @@ -356,7 +353,7 @@ def get_event_log(request, agent_id, logtype, days): | ||||
|         return demo_get_eventlog() | ||||
|  | ||||
|     agent = get_object_or_404(Agent, agent_id=agent_id) | ||||
|     timeout = 180 if logtype == "Security" else 30 | ||||
|     timeout = 180 if logtype == EvtLogNames.SECURITY else 30 | ||||
|  | ||||
|     data = { | ||||
|         "func": "eventlog", | ||||
| @@ -430,6 +427,8 @@ class Reboot(APIView): | ||||
|     # reboot later | ||||
|     def patch(self, request, agent_id): | ||||
|         agent = get_object_or_404(Agent, agent_id=agent_id) | ||||
|         if agent.is_posix: | ||||
|             return notify_error(f"Not currently implemented for {agent.plat}") | ||||
|  | ||||
|         try: | ||||
|             obj = dt.datetime.strptime(request.data["datetime"], "%Y-%m-%dT%H:%M:%S") | ||||
| @@ -471,7 +470,7 @@ class Reboot(APIView): | ||||
|  | ||||
|         details = {"taskname": task_name, "time": str(obj)} | ||||
|         PendingAction.objects.create( | ||||
|             agent=agent, action_type="schedreboot", details=details | ||||
|             agent=agent, action_type=PAAction.SCHED_REBOOT, details=details | ||||
|         ) | ||||
|         nice_time = dt.datetime.strftime(obj, "%B %d, %Y at %I:%M %p") | ||||
|         return Response( | ||||
| @@ -482,9 +481,10 @@ class Reboot(APIView): | ||||
| @api_view(["POST"]) | ||||
| @permission_classes([IsAuthenticated, InstallAgentPerms]) | ||||
| def install_agent(request): | ||||
|     from knox.models import AuthToken | ||||
|  | ||||
|     from accounts.models import User | ||||
|     from agents.utils import get_agent_url | ||||
|     from knox.models import AuthToken | ||||
|  | ||||
|     client_id = request.data["client"] | ||||
|     site_id = request.data["site"] | ||||
| @@ -639,28 +639,23 @@ def install_agent(request): | ||||
|  | ||||
| @api_view(["POST"]) | ||||
| @permission_classes([IsAuthenticated, RecoverAgentPerms]) | ||||
| def recover(request, agent_id): | ||||
|     agent = get_object_or_404(Agent, agent_id=agent_id) | ||||
| def recover(request, agent_id: str) -> Response: | ||||
|     agent: Agent = get_object_or_404( | ||||
|         Agent.objects.defer(*AGENT_DEFER), agent_id=agent_id | ||||
|     ) | ||||
|     mode = request.data["mode"] | ||||
|  | ||||
|     if mode == "tacagent": | ||||
|         if agent.is_posix: | ||||
|             cmd = "systemctl restart tacticalagent.service" | ||||
|             shell = 3 | ||||
|         else: | ||||
|             cmd = "net stop tacticalrmm & taskkill /F /IM tacticalrmm.exe & net start tacticalrmm" | ||||
|             shell = 1 | ||||
|         uri = get_mesh_ws_url() | ||||
|         asyncio.run(send_command_with_mesh(cmd, uri, agent.mesh_node_id, shell, 0)) | ||||
|         agent.recover(mode, uri, wait=False) | ||||
|         return Response("Recovery will be attempted shortly") | ||||
|  | ||||
|     elif mode == "mesh": | ||||
|         data = {"func": "recover", "payload": {"mode": mode}} | ||||
|         r = asyncio.run(agent.nats_cmd(data, timeout=20)) | ||||
|         if r == "ok": | ||||
|             return Response("Successfully completed recovery") | ||||
|         r, err = agent.recover(mode, "") | ||||
|         if err: | ||||
|             return notify_error(f"Unable to complete recovery: {r}") | ||||
|  | ||||
|     return notify_error("Something went wrong") | ||||
|     return Response("Successfully completed recovery") | ||||
|  | ||||
|  | ||||
| @api_view(["POST"]) | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| # Generated by Django 4.0.3 on 2022-04-07 17:28 | ||||
|  | ||||
| from django.db import migrations, models | ||||
| import django.db.models.deletion | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| def delete_alerts_without_agent(apps, schema): | ||||
|   | ||||
| @@ -1,14 +1,15 @@ | ||||
| from __future__ import annotations | ||||
|  | ||||
| import re | ||||
| from typing import TYPE_CHECKING, Union, Optional, Dict, Any, List, cast | ||||
| from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union, cast | ||||
|  | ||||
| from django.contrib.postgres.fields import ArrayField | ||||
| from django.db import models | ||||
| from django.db.models.fields import BooleanField, PositiveIntegerField | ||||
| from django.utils import timezone as djangotime | ||||
| from logs.models import BaseAuditModel, DebugLog | ||||
|  | ||||
| from logs.models import BaseAuditModel, DebugLog | ||||
| from tacticalrmm.constants import CheckType, DebugLogType | ||||
| from tacticalrmm.models import PermissionQuerySet | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
| @@ -85,10 +86,10 @@ class Alert(models.Model): | ||||
|     ) | ||||
|  | ||||
|     def __str__(self) -> str: | ||||
|         return self.message | ||||
|         return f"{self.alert_type} - {self.message}" | ||||
|  | ||||
|     @property | ||||
|     def assigned_agent(self) -> "Agent": | ||||
|     def assigned_agent(self) -> "Optional[Agent]": | ||||
|         return self.agent | ||||
|  | ||||
|     @property | ||||
| @@ -176,7 +177,12 @@ class Alert(models.Model): | ||||
|                     alert_type="check", | ||||
|                     severity=check.alert_severity | ||||
|                     if check.check_type | ||||
|                     not in ["memory", "cpuload", "diskspace", "script"] | ||||
|                     not in [ | ||||
|                         CheckType.MEMORY, | ||||
|                         CheckType.CPU_LOAD, | ||||
|                         CheckType.DISK_SPACE, | ||||
|                         CheckType.SCRIPT, | ||||
|                     ] | ||||
|                     else alert_severity, | ||||
|                     message=f"{agent.hostname} has a {check.check_type} check: {check.readable_desc} that failed.", | ||||
|                     hidden=True, | ||||
| @@ -298,12 +304,12 @@ class Alert(models.Model): | ||||
|             maintenance_mode = instance.maintenance_mode | ||||
|             alert_severity = "error" | ||||
|             agent = instance | ||||
|             dashboard_severities = ["error"] | ||||
|             email_severities = ["error"] | ||||
|             text_severities = ["error"] | ||||
|  | ||||
|             # set alert_template settings | ||||
|             if alert_template: | ||||
|                 dashboard_severities = ["error"] | ||||
|                 email_severities = ["error"] | ||||
|                 text_severities = ["error"] | ||||
|                 always_dashboard = alert_template.agent_always_alert | ||||
|                 always_email = alert_template.agent_always_email | ||||
|                 always_text = alert_template.agent_always_text | ||||
| @@ -327,16 +333,33 @@ class Alert(models.Model): | ||||
|             alert_severity = ( | ||||
|                 instance.assigned_check.alert_severity | ||||
|                 if instance.assigned_check.check_type | ||||
|                 not in ["memory", "cpuload", "diskspace", "script"] | ||||
|                 not in [ | ||||
|                     CheckType.MEMORY, | ||||
|                     CheckType.CPU_LOAD, | ||||
|                     CheckType.DISK_SPACE, | ||||
|                     CheckType.SCRIPT, | ||||
|                 ] | ||||
|                 else instance.alert_severity | ||||
|             ) | ||||
|             agent = instance.agent | ||||
|  | ||||
|             # set alert_template settings | ||||
|             if alert_template: | ||||
|                 dashboard_severities = alert_template.check_dashboard_alert_severity | ||||
|                 email_severities = alert_template.check_email_alert_severity | ||||
|                 text_severities = alert_template.check_text_alert_severity | ||||
|                 dashboard_severities = ( | ||||
|                     alert_template.check_dashboard_alert_severity | ||||
|                     if alert_template.check_dashboard_alert_severity | ||||
|                     else ["error", "warning", "info"] | ||||
|                 ) | ||||
|                 email_severities = ( | ||||
|                     alert_template.check_email_alert_severity | ||||
|                     if alert_template.check_email_alert_severity | ||||
|                     else ["error", "warning"] | ||||
|                 ) | ||||
|                 text_severities = ( | ||||
|                     alert_template.check_text_alert_severity | ||||
|                     if alert_template.check_text_alert_severity | ||||
|                     else ["error", "warning"] | ||||
|                 ) | ||||
|                 always_dashboard = alert_template.check_always_alert | ||||
|                 always_email = alert_template.check_always_email | ||||
|                 always_text = alert_template.check_always_text | ||||
| @@ -359,9 +382,21 @@ class Alert(models.Model): | ||||
|  | ||||
|             # set alert_template settings | ||||
|             if alert_template: | ||||
|                 dashboard_severities = alert_template.task_dashboard_alert_severity | ||||
|                 email_severities = alert_template.task_email_alert_severity | ||||
|                 text_severities = alert_template.task_text_alert_severity | ||||
|                 dashboard_severities = ( | ||||
|                     alert_template.task_dashboard_alert_severity | ||||
|                     if alert_template.task_dashboard_alert_severity | ||||
|                     else ["error", "warning"] | ||||
|                 ) | ||||
|                 email_severities = ( | ||||
|                     alert_template.task_email_alert_severity | ||||
|                     if alert_template.task_email_alert_severity | ||||
|                     else ["error", "warning"] | ||||
|                 ) | ||||
|                 text_severities = ( | ||||
|                     alert_template.task_text_alert_severity | ||||
|                     if alert_template.task_text_alert_severity | ||||
|                     else ["error", "warning"] | ||||
|                 ) | ||||
|                 always_dashboard = alert_template.task_always_alert | ||||
|                 always_email = alert_template.task_always_email | ||||
|                 always_text = alert_template.task_always_text | ||||
| @@ -449,7 +484,7 @@ class Alert(models.Model): | ||||
|             else: | ||||
|                 DebugLog.error( | ||||
|                     agent=agent, | ||||
|                     log_type="scripting", | ||||
|                     log_type=DebugLogType.SCRIPTING, | ||||
|                     message=f"Failure action: {alert_template.action.name} failed to run on any agent for {agent.hostname}({agent.pk}) failure alert", | ||||
|                 ) | ||||
|  | ||||
| @@ -573,7 +608,7 @@ class Alert(models.Model): | ||||
|             else: | ||||
|                 DebugLog.error( | ||||
|                     agent=agent, | ||||
|                     log_type="scripting", | ||||
|                     log_type=DebugLogType.SCRIPTING, | ||||
|                     message=f"Resolved action: {alert_template.action.name} failed to run on any agent for {agent.hostname}({agent.pk}) resolved alert", | ||||
|                 ) | ||||
|  | ||||
| @@ -600,7 +635,7 @@ class Alert(models.Model): | ||||
|                 try: | ||||
|                     temp_args.append(re.sub("\\{\\{.*\\}\\}", value, arg)) | ||||
|                 except Exception as e: | ||||
|                     DebugLog.error(log_type="scripting", message=str(e)) | ||||
|                     DebugLog.error(log_type=DebugLogType.SCRIPTING, message=str(e)) | ||||
|                     continue | ||||
|  | ||||
|             else: | ||||
|   | ||||
| @@ -1,6 +1,8 @@ | ||||
| from typing import TYPE_CHECKING | ||||
|  | ||||
| from django.shortcuts import get_object_or_404 | ||||
| from rest_framework import permissions | ||||
| from typing import TYPE_CHECKING | ||||
|  | ||||
| from tacticalrmm.permissions import _has_perm, _has_perm_on_agent | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|   | ||||
| @@ -1,8 +1,9 @@ | ||||
| from automation.serializers import PolicySerializer | ||||
| from clients.serializers import ClientMinimumSerializer, SiteMinimumSerializer | ||||
| from rest_framework.fields import SerializerMethodField | ||||
| from rest_framework.serializers import ModelSerializer, ReadOnlyField | ||||
|  | ||||
| from automation.serializers import PolicySerializer | ||||
| from clients.serializers import ClientMinimumSerializer, SiteMinimumSerializer | ||||
|  | ||||
| from .models import Alert, AlertTemplate | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -1,8 +1,10 @@ | ||||
| from django.utils import timezone as djangotime | ||||
| from .models import Alert | ||||
|  | ||||
| from agents.models import Agent | ||||
| from tacticalrmm.celery import app | ||||
|  | ||||
| from .models import Alert | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| def unsnooze_alerts() -> str: | ||||
|   | ||||
| @@ -2,16 +2,17 @@ from datetime import datetime, timedelta | ||||
| from itertools import cycle | ||||
| from unittest.mock import patch | ||||
|  | ||||
| from alerts.tasks import cache_agents_alert_template | ||||
| from core.utils import get_core_settings | ||||
| from core.tasks import cache_db_fields_task, handle_resolved_stuff | ||||
| from django.conf import settings | ||||
| from django.utils import timezone as djangotime | ||||
| from model_bakery import baker, seq | ||||
|  | ||||
| from alerts.tasks import cache_agents_alert_template | ||||
| from autotasks.models import TaskResult | ||||
| from core.tasks import cache_db_fields_task, handle_resolved_stuff | ||||
| from core.utils import get_core_settings | ||||
| from tacticalrmm.constants import CheckStatus | ||||
| from tacticalrmm.test import TacticalTestCase | ||||
|  | ||||
| from autotasks.models import TaskResult | ||||
| from .models import Alert, AlertTemplate | ||||
| from .serializers import ( | ||||
|     AlertSerializer, | ||||
| @@ -70,8 +71,8 @@ class TestAlertsViews(TacticalTestCase): | ||||
|         data = {"top": 3} | ||||
|         resp = self.client.patch(url, data, format="json") | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|         self.assertEquals(resp.data["alerts"], AlertSerializer(alerts, many=True).data) | ||||
|         self.assertEquals(resp.data["alerts_count"], 10) | ||||
|         self.assertEqual(resp.data["alerts"], AlertSerializer(alerts, many=True).data) | ||||
|         self.assertEqual(resp.data["alerts_count"], 10) | ||||
|  | ||||
|         # test filter data | ||||
|         # test data and result counts | ||||
| @@ -409,15 +410,15 @@ class TestAlertTasks(TacticalTestCase): | ||||
|         core.server_policy = policy | ||||
|         core.save() | ||||
|  | ||||
|         self.assertEquals(server.set_alert_template().pk, alert_templates[0].pk) | ||||
|         self.assertEquals(workstation.set_alert_template().pk, alert_templates[0].pk) | ||||
|         self.assertEqual(server.set_alert_template().pk, alert_templates[0].pk) | ||||
|         self.assertEqual(workstation.set_alert_template().pk, alert_templates[0].pk) | ||||
|  | ||||
|         # assign second Alert Template to as default alert template | ||||
|         core.alert_template = alert_templates[1] | ||||
|         core.save() | ||||
|  | ||||
|         self.assertEquals(workstation.set_alert_template().pk, alert_templates[1].pk) | ||||
|         self.assertEquals(server.set_alert_template().pk, alert_templates[1].pk) | ||||
|         self.assertEqual(workstation.set_alert_template().pk, alert_templates[1].pk) | ||||
|         self.assertEqual(server.set_alert_template().pk, alert_templates[1].pk) | ||||
|  | ||||
|         # assign third Alert Template to client | ||||
|         workstation.client.alert_template = alert_templates[2] | ||||
| @@ -425,8 +426,8 @@ class TestAlertTasks(TacticalTestCase): | ||||
|         workstation.client.save() | ||||
|         server.client.save() | ||||
|  | ||||
|         self.assertEquals(workstation.set_alert_template().pk, alert_templates[2].pk) | ||||
|         self.assertEquals(server.set_alert_template().pk, alert_templates[2].pk) | ||||
|         self.assertEqual(workstation.set_alert_template().pk, alert_templates[2].pk) | ||||
|         self.assertEqual(server.set_alert_template().pk, alert_templates[2].pk) | ||||
|  | ||||
|         # apply policy to client and should override | ||||
|         workstation.client.workstation_policy = policy | ||||
| @@ -434,8 +435,8 @@ class TestAlertTasks(TacticalTestCase): | ||||
|         workstation.client.save() | ||||
|         server.client.save() | ||||
|  | ||||
|         self.assertEquals(workstation.set_alert_template().pk, alert_templates[0].pk) | ||||
|         self.assertEquals(server.set_alert_template().pk, alert_templates[0].pk) | ||||
|         self.assertEqual(workstation.set_alert_template().pk, alert_templates[0].pk) | ||||
|         self.assertEqual(server.set_alert_template().pk, alert_templates[0].pk) | ||||
|  | ||||
|         # assign fouth Alert Template to site | ||||
|         workstation.site.alert_template = alert_templates[3] | ||||
| @@ -443,8 +444,8 @@ class TestAlertTasks(TacticalTestCase): | ||||
|         workstation.site.save() | ||||
|         server.site.save() | ||||
|  | ||||
|         self.assertEquals(workstation.set_alert_template().pk, alert_templates[3].pk) | ||||
|         self.assertEquals(server.set_alert_template().pk, alert_templates[3].pk) | ||||
|         self.assertEqual(workstation.set_alert_template().pk, alert_templates[3].pk) | ||||
|         self.assertEqual(server.set_alert_template().pk, alert_templates[3].pk) | ||||
|  | ||||
|         # apply policy to site | ||||
|         workstation.site.workstation_policy = policy | ||||
| @@ -452,8 +453,8 @@ class TestAlertTasks(TacticalTestCase): | ||||
|         workstation.site.save() | ||||
|         server.site.save() | ||||
|  | ||||
|         self.assertEquals(workstation.set_alert_template().pk, alert_templates[0].pk) | ||||
|         self.assertEquals(server.set_alert_template().pk, alert_templates[0].pk) | ||||
|         self.assertEqual(workstation.set_alert_template().pk, alert_templates[0].pk) | ||||
|         self.assertEqual(server.set_alert_template().pk, alert_templates[0].pk) | ||||
|  | ||||
|         # apply policy to agents | ||||
|         workstation.policy = policy | ||||
| @@ -461,35 +462,35 @@ class TestAlertTasks(TacticalTestCase): | ||||
|         workstation.save() | ||||
|         server.save() | ||||
|  | ||||
|         self.assertEquals(workstation.set_alert_template().pk, alert_templates[0].pk) | ||||
|         self.assertEquals(server.set_alert_template().pk, alert_templates[0].pk) | ||||
|         self.assertEqual(workstation.set_alert_template().pk, alert_templates[0].pk) | ||||
|         self.assertEqual(server.set_alert_template().pk, alert_templates[0].pk) | ||||
|  | ||||
|         # test disabling alert template | ||||
|         alert_templates[0].is_active = False | ||||
|         alert_templates[0].save() | ||||
|  | ||||
|         self.assertEquals(workstation.set_alert_template().pk, alert_templates[3].pk) | ||||
|         self.assertEquals(server.set_alert_template().pk, alert_templates[3].pk) | ||||
|         self.assertEqual(workstation.set_alert_template().pk, alert_templates[3].pk) | ||||
|         self.assertEqual(server.set_alert_template().pk, alert_templates[3].pk) | ||||
|  | ||||
|         # test policy exclusions | ||||
|         alert_templates[3].excluded_agents.set([workstation.pk]) | ||||
|  | ||||
|         self.assertEquals(workstation.set_alert_template().pk, alert_templates[2].pk) | ||||
|         self.assertEquals(server.set_alert_template().pk, alert_templates[3].pk) | ||||
|         self.assertEqual(workstation.set_alert_template().pk, alert_templates[2].pk) | ||||
|         self.assertEqual(server.set_alert_template().pk, alert_templates[3].pk) | ||||
|  | ||||
|         # test workstation exclusions | ||||
|         alert_templates[2].exclude_workstations = True | ||||
|         alert_templates[2].save() | ||||
|  | ||||
|         self.assertEquals(workstation.set_alert_template().pk, alert_templates[1].pk) | ||||
|         self.assertEquals(server.set_alert_template().pk, alert_templates[3].pk) | ||||
|         self.assertEqual(workstation.set_alert_template().pk, alert_templates[1].pk) | ||||
|         self.assertEqual(server.set_alert_template().pk, alert_templates[3].pk) | ||||
|  | ||||
|         # test server exclusions | ||||
|         alert_templates[3].exclude_servers = True | ||||
|         alert_templates[3].save() | ||||
|  | ||||
|         self.assertEquals(workstation.set_alert_template().pk, alert_templates[1].pk) | ||||
|         self.assertEquals(server.set_alert_template().pk, alert_templates[2].pk) | ||||
|         self.assertEqual(workstation.set_alert_template().pk, alert_templates[1].pk) | ||||
|         self.assertEqual(server.set_alert_template().pk, alert_templates[2].pk) | ||||
|  | ||||
|     @patch("agents.tasks.sleep") | ||||
|     @patch("core.models.CoreSettings.send_mail") | ||||
| @@ -523,7 +524,7 @@ class TestAlertTasks(TacticalTestCase): | ||||
|         # call outages task and no alert should be created | ||||
|         agent_outages_task() | ||||
|  | ||||
|         self.assertEquals(Alert.objects.count(), 0) | ||||
|         self.assertEqual(Alert.objects.count(), 0) | ||||
|  | ||||
|         # set overdue_dashboard_alert and alert should be created | ||||
|         agent_dashboard_alert.overdue_dashboard_alert = True | ||||
| @@ -574,22 +575,22 @@ class TestAlertTasks(TacticalTestCase): | ||||
|         agent_outages_task() | ||||
|  | ||||
|         # should have created 6 alerts | ||||
|         self.assertEquals(Alert.objects.count(), 6) | ||||
|         self.assertEqual(Alert.objects.count(), 6) | ||||
|  | ||||
|         # other specific agents should have created alerts | ||||
|         self.assertEquals(Alert.objects.filter(agent=agent_dashboard_alert).count(), 1) | ||||
|         self.assertEquals(Alert.objects.filter(agent=agent_text_alert).count(), 1) | ||||
|         self.assertEquals(Alert.objects.filter(agent=agent_email_alert).count(), 1) | ||||
|         self.assertEquals(Alert.objects.filter(agent=agent_template_email).count(), 1) | ||||
|         self.assertEquals( | ||||
|         self.assertEqual(Alert.objects.filter(agent=agent_dashboard_alert).count(), 1) | ||||
|         self.assertEqual(Alert.objects.filter(agent=agent_text_alert).count(), 1) | ||||
|         self.assertEqual(Alert.objects.filter(agent=agent_email_alert).count(), 1) | ||||
|         self.assertEqual(Alert.objects.filter(agent=agent_template_email).count(), 1) | ||||
|         self.assertEqual( | ||||
|             Alert.objects.filter(agent=agent_template_dashboard).count(), 1 | ||||
|         ) | ||||
|         self.assertEquals(Alert.objects.filter(agent=agent_template_text).count(), 1) | ||||
|         self.assertEquals(Alert.objects.filter(agent=agent_template_blank).count(), 0) | ||||
|         self.assertEqual(Alert.objects.filter(agent=agent_template_text).count(), 1) | ||||
|         self.assertEqual(Alert.objects.filter(agent=agent_template_blank).count(), 0) | ||||
|  | ||||
|         # check if email and text tasks were called | ||||
|         self.assertEquals(outage_email.call_count, 2) | ||||
|         self.assertEquals(outage_sms.call_count, 2) | ||||
|         self.assertEqual(outage_email.call_count, 2) | ||||
|         self.assertEqual(outage_sms.call_count, 2) | ||||
|  | ||||
|         outage_sms.assert_any_call( | ||||
|             pk=Alert.objects.get(agent=agent_text_alert).pk, alert_interval=None | ||||
| @@ -630,7 +631,7 @@ class TestAlertTasks(TacticalTestCase): | ||||
|  | ||||
|         # calling agent outage task again shouldn't create duplicate alerts and won't send alerts | ||||
|         agent_outages_task() | ||||
|         self.assertEquals(Alert.objects.count(), 6) | ||||
|         self.assertEqual(Alert.objects.count(), 6) | ||||
|  | ||||
|         # test periodic notification | ||||
|         # change email/text sent to sometime in the past | ||||
| @@ -841,7 +842,7 @@ class TestAlertTasks(TacticalTestCase): | ||||
|  | ||||
|         # test agent with check that has alert settings | ||||
|         check_agent_result.alert_severity = "warning" | ||||
|         check_agent_result.status = "failing" | ||||
|         check_agent_result.status = CheckStatus.FAILING | ||||
|  | ||||
|         Alert.handle_alert_failure(check_agent_result) | ||||
|  | ||||
| @@ -942,7 +943,7 @@ class TestAlertTasks(TacticalTestCase): | ||||
|         send_email.assert_not_called() | ||||
|         send_sms.assert_not_called() | ||||
|  | ||||
|         self.assertEquals( | ||||
|         self.assertEqual( | ||||
|             Alert.objects.filter(assigned_check=check_template_email).count(), 1 | ||||
|         ) | ||||
|  | ||||
| @@ -1244,7 +1245,7 @@ class TestAlertTasks(TacticalTestCase): | ||||
|         send_email.assert_not_called() | ||||
|         send_sms.assert_not_called() | ||||
|  | ||||
|         self.assertEquals( | ||||
|         self.assertEqual( | ||||
|             Alert.objects.filter(assigned_task=task_template_email).count(), 1 | ||||
|         ) | ||||
|  | ||||
|   | ||||
| @@ -7,7 +7,7 @@ from rest_framework.permissions import IsAuthenticated | ||||
| from rest_framework.response import Response | ||||
| from rest_framework.views import APIView | ||||
|  | ||||
| from tacticalrmm.utils import notify_error | ||||
| from tacticalrmm.helpers import notify_error | ||||
|  | ||||
| from .models import Alert, AlertTemplate | ||||
| from .permissions import AlertPerms, AlertTemplatePerms | ||||
|   | ||||
							
								
								
									
										0
									
								
								api/tacticalrmm/apiv3/tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								api/tacticalrmm/apiv3/tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -1,11 +1,7 @@ | ||||
| import json | ||||
| import os | ||||
| 
 | ||||
| from autotasks.models import TaskResult | ||||
| from django.conf import settings | ||||
| from django.utils import timezone as djangotime | ||||
| from model_bakery import baker | ||||
| 
 | ||||
| from autotasks.models import TaskResult | ||||
| from tacticalrmm.test import TacticalTestCase | ||||
| 
 | ||||
| 
 | ||||
| @@ -62,7 +58,7 @@ class TestAPIv3(TacticalTestCase): | ||||
|         r = self.client.get(url) | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         self.assertEqual(r.data["check_interval"], 20) | ||||
|         self.assertEquals(len(r.data["checks"]), 2) | ||||
|         self.assertEqual(len(r.data["checks"]), 2) | ||||
| 
 | ||||
|         url = "/api/v3/Maj34ACb324j234asdj2n34kASDjh34-DESKTOPTEST123/checkrunner/" | ||||
|         r = self.client.get(url) | ||||
| @@ -70,24 +66,6 @@ class TestAPIv3(TacticalTestCase): | ||||
| 
 | ||||
|         self.check_not_authenticated("get", url) | ||||
| 
 | ||||
|     def test_sysinfo(self): | ||||
|         # TODO replace this with golang wmi sample data | ||||
| 
 | ||||
|         url = "/api/v3/sysinfo/" | ||||
|         with open( | ||||
|             os.path.join( | ||||
|                 settings.BASE_DIR, "tacticalrmm/test_data/wmi_python_agent.json" | ||||
|             ) | ||||
|         ) as f: | ||||
|             wmi_py = json.load(f) | ||||
| 
 | ||||
|         payload = {"agent_id": self.agent.agent_id, "sysinfo": wmi_py} | ||||
| 
 | ||||
|         r = self.client.patch(url, payload, format="json") | ||||
|         self.assertEqual(r.status_code, 200) | ||||
| 
 | ||||
|         self.check_not_authenticated("patch", url) | ||||
| 
 | ||||
|     def test_checkrunner_interval(self): | ||||
|         url = f"/api/v3/{self.agent.agent_id}/checkinterval/" | ||||
|         r = self.client.get(url, format="json") | ||||
| @@ -137,8 +115,6 @@ class TestAPIv3(TacticalTestCase): | ||||
|         self.assertEqual(len(r.json()["checks"]), 15) | ||||
| 
 | ||||
|     def test_task_runner_get(self): | ||||
|         from autotasks.serializers import TaskGOGetSerializer | ||||
| 
 | ||||
|         r = self.client.get("/api/v3/500/asdf9df9dfdf/taskrunner/") | ||||
|         self.assertEqual(r.status_code, 404) | ||||
| 
 | ||||
| @@ -163,7 +139,6 @@ class TestAPIv3(TacticalTestCase): | ||||
| 
 | ||||
|         r = self.client.get(url) | ||||
|         self.assertEqual(r.status_code, 200) | ||||
|         self.assertEqual(TaskGOGetSerializer(task).data, r.data) | ||||
| 
 | ||||
|     def test_task_runner_results(self): | ||||
|         from agents.models import AgentCustomField | ||||
| @@ -9,7 +9,6 @@ urlpatterns = [ | ||||
|     path("<str:agentid>/checkinterval/", views.CheckRunnerInterval.as_view()), | ||||
|     path("<int:pk>/<str:agentid>/taskrunner/", views.TaskRunner.as_view()), | ||||
|     path("meshexe/", views.MeshExe.as_view()), | ||||
|     path("sysinfo/", views.SysInfo.as_view()), | ||||
|     path("newagent/", views.NewAgent.as_view()), | ||||
|     path("software/", views.Software.as_view()), | ||||
|     path("installer/", views.Installer.as_view()), | ||||
|   | ||||
| @@ -1,30 +1,44 @@ | ||||
| import asyncio | ||||
| import time | ||||
|  | ||||
| from accounts.models import User | ||||
| from agents.models import Agent, AgentHistory | ||||
| from agents.serializers import AgentHistorySerializer | ||||
| from autotasks.models import AutomatedTask, TaskResult | ||||
| from autotasks.serializers import TaskGOGetSerializer, TaskResultSerializer | ||||
| from checks.models import Check, CheckResult | ||||
| from checks.serializers import CheckRunnerGetSerializer | ||||
| from core.utils import get_core_settings | ||||
| from core.utils import download_mesh_agent, get_mesh_device_id, get_mesh_ws_url | ||||
| from django.conf import settings | ||||
| from django.db.models import Prefetch | ||||
| from django.shortcuts import get_object_or_404 | ||||
| from django.utils import timezone as djangotime | ||||
| from logs.models import DebugLog, PendingAction | ||||
| from packaging import version as pyver | ||||
| from rest_framework.authentication import TokenAuthentication | ||||
| from rest_framework.authtoken.models import Token | ||||
| from rest_framework.permissions import IsAuthenticated | ||||
| from rest_framework.response import Response | ||||
| from rest_framework.views import APIView | ||||
| from software.models import InstalledSoftware | ||||
| from winupdate.models import WinUpdate, WinUpdatePolicy | ||||
|  | ||||
| from tacticalrmm.constants import MeshAgentIdent | ||||
| from tacticalrmm.utils import notify_error, reload_nats | ||||
| from accounts.models import User | ||||
| from agents.models import Agent, AgentHistory | ||||
| from agents.serializers import AgentHistorySerializer | ||||
| from autotasks.models import AutomatedTask, TaskResult | ||||
| from autotasks.serializers import TaskGOGetSerializer, TaskResultSerializer | ||||
| from checks.constants import CHECK_DEFER, CHECK_RESULT_DEFER | ||||
| from checks.models import Check, CheckResult | ||||
| from checks.serializers import CheckRunnerGetSerializer | ||||
| from core.utils import ( | ||||
|     download_mesh_agent, | ||||
|     get_core_settings, | ||||
|     get_mesh_device_id, | ||||
|     get_mesh_ws_url, | ||||
| ) | ||||
| from logs.models import DebugLog, PendingAction | ||||
| from software.models import InstalledSoftware | ||||
| from tacticalrmm.constants import ( | ||||
|     AGENT_DEFER, | ||||
|     AuditActionType, | ||||
|     AuditObjType, | ||||
|     CheckStatus, | ||||
|     DebugLogType, | ||||
|     MeshAgentIdent, | ||||
|     PAStatus, | ||||
| ) | ||||
| from tacticalrmm.helpers import notify_error | ||||
| from tacticalrmm.utils import reload_nats | ||||
| from winupdate.models import WinUpdate, WinUpdatePolicy | ||||
|  | ||||
|  | ||||
| class CheckIn(APIView): | ||||
| @@ -34,7 +48,9 @@ class CheckIn(APIView): | ||||
|  | ||||
|     # called once during tacticalagent windows service startup | ||||
|     def post(self, request): | ||||
|         agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) | ||||
|         agent = get_object_or_404( | ||||
|             Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"] | ||||
|         ) | ||||
|         if not agent.choco_installed: | ||||
|             asyncio.run(agent.nats_cmd({"func": "installchoco"}, wait=False)) | ||||
|  | ||||
| @@ -47,7 +63,9 @@ class SyncMeshNodeID(APIView): | ||||
|     permission_classes = [IsAuthenticated] | ||||
|  | ||||
|     def post(self, request): | ||||
|         agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) | ||||
|         agent = get_object_or_404( | ||||
|             Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"] | ||||
|         ) | ||||
|         if agent.mesh_node_id != request.data["nodeid"]: | ||||
|             agent.mesh_node_id = request.data["nodeid"] | ||||
|             agent.save(update_fields=["mesh_node_id"]) | ||||
| @@ -60,7 +78,9 @@ class Choco(APIView): | ||||
|     permission_classes = [IsAuthenticated] | ||||
|  | ||||
|     def post(self, request): | ||||
|         agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) | ||||
|         agent = get_object_or_404( | ||||
|             Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"] | ||||
|         ) | ||||
|         agent.choco_installed = request.data["installed"] | ||||
|         agent.save(update_fields=["choco_installed"]) | ||||
|         return Response("ok") | ||||
| @@ -71,7 +91,9 @@ class WinUpdates(APIView): | ||||
|     permission_classes = [IsAuthenticated] | ||||
|  | ||||
|     def put(self, request): | ||||
|         agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) | ||||
|         agent = get_object_or_404( | ||||
|             Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"] | ||||
|         ) | ||||
|  | ||||
|         needs_reboot: bool = request.data["needs_reboot"] | ||||
|         agent.needs_reboot = needs_reboot | ||||
| @@ -89,7 +111,7 @@ class WinUpdates(APIView): | ||||
|             asyncio.run(agent.nats_cmd({"func": "rebootnow"}, wait=False)) | ||||
|             DebugLog.info( | ||||
|                 agent=agent, | ||||
|                 log_type="windows_updates", | ||||
|                 log_type=DebugLogType.WIN_UPDATES, | ||||
|                 message=f"{agent.hostname} is rebooting after updates were installed.", | ||||
|             ) | ||||
|  | ||||
| @@ -97,7 +119,9 @@ class WinUpdates(APIView): | ||||
|         return Response("ok") | ||||
|  | ||||
|     def patch(self, request): | ||||
|         agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) | ||||
|         agent = get_object_or_404( | ||||
|             Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"] | ||||
|         ) | ||||
|         u = agent.winupdates.filter(guid=request.data["guid"]).last()  # type: ignore | ||||
|         if not u: | ||||
|             raise WinUpdate.DoesNotExist | ||||
| @@ -124,8 +148,14 @@ class WinUpdates(APIView): | ||||
|         return Response("ok") | ||||
|  | ||||
|     def post(self, request): | ||||
|         agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) | ||||
|         updates = request.data["wua_updates"] | ||||
|         if not updates: | ||||
|             return notify_error("Empty payload") | ||||
|  | ||||
|         agent = get_object_or_404( | ||||
|             Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"] | ||||
|         ) | ||||
|  | ||||
|         for update in updates: | ||||
|             if agent.winupdates.filter(guid=update["guid"]).exists():  # type: ignore | ||||
|                 u = agent.winupdates.filter(guid=update["guid"]).last()  # type: ignore | ||||
| @@ -164,7 +194,9 @@ class SupersededWinUpdate(APIView): | ||||
|     permission_classes = [IsAuthenticated] | ||||
|  | ||||
|     def post(self, request): | ||||
|         agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) | ||||
|         agent = get_object_or_404( | ||||
|             Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"] | ||||
|         ) | ||||
|         updates = agent.winupdates.filter(guid=request.data["guid"])  # type: ignore | ||||
|         for u in updates: | ||||
|             u.delete() | ||||
| @@ -177,12 +209,19 @@ class RunChecks(APIView): | ||||
|     permission_classes = [IsAuthenticated] | ||||
|  | ||||
|     def get(self, request, agentid): | ||||
|         agent = get_object_or_404(Agent, agent_id=agentid) | ||||
|         agent = get_object_or_404( | ||||
|             Agent.objects.defer(*AGENT_DEFER).prefetch_related( | ||||
|                 Prefetch("agentchecks", queryset=Check.objects.select_related("script")) | ||||
|             ), | ||||
|             agent_id=agentid, | ||||
|         ) | ||||
|         checks = agent.get_checks_with_policies(exclude_overridden=True) | ||||
|         ret = { | ||||
|             "agent": agent.pk, | ||||
|             "check_interval": agent.check_interval, | ||||
|             "checks": CheckRunnerGetSerializer(checks, many=True).data, | ||||
|             "checks": CheckRunnerGetSerializer( | ||||
|                 checks, context={"agent": agent}, many=True | ||||
|             ).data, | ||||
|         } | ||||
|         return Response(ret) | ||||
|  | ||||
| @@ -192,7 +231,12 @@ class CheckRunner(APIView): | ||||
|     permission_classes = [IsAuthenticated] | ||||
|  | ||||
|     def get(self, request, agentid): | ||||
|         agent = get_object_or_404(Agent, agent_id=agentid) | ||||
|         agent = get_object_or_404( | ||||
|             Agent.objects.defer(*AGENT_DEFER).prefetch_related( | ||||
|                 Prefetch("agentchecks", queryset=Check.objects.select_related("script")) | ||||
|             ), | ||||
|             agent_id=agentid, | ||||
|         ) | ||||
|         checks = agent.get_checks_with_policies(exclude_overridden=True) | ||||
|  | ||||
|         run_list = [ | ||||
| @@ -216,30 +260,38 @@ class CheckRunner(APIView): | ||||
|         ret = { | ||||
|             "agent": agent.pk, | ||||
|             "check_interval": agent.check_run_interval(), | ||||
|             "checks": CheckRunnerGetSerializer(run_list, many=True).data, | ||||
|             "checks": CheckRunnerGetSerializer( | ||||
|                 run_list, context={"agent": agent}, many=True | ||||
|             ).data, | ||||
|         } | ||||
|         return Response(ret) | ||||
|  | ||||
|     def patch(self, request): | ||||
|         check = get_object_or_404(Check, pk=request.data["id"]) | ||||
|  | ||||
|         if "agent_id" not in request.data.keys(): | ||||
|             return notify_error("Agent upgrade required") | ||||
|  | ||||
|         agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) | ||||
|         check = get_object_or_404( | ||||
|             Check.objects.defer(*CHECK_DEFER), | ||||
|             pk=request.data["id"], | ||||
|         ) | ||||
|         agent = get_object_or_404( | ||||
|             Agent.objects.defer(*AGENT_DEFER), agent_id=request.data["agent_id"] | ||||
|         ) | ||||
|  | ||||
|         # check check result or create if doesn't exist | ||||
|         try: | ||||
|             check_result = CheckResult.objects.get(assigned_check=check, agent=agent) | ||||
|         except CheckResult.DoesNotExist: | ||||
|             check_result = CheckResult(assigned_check=check, agent=agent) | ||||
|         # get check result or create if doesn't exist | ||||
|         check_result, created = CheckResult.objects.defer( | ||||
|             *CHECK_RESULT_DEFER | ||||
|         ).get_or_create( | ||||
|             assigned_check=check, | ||||
|             agent=agent, | ||||
|         ) | ||||
|  | ||||
|         check_result.last_run = djangotime.now() | ||||
|         check_result.save() | ||||
|         if created: | ||||
|             check_result.save() | ||||
|  | ||||
|         status = check_result.handle_check(request.data) | ||||
|         if status == "failing" and check.assignedtasks.exists():  # type: ignore | ||||
|             for task in check.assignedtasks.all():  # type: ignore | ||||
|         status = check_result.handle_check(request.data, check, agent) | ||||
|         if status == CheckStatus.FAILING and check.assignedtasks.exists(): | ||||
|             for task in check.assignedtasks.all(): | ||||
|                 if task.enabled: | ||||
|                     if task.policy: | ||||
|                         task.run_win_task(agent) | ||||
| @@ -254,7 +306,10 @@ class CheckRunnerInterval(APIView): | ||||
|     permission_classes = [IsAuthenticated] | ||||
|  | ||||
|     def get(self, request, agentid): | ||||
|         agent = get_object_or_404(Agent, agent_id=agentid) | ||||
|         agent = get_object_or_404( | ||||
|             Agent.objects.defer(*AGENT_DEFER).prefetch_related("agentchecks"), | ||||
|             agent_id=agentid, | ||||
|         ) | ||||
|  | ||||
|         return Response( | ||||
|             {"agent": agent.pk, "check_interval": agent.check_run_interval()} | ||||
| @@ -266,19 +321,28 @@ class TaskRunner(APIView): | ||||
|     permission_classes = [IsAuthenticated] | ||||
|  | ||||
|     def get(self, request, pk, agentid): | ||||
|         _ = get_object_or_404(Agent, agent_id=agentid) | ||||
|         agent = get_object_or_404(Agent.objects.defer(*AGENT_DEFER), agent_id=agentid) | ||||
|         task = get_object_or_404(AutomatedTask, pk=pk) | ||||
|         return Response(TaskGOGetSerializer(task).data) | ||||
|         return Response(TaskGOGetSerializer(task, context={"agent": agent}).data) | ||||
|  | ||||
|     def patch(self, request, pk, agentid): | ||||
|         from alerts.models import Alert | ||||
|  | ||||
|         agent = get_object_or_404(Agent, agent_id=agentid) | ||||
|         task = get_object_or_404(AutomatedTask, pk=pk) | ||||
|         agent = get_object_or_404( | ||||
|             Agent.objects.defer(*AGENT_DEFER), | ||||
|             agent_id=agentid, | ||||
|         ) | ||||
|         task = get_object_or_404( | ||||
|             AutomatedTask.objects.select_related("custom_field"), pk=pk | ||||
|         ) | ||||
|  | ||||
|         # check check result or create if doesn't exist | ||||
|         # get task result or create if doesn't exist | ||||
|         try: | ||||
|             task_result = TaskResult.objects.get(task=task, agent=agent) | ||||
|             task_result = ( | ||||
|                 TaskResult.objects.select_related("agent") | ||||
|                 .defer("agent__services", "agent__wmi_detail") | ||||
|                 .get(task=task, agent=agent) | ||||
|             ) | ||||
|             serializer = TaskResultSerializer( | ||||
|                 data=request.data, instance=task_result, partial=True | ||||
|             ) | ||||
| @@ -290,7 +354,7 @@ class TaskRunner(APIView): | ||||
|  | ||||
|         AgentHistory.objects.create( | ||||
|             agent=agent, | ||||
|             type="task_run", | ||||
|             type=AuditActionType.TASK_RUN, | ||||
|             command=task.name, | ||||
|             script_results=request.data, | ||||
|         ) | ||||
| @@ -301,11 +365,13 @@ class TaskRunner(APIView): | ||||
|  | ||||
|                 task_result.save_collector_results() | ||||
|  | ||||
|                 status = "passing" | ||||
|                 status = CheckStatus.PASSING | ||||
|             else: | ||||
|                 status = "failing" | ||||
|                 status = CheckStatus.FAILING | ||||
|         else: | ||||
|             status = "failing" if task_result.retcode != 0 else "passing" | ||||
|             status = ( | ||||
|                 CheckStatus.FAILING if task_result.retcode != 0 else CheckStatus.PASSING | ||||
|             ) | ||||
|  | ||||
|         if task_result: | ||||
|             task_result.status = status | ||||
| @@ -314,7 +380,7 @@ class TaskRunner(APIView): | ||||
|             task_result.status = status | ||||
|             task.save(update_fields=["status"]) | ||||
|  | ||||
|         if status == "passing": | ||||
|         if status == CheckStatus.PASSING: | ||||
|             if Alert.create_or_return_task_alert(task, agent=agent, skip_create=True): | ||||
|                 Alert.handle_alert_resolve(task_result) | ||||
|         else: | ||||
| @@ -323,21 +389,6 @@ class TaskRunner(APIView): | ||||
|         return Response("ok") | ||||
|  | ||||
|  | ||||
| class SysInfo(APIView): | ||||
|     authentication_classes = [TokenAuthentication] | ||||
|     permission_classes = [IsAuthenticated] | ||||
|  | ||||
|     def patch(self, request): | ||||
|         agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) | ||||
|  | ||||
|         if not isinstance(request.data["sysinfo"], dict): | ||||
|             return notify_error("err") | ||||
|  | ||||
|         agent.wmi_detail = request.data["sysinfo"] | ||||
|         agent.save(update_fields=["wmi_detail"]) | ||||
|         return Response("ok") | ||||
|  | ||||
|  | ||||
| class MeshExe(APIView): | ||||
|     """Sends the mesh exe to the installer""" | ||||
|  | ||||
| @@ -414,8 +465,8 @@ class NewAgent(APIView): | ||||
|         AuditLog.objects.create( | ||||
|             username=request.user, | ||||
|             agent=agent.hostname, | ||||
|             object_type="agent", | ||||
|             action="agent_install", | ||||
|             object_type=AuditObjType.AGENT, | ||||
|             action=AuditActionType.AGENT_INSTALL, | ||||
|             message=f"{request.user} installed new agent {agent.hostname}", | ||||
|             after_value=Agent.serialize(agent), | ||||
|             debug_info={"ip": request._client_ip}, | ||||
| @@ -487,7 +538,7 @@ class ChocoResult(APIView): | ||||
|  | ||||
|         action.details["output"] = results | ||||
|         action.details["installed"] = installed | ||||
|         action.status = "completed" | ||||
|         action.status = PAStatus.COMPLETED | ||||
|         action.save(update_fields=["details", "status"]) | ||||
|         return Response("ok") | ||||
|  | ||||
| @@ -497,8 +548,9 @@ class AgentHistoryResult(APIView): | ||||
|     permission_classes = [IsAuthenticated] | ||||
|  | ||||
|     def patch(self, request, agentid, pk): | ||||
|         _ = get_object_or_404(Agent, agent_id=agentid) | ||||
|         hist = get_object_or_404(AgentHistory, pk=pk) | ||||
|         hist = get_object_or_404( | ||||
|             AgentHistory.objects.filter(agent__agent_id=agentid), pk=pk | ||||
|         ) | ||||
|         s = AgentHistorySerializer(instance=hist, data=request.data, partial=True) | ||||
|         s.is_valid(raise_exception=True) | ||||
|         s.save() | ||||
|   | ||||
| @@ -1,16 +1,16 @@ | ||||
| from typing import TYPE_CHECKING, Any, Dict, List, Optional | ||||
|  | ||||
| from django.core.cache import cache | ||||
| from django.db import models | ||||
|  | ||||
| from agents.models import Agent | ||||
| from clients.models import Client, Site | ||||
| from django.db import models | ||||
| from django.core.cache import cache | ||||
| from logs.models import BaseAuditModel | ||||
|  | ||||
| from typing import Optional, Dict, Any, List, TYPE_CHECKING | ||||
|  | ||||
| from tacticalrmm.constants import CORESETTINGS_CACHE_KEY | ||||
| from tacticalrmm.constants import CORESETTINGS_CACHE_KEY, CheckType | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|     from checks.models import Check | ||||
|     from autotasks.models import AutomatedTask | ||||
|     from checks.models import Check | ||||
|  | ||||
|  | ||||
| class Policy(BaseAuditModel): | ||||
| @@ -279,7 +279,7 @@ class Policy(BaseAuditModel): | ||||
|  | ||||
|         # Loop over checks in with enforced policies first, then non-enforced policies | ||||
|         for check in enforced_checks + agent_checks + policy_checks: | ||||
|             if check.check_type == "diskspace" and agent.plat == "windows": | ||||
|             if check.check_type == CheckType.DISK_SPACE and agent.plat == "windows": | ||||
|                 # Check if drive letter was already added | ||||
|                 if check.disk not in added_diskspace_checks: | ||||
|                     added_diskspace_checks.append(check.disk) | ||||
| @@ -289,7 +289,7 @@ class Policy(BaseAuditModel): | ||||
|                 elif check.agent: | ||||
|                     overridden_checks.append(check.pk) | ||||
|  | ||||
|             elif check.check_type == "ping": | ||||
|             elif check.check_type == CheckType.PING: | ||||
|                 # Check if IP/host was already added | ||||
|                 if check.ip not in added_ping_checks: | ||||
|                     added_ping_checks.append(check.ip) | ||||
| @@ -299,7 +299,7 @@ class Policy(BaseAuditModel): | ||||
|                 elif check.agent: | ||||
|                     overridden_checks.append(check.pk) | ||||
|  | ||||
|             elif check.check_type == "cpuload" and agent.plat == "windows": | ||||
|             elif check.check_type == CheckType.CPU_LOAD and agent.plat == "windows": | ||||
|                 # Check if cpuload list is empty | ||||
|                 if not added_cpuload_checks: | ||||
|                     added_cpuload_checks.append(check.pk) | ||||
| @@ -309,7 +309,7 @@ class Policy(BaseAuditModel): | ||||
|                 elif check.agent: | ||||
|                     overridden_checks.append(check.pk) | ||||
|  | ||||
|             elif check.check_type == "memory" and agent.plat == "windows": | ||||
|             elif check.check_type == CheckType.MEMORY and agent.plat == "windows": | ||||
|                 # Check if memory check list is empty | ||||
|                 if not added_memory_checks: | ||||
|                     added_memory_checks.append(check.pk) | ||||
| @@ -319,7 +319,7 @@ class Policy(BaseAuditModel): | ||||
|                 elif check.agent: | ||||
|                     overridden_checks.append(check.pk) | ||||
|  | ||||
|             elif check.check_type == "winsvc" and agent.plat == "windows": | ||||
|             elif check.check_type == CheckType.WINSVC and agent.plat == "windows": | ||||
|                 # Check if service name was already added | ||||
|                 if check.svc_name not in added_winsvc_checks: | ||||
|                     added_winsvc_checks.append(check.svc_name) | ||||
| @@ -329,7 +329,7 @@ class Policy(BaseAuditModel): | ||||
|                 elif check.agent: | ||||
|                     overridden_checks.append(check.pk) | ||||
|  | ||||
|             elif check.check_type == "script" and agent.is_supported_script( | ||||
|             elif check.check_type == CheckType.SCRIPT and agent.is_supported_script( | ||||
|                 check.script.supported_platforms | ||||
|             ): | ||||
|                 # Check if script id was already added | ||||
| @@ -341,7 +341,7 @@ class Policy(BaseAuditModel): | ||||
|                 elif check.agent: | ||||
|                     overridden_checks.append(check.pk) | ||||
|  | ||||
|             elif check.check_type == "eventlog" and agent.plat == "windows": | ||||
|             elif check.check_type == CheckType.EVENT_LOG and agent.plat == "windows": | ||||
|                 # Check if events were already added | ||||
|                 if [check.log_name, check.event_id] not in added_eventlog_checks: | ||||
|                     added_eventlog_checks.append([check.log_name, check.event_id]) | ||||
|   | ||||
| @@ -1,13 +1,14 @@ | ||||
| from agents.serializers import AgentHostnameSerializer | ||||
| from autotasks.models import TaskResult | ||||
| from checks.models import CheckResult | ||||
| from clients.models import Client | ||||
| from clients.serializers import ClientMinimumSerializer, SiteMinimumSerializer | ||||
| from rest_framework.serializers import ( | ||||
|     ModelSerializer, | ||||
|     ReadOnlyField, | ||||
|     SerializerMethodField, | ||||
| ) | ||||
|  | ||||
| from agents.serializers import AgentHostnameSerializer | ||||
| from autotasks.models import TaskResult | ||||
| from checks.models import CheckResult | ||||
| from clients.models import Client | ||||
| from clients.serializers import ClientMinimumSerializer, SiteMinimumSerializer | ||||
| from winupdate.serializers import WinUpdatePolicySerializer | ||||
|  | ||||
| from .models import Policy | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
| from itertools import cycle | ||||
| from unittest.mock import patch | ||||
|  | ||||
| from model_bakery import baker, seq | ||||
|  | ||||
| from agents.models import Agent | ||||
| from core.utils import get_core_settings | ||||
| from model_bakery import baker, seq | ||||
| from winupdate.models import WinUpdatePolicy | ||||
|  | ||||
| from tacticalrmm.test import TacticalTestCase | ||||
| from winupdate.models import WinUpdatePolicy | ||||
|  | ||||
| from .serializers import ( | ||||
|     PolicyCheckStatusSerializer, | ||||
| @@ -410,11 +410,11 @@ class TestPolicyTasks(TacticalTestCase): | ||||
|         ) | ||||
|  | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|         self.assertEquals(len(resp.data["server_clients"]), 1) | ||||
|         self.assertEquals(len(resp.data["server_sites"]), 0) | ||||
|         self.assertEquals(len(resp.data["workstation_clients"]), 1) | ||||
|         self.assertEquals(len(resp.data["workstation_sites"]), 0) | ||||
|         self.assertEquals(len(resp.data["agents"]), 0) | ||||
|         self.assertEqual(len(resp.data["server_clients"]), 1) | ||||
|         self.assertEqual(len(resp.data["server_sites"]), 0) | ||||
|         self.assertEqual(len(resp.data["workstation_clients"]), 1) | ||||
|         self.assertEqual(len(resp.data["workstation_sites"]), 0) | ||||
|         self.assertEqual(len(resp.data["agents"]), 0) | ||||
|  | ||||
|         # Add Site to Policy | ||||
|         policy.server_sites.add(server_agents[10].site) | ||||
| @@ -422,9 +422,9 @@ class TestPolicyTasks(TacticalTestCase): | ||||
|         resp = self.client.get( | ||||
|             f"/automation/policies/{policy.pk}/related/", format="json" | ||||
|         ) | ||||
|         self.assertEquals(len(resp.data["server_sites"]), 1) | ||||
|         self.assertEquals(len(resp.data["workstation_sites"]), 1) | ||||
|         self.assertEquals(len(resp.data["agents"]), 0) | ||||
|         self.assertEqual(len(resp.data["server_sites"]), 1) | ||||
|         self.assertEqual(len(resp.data["workstation_sites"]), 1) | ||||
|         self.assertEqual(len(resp.data["agents"]), 0) | ||||
|  | ||||
|         # Add Agent to Policy | ||||
|         policy.agents.add(server_agents[2]) | ||||
| @@ -432,7 +432,7 @@ class TestPolicyTasks(TacticalTestCase): | ||||
|         resp = self.client.get( | ||||
|             f"/automation/policies/{policy.pk}/related/", format="json" | ||||
|         ) | ||||
|         self.assertEquals(len(resp.data["agents"]), 2) | ||||
|         self.assertEqual(len(resp.data["agents"]), 2) | ||||
|  | ||||
|     def test_getting_agent_policy_checks(self): | ||||
|  | ||||
| @@ -442,7 +442,7 @@ class TestPolicyTasks(TacticalTestCase): | ||||
|         agent = baker.make_recipe("agents.agent", policy=policy) | ||||
|  | ||||
|         # test policy assigned to agent | ||||
|         self.assertEquals(len(agent.get_checks_from_policies()), 7) | ||||
|         self.assertEqual(len(agent.get_checks_from_policies()), 7) | ||||
|  | ||||
|     def test_getting_agent_policy_checks_with_enforced(self): | ||||
|         # setup data | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| from django.urls import path | ||||
|  | ||||
| from autotasks.views import GetAddAutoTasks | ||||
| from checks.views import GetAddChecks | ||||
| from django.urls import path | ||||
|  | ||||
| from . import views | ||||
|  | ||||
|   | ||||
| @@ -1,17 +1,17 @@ | ||||
| from agents.models import Agent | ||||
| from autotasks.models import TaskResult | ||||
| from checks.models import CheckResult | ||||
| from clients.models import Client | ||||
| from django.shortcuts import get_object_or_404 | ||||
| from rest_framework.exceptions import PermissionDenied | ||||
| from rest_framework.permissions import IsAuthenticated | ||||
| from rest_framework.response import Response | ||||
| from rest_framework.views import APIView | ||||
|  | ||||
| from agents.models import Agent | ||||
| from autotasks.models import TaskResult | ||||
| from checks.models import CheckResult | ||||
| from clients.models import Client | ||||
| from tacticalrmm.permissions import _has_perm_on_client, _has_perm_on_site | ||||
| from winupdate.models import WinUpdatePolicy | ||||
| from winupdate.serializers import WinUpdatePolicySerializer | ||||
|  | ||||
| from tacticalrmm.permissions import _has_perm_on_client, _has_perm_on_site | ||||
|  | ||||
| from .models import Policy | ||||
| from .permissions import AutomationPolicyPerms | ||||
| from .serializers import ( | ||||
| @@ -28,7 +28,9 @@ class GetAddPolicies(APIView): | ||||
|     permission_classes = [IsAuthenticated, AutomationPolicyPerms] | ||||
|  | ||||
|     def get(self, request): | ||||
|         policies = Policy.objects.all() | ||||
|         policies = Policy.objects.select_related("alert_template").prefetch_related( | ||||
|             "excluded_agents", "excluded_sites", "excluded_clients" | ||||
|         ) | ||||
|  | ||||
|         return Response( | ||||
|             PolicyTableSerializer( | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| from autotasks.tasks import remove_orphaned_win_tasks | ||||
| from django.core.management.base import BaseCommand | ||||
|  | ||||
| from autotasks.tasks import remove_orphaned_win_tasks | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     help = "Checks for orphaned tasks on all agents and removes them" | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| # Generated by Django 3.2.12 on 2022-04-01 22:44 | ||||
|  | ||||
| from django.db import migrations, models | ||||
| import django.db.models.deletion | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|   | ||||
| @@ -1,8 +1,9 @@ | ||||
| # Generated by Django 4.0.3 on 2022-04-15 20:52 | ||||
|  | ||||
| import autotasks.models | ||||
| from django.db import migrations, models | ||||
|  | ||||
| import autotasks.models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|   | ||||
| @@ -1,22 +1,24 @@ | ||||
| import asyncio | ||||
| import random | ||||
| import string | ||||
| import pytz | ||||
| from typing import TYPE_CHECKING, List, Dict, Any, Optional, Union | ||||
| from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union | ||||
|  | ||||
| from alerts.models import SEVERITY_CHOICES | ||||
| from django.core.validators import MaxValueValidator, MinValueValidator | ||||
| import pytz | ||||
| from django.core.cache import cache | ||||
| from django.utils import timezone as djangotime | ||||
| from django.core.validators import MaxValueValidator, MinValueValidator | ||||
| from django.db import models | ||||
| from django.db.models.fields import DateTimeField | ||||
| from django.db.models.fields.json import JSONField | ||||
| from django.db.utils import DatabaseError | ||||
| from logs.models import BaseAuditModel, DebugLog | ||||
| from django.utils import timezone as djangotime | ||||
|  | ||||
| from alerts.models import SEVERITY_CHOICES | ||||
| from core.utils import get_core_settings | ||||
| from logs.models import BaseAuditModel, DebugLog | ||||
| from tacticalrmm.constants import ( | ||||
|     FIELDS_TRIGGER_TASK_UPDATE_AGENT, | ||||
|     POLICY_TASK_FIELDS_TO_COPY, | ||||
|     DebugLogType, | ||||
| ) | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
| @@ -366,7 +368,7 @@ class AutomatedTask(BaseAuditModel): | ||||
|             task_result.save(update_fields=["sync_status"]) | ||||
|             DebugLog.warning( | ||||
|                 agent=agent, | ||||
|                 log_type="agent_issues", | ||||
|                 log_type=DebugLogType.AGENT_ISSUES, | ||||
|                 message=f"Unable to create scheduled task {self.name} on {task_result.agent.hostname}. It will be created when the agent checks in.", | ||||
|             ) | ||||
|             return "timeout" | ||||
| @@ -375,7 +377,7 @@ class AutomatedTask(BaseAuditModel): | ||||
|             task_result.save(update_fields=["sync_status"]) | ||||
|             DebugLog.info( | ||||
|                 agent=agent, | ||||
|                 log_type="agent_issues", | ||||
|                 log_type=DebugLogType.AGENT_ISSUES, | ||||
|                 message=f"{task_result.agent.hostname} task {self.name} was successfully created", | ||||
|             ) | ||||
|  | ||||
| @@ -405,7 +407,7 @@ class AutomatedTask(BaseAuditModel): | ||||
|             task_result.save(update_fields=["sync_status"]) | ||||
|             DebugLog.warning( | ||||
|                 agent=agent, | ||||
|                 log_type="agent_issues", | ||||
|                 log_type=DebugLogType.AGENT_ISSUES, | ||||
|                 message=f"Unable to modify scheduled task {self.name} on {task_result.agent.hostname}({task_result.agent.agent_id}). It will try again on next agent checkin", | ||||
|             ) | ||||
|             return "timeout" | ||||
| @@ -414,7 +416,7 @@ class AutomatedTask(BaseAuditModel): | ||||
|             task_result.save(update_fields=["sync_status"]) | ||||
|             DebugLog.info( | ||||
|                 agent=agent, | ||||
|                 log_type="agent_issues", | ||||
|                 log_type=DebugLogType.AGENT_ISSUES, | ||||
|                 message=f"{task_result.agent.hostname} task {self.name} was successfully modified", | ||||
|             ) | ||||
|  | ||||
| @@ -448,7 +450,7 @@ class AutomatedTask(BaseAuditModel): | ||||
|  | ||||
|             DebugLog.warning( | ||||
|                 agent=agent, | ||||
|                 log_type="agent_issues", | ||||
|                 log_type=DebugLogType.AGENT_ISSUES, | ||||
|                 message=f"{task_result.agent.hostname} task {self.name} will be deleted on next checkin", | ||||
|             ) | ||||
|             return "timeout" | ||||
| @@ -456,7 +458,7 @@ class AutomatedTask(BaseAuditModel): | ||||
|             self.delete() | ||||
|             DebugLog.info( | ||||
|                 agent=agent, | ||||
|                 log_type="agent_issues", | ||||
|                 log_type=DebugLogType.AGENT_ISSUES, | ||||
|                 message=f"{task_result.agent.hostname}({task_result.agent.agent_id}) task {self.name} was deleted", | ||||
|             ) | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| from rest_framework import serializers | ||||
|  | ||||
| from scripts.models import Script | ||||
| from django.core.exceptions import ObjectDoesNotExist | ||||
|  | ||||
| from .models import AutomatedTask, TaskResult | ||||
|  | ||||
| @@ -9,6 +9,7 @@ class TaskResultSerializer(serializers.ModelSerializer): | ||||
|     class Meta: | ||||
|         model = TaskResult | ||||
|         fields = "__all__" | ||||
|         read_only_fields = ("agent", "task") | ||||
|  | ||||
|  | ||||
| class TaskSerializer(serializers.ModelSerializer): | ||||
| @@ -198,13 +199,14 @@ class TaskGOGetSerializer(serializers.ModelSerializer): | ||||
|     def get_task_actions(self, obj): | ||||
|         tmp = [] | ||||
|         actions_to_remove = [] | ||||
|         agent = self.context["agent"] | ||||
|         for action in obj.actions: | ||||
|             if action["type"] == "cmd": | ||||
|                 tmp.append( | ||||
|                     { | ||||
|                         "type": "cmd", | ||||
|                         "command": Script.parse_script_args( | ||||
|                             agent=obj.agent, | ||||
|                             agent=agent, | ||||
|                             shell=action["shell"], | ||||
|                             args=[action["command"]], | ||||
|                         )[0], | ||||
| @@ -215,7 +217,7 @@ class TaskGOGetSerializer(serializers.ModelSerializer): | ||||
|             elif action["type"] == "script": | ||||
|                 try: | ||||
|                     script = Script.objects.get(pk=action["script"]) | ||||
|                 except ObjectDoesNotExist: | ||||
|                 except Script.DoesNotExist: | ||||
|                     # script doesn't exist so remove it | ||||
|                     actions_to_remove.append(action["script"]) | ||||
|                     continue | ||||
| @@ -225,7 +227,7 @@ class TaskGOGetSerializer(serializers.ModelSerializer): | ||||
|                         "script_name": script.name, | ||||
|                         "code": script.code, | ||||
|                         "script_args": Script.parse_script_args( | ||||
|                             agent=obj.agent, | ||||
|                             agent=agent, | ||||
|                             shell=script.shell, | ||||
|                             args=action["script_args"], | ||||
|                         ), | ||||
|   | ||||
| @@ -2,15 +2,16 @@ import asyncio | ||||
| import datetime as dt | ||||
| import random | ||||
| from time import sleep | ||||
| from typing import Union, Optional | ||||
| from typing import Optional, Union | ||||
|  | ||||
| from autotasks.models import AutomatedTask | ||||
| from agents.models import Agent | ||||
| from django.utils import timezone as djangotime | ||||
| from logs.models import DebugLog | ||||
|  | ||||
| from agents.models import Agent | ||||
| from alerts.models import Alert | ||||
| from autotasks.models import TaskResult | ||||
| from autotasks.models import AutomatedTask, TaskResult | ||||
| from logs.models import DebugLog | ||||
| from tacticalrmm.celery import app | ||||
| from tacticalrmm.constants import DebugLogType | ||||
|  | ||||
|  | ||||
| @app.task | ||||
| @@ -83,7 +84,7 @@ def remove_orphaned_win_tasks() -> None: | ||||
|         if not isinstance(r, list):  # empty list | ||||
|             DebugLog.error( | ||||
|                 agent=agent, | ||||
|                 log_type="agent_issues", | ||||
|                 log_type=DebugLogType.AGENT_ISSUES, | ||||
|                 message=f"Unable to pull list of scheduled tasks on {agent.hostname}: {r}", | ||||
|             ) | ||||
|             return | ||||
| @@ -114,13 +115,13 @@ def remove_orphaned_win_tasks() -> None: | ||||
|                 if ret != "ok": | ||||
|                     DebugLog.error( | ||||
|                         agent=agent, | ||||
|                         log_type="agent_issues", | ||||
|                         log_type=DebugLogType.AGENT_ISSUES, | ||||
|                         message=f"Unable to clean up orphaned task {task} on {agent.hostname}: {ret}", | ||||
|                     ) | ||||
|                 else: | ||||
|                     DebugLog.info( | ||||
|                         agent=agent, | ||||
|                         log_type="agent_issues", | ||||
|                         log_type=DebugLogType.AGENT_ISSUES, | ||||
|                         message=f"Removed orphaned task {task} from {agent.hostname}", | ||||
|                     ) | ||||
|  | ||||
|   | ||||
| @@ -1,11 +1,11 @@ | ||||
| from agents.models import Agent | ||||
| from automation.models import Policy | ||||
| from django.shortcuts import get_object_or_404 | ||||
| from rest_framework.exceptions import PermissionDenied | ||||
| from rest_framework.permissions import IsAuthenticated | ||||
| from rest_framework.response import Response | ||||
| from rest_framework.views import APIView | ||||
|  | ||||
| from agents.models import Agent | ||||
| from automation.models import Policy | ||||
| from tacticalrmm.permissions import _has_perm_on_agent | ||||
|  | ||||
| from .models import AutomatedTask | ||||
|   | ||||
| @@ -1,23 +1,25 @@ | ||||
| from model_bakery.recipe import Recipe | ||||
|  | ||||
| from tacticalrmm.constants import CheckType, EvtLogTypes | ||||
|  | ||||
| check = Recipe("checks.Check") | ||||
|  | ||||
| diskspace_check = check.extend( | ||||
|     check_type="diskspace", disk="C:", warning_threshold=30, error_threshold=10 | ||||
|     check_type=CheckType.DISK_SPACE, disk="C:", warning_threshold=30, error_threshold=10 | ||||
| ) | ||||
|  | ||||
| cpuload_check = check.extend( | ||||
|     check_type="cpuload", warning_threshold=30, error_threshold=75 | ||||
|     check_type=CheckType.CPU_LOAD, warning_threshold=30, error_threshold=75 | ||||
| ) | ||||
|  | ||||
| ping_check = check.extend(check_type="ping", ip="10.10.10.10") | ||||
|  | ||||
| memory_check = check.extend( | ||||
|     check_type="memory", warning_threshold=60, error_threshold=75 | ||||
|     check_type=CheckType.MEMORY, warning_threshold=60, error_threshold=75 | ||||
| ) | ||||
|  | ||||
| winsvc_check = check.extend( | ||||
|     check_type="winsvc", | ||||
|     check_type=CheckType.WINSVC, | ||||
|     svc_name="ServiceName", | ||||
|     svc_display_name="ServiceName", | ||||
|     svc_policy_mode="manual", | ||||
| @@ -25,9 +27,9 @@ winsvc_check = check.extend( | ||||
| ) | ||||
|  | ||||
| eventlog_check = check.extend( | ||||
|     check_type="eventlog", event_id=5000, event_type="application" | ||||
|     check_type=CheckType.EVENT_LOG, event_id=5000, event_type=EvtLogTypes.INFO | ||||
| ) | ||||
|  | ||||
| script_check = check.extend( | ||||
|     name="Script Name", check_type="script", script__name="Script Name" | ||||
|     name="Script Name", check_type=CheckType.SCRIPT, script__name="Script Name" | ||||
| ) | ||||
|   | ||||
							
								
								
									
										26
									
								
								api/tacticalrmm/checks/constants.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								api/tacticalrmm/checks/constants.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| CHECK_DEFER = ( | ||||
|     "created_by", | ||||
|     "created_time", | ||||
|     "modified_by", | ||||
|     "modified_time", | ||||
|     "timeout", | ||||
|     "svc_display_name", | ||||
|     "svc_policy_mode", | ||||
|     "log_name", | ||||
|     "event_id", | ||||
|     "event_id_is_wildcard", | ||||
|     "event_type", | ||||
|     "event_source", | ||||
|     "event_message", | ||||
|     "fail_when", | ||||
|     "search_last_days", | ||||
| ) | ||||
|  | ||||
| CHECK_RESULT_DEFER = ( | ||||
|     "more_info", | ||||
|     "outage_history", | ||||
|     "extra_details", | ||||
|     "stdout", | ||||
|     "stderr", | ||||
|     "execution_time", | ||||
| ) | ||||
| @@ -1,8 +1,8 @@ | ||||
| # Generated by Django 3.2.12 on 2022-04-01 22:44 | ||||
|  | ||||
| import django.contrib.postgres.fields | ||||
| from django.db import migrations, models | ||||
| import django.db.models.deletion | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|   | ||||
| @@ -3,6 +3,8 @@ | ||||
| from django.db import migrations, transaction | ||||
| from django.db.utils import IntegrityError | ||||
|  | ||||
| from tacticalrmm.constants import CheckType | ||||
|  | ||||
|  | ||||
| def migrate_check_results(apps, schema_editor): | ||||
|     Check = apps.get_model("checks", "Check") | ||||
| @@ -28,7 +30,12 @@ def migrate_check_results(apps, schema_editor): | ||||
|                         history=check.history, | ||||
|                         alert_severity=check.alert_severity | ||||
|                         if check.check_type | ||||
|                         in ["cpuload", "memory", "diskspace", "script"] | ||||
|                         in [ | ||||
|                             CheckType.MEMORY, | ||||
|                             CheckType.CPU_LOAD, | ||||
|                             CheckType.DISK_SPACE, | ||||
|                             CheckType.SCRIPT, | ||||
|                         ] | ||||
|                         else None, | ||||
|                     ) | ||||
|  | ||||
| @@ -49,7 +56,12 @@ def migrate_check_results(apps, schema_editor): | ||||
|                         history=check.history, | ||||
|                         alert_severity=check.alert_severity | ||||
|                         if check.check_type | ||||
|                         in ["cpuload", "memory", "diskspace", "script"] | ||||
|                         in [ | ||||
|                             CheckType.MEMORY, | ||||
|                             CheckType.CPU_LOAD, | ||||
|                             CheckType.DISK_SPACE, | ||||
|                             CheckType.SCRIPT, | ||||
|                         ] | ||||
|                         else None, | ||||
|                     ) | ||||
|         except IntegrityError: | ||||
|   | ||||
| @@ -1,59 +1,31 @@ | ||||
| from statistics import mean | ||||
| from typing import TYPE_CHECKING, Any, Union, Dict, Optional | ||||
| from typing import TYPE_CHECKING, Any, Dict, Optional, Union | ||||
|  | ||||
| from django.core.cache import cache | ||||
| from alerts.models import SEVERITY_CHOICES | ||||
| from django.contrib.postgres.fields import ArrayField | ||||
| from django.core.cache import cache | ||||
| from django.core.validators import MaxValueValidator, MinValueValidator | ||||
| from django.db import models | ||||
| from logs.models import BaseAuditModel | ||||
| from django.utils import timezone as djangotime | ||||
|  | ||||
| from tacticalrmm.models import PermissionQuerySet | ||||
| from alerts.models import SEVERITY_CHOICES | ||||
| from core.utils import get_core_settings | ||||
| from logs.models import BaseAuditModel | ||||
| from tacticalrmm.constants import ( | ||||
|     CHECKS_NON_EDITABLE_FIELDS, | ||||
|     POLICY_CHECK_FIELDS_TO_COPY, | ||||
|     CheckStatus, | ||||
|     CheckType, | ||||
|     EvtLogFailWhen, | ||||
|     EvtLogNames, | ||||
|     EvtLogTypes, | ||||
| ) | ||||
| from tacticalrmm.models import PermissionQuerySet | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|     from agents.models import Agent  # pragma: no cover | ||||
|     from alerts.models import Alert, AlertTemplate  # pragma: no cover | ||||
|     from automation.models import Policy  # pragma: no cover | ||||
|  | ||||
| CHECK_TYPE_CHOICES = [ | ||||
|     ("diskspace", "Disk Space Check"), | ||||
|     ("ping", "Ping Check"), | ||||
|     ("cpuload", "CPU Load Check"), | ||||
|     ("memory", "Memory Check"), | ||||
|     ("winsvc", "Service Check"), | ||||
|     ("script", "Script Check"), | ||||
|     ("eventlog", "Event Log Check"), | ||||
| ] | ||||
|  | ||||
| CHECK_STATUS_CHOICES = [ | ||||
|     ("passing", "Passing"), | ||||
|     ("failing", "Failing"), | ||||
|     ("pending", "Pending"), | ||||
| ] | ||||
|  | ||||
| EVT_LOG_NAME_CHOICES = [ | ||||
|     ("Application", "Application"), | ||||
|     ("System", "System"), | ||||
|     ("Security", "Security"), | ||||
| ] | ||||
|  | ||||
| EVT_LOG_TYPE_CHOICES = [ | ||||
|     ("INFO", "Information"), | ||||
|     ("WARNING", "Warning"), | ||||
|     ("ERROR", "Error"), | ||||
|     ("AUDIT_SUCCESS", "Success Audit"), | ||||
|     ("AUDIT_FAILURE", "Failure Audit"), | ||||
| ] | ||||
|  | ||||
| EVT_LOG_FAIL_WHEN_CHOICES = [ | ||||
|     ("contains", "Log contains"), | ||||
|     ("not_contains", "Log does not contain"), | ||||
| ] | ||||
|  | ||||
|  | ||||
| class Check(BaseAuditModel): | ||||
|     objects = PermissionQuerySet.as_manager() | ||||
| @@ -77,7 +49,7 @@ class Check(BaseAuditModel): | ||||
|     overridden_by_policy = models.BooleanField(default=False) | ||||
|     name = models.CharField(max_length=255, null=True, blank=True) | ||||
|     check_type = models.CharField( | ||||
|         max_length=50, choices=CHECK_TYPE_CHOICES, default="diskspace" | ||||
|         max_length=50, choices=CheckType.choices, default=CheckType.DISK_SPACE | ||||
|     ) | ||||
|     email_alert = models.BooleanField(default=False) | ||||
|     text_alert = models.BooleanField(default=False) | ||||
| @@ -151,17 +123,17 @@ class Check(BaseAuditModel): | ||||
|  | ||||
|     # event log checks | ||||
|     log_name = models.CharField( | ||||
|         max_length=255, choices=EVT_LOG_NAME_CHOICES, null=True, blank=True | ||||
|         max_length=255, choices=EvtLogNames.choices, null=True, blank=True | ||||
|     ) | ||||
|     event_id = models.IntegerField(null=True, blank=True) | ||||
|     event_id_is_wildcard = models.BooleanField(default=False) | ||||
|     event_type = models.CharField( | ||||
|         max_length=255, choices=EVT_LOG_TYPE_CHOICES, null=True, blank=True | ||||
|         max_length=255, choices=EvtLogTypes.choices, null=True, blank=True | ||||
|     ) | ||||
|     event_source = models.CharField(max_length=255, null=True, blank=True) | ||||
|     event_message = models.TextField(null=True, blank=True) | ||||
|     fail_when = models.CharField( | ||||
|         max_length=255, choices=EVT_LOG_FAIL_WHEN_CHOICES, null=True, blank=True | ||||
|         max_length=255, choices=EvtLogFailWhen.choices, null=True, blank=True | ||||
|     ) | ||||
|     search_last_days = models.PositiveIntegerField(null=True, blank=True) | ||||
|     number_of_events_b4_alert = models.PositiveIntegerField( | ||||
| @@ -215,7 +187,7 @@ class Check(BaseAuditModel): | ||||
|     @property | ||||
|     def readable_desc(self): | ||||
|         display = self.get_check_type_display()  # type: ignore | ||||
|         if self.check_type == "diskspace": | ||||
|         if self.check_type == CheckType.DISK_SPACE: | ||||
|  | ||||
|             text = "" | ||||
|             if self.warning_threshold: | ||||
| @@ -224,9 +196,11 @@ class Check(BaseAuditModel): | ||||
|                 text += f" Error Threshold: {self.error_threshold}%" | ||||
|  | ||||
|             return f"{display}: Drive {self.disk} - {text}" | ||||
|         elif self.check_type == "ping": | ||||
|         elif self.check_type == CheckType.PING: | ||||
|             return f"{display}: {self.name}" | ||||
|         elif self.check_type == "cpuload" or self.check_type == "memory": | ||||
|         elif ( | ||||
|             self.check_type == CheckType.CPU_LOAD or self.check_type == CheckType.MEMORY | ||||
|         ): | ||||
|  | ||||
|             text = "" | ||||
|             if self.warning_threshold: | ||||
| @@ -235,11 +209,11 @@ class Check(BaseAuditModel): | ||||
|                 text += f" Error Threshold: {self.error_threshold}%" | ||||
|  | ||||
|             return f"{display} - {text}" | ||||
|         elif self.check_type == "winsvc": | ||||
|         elif self.check_type == CheckType.WINSVC: | ||||
|             return f"{display}: {self.svc_display_name}" | ||||
|         elif self.check_type == "eventlog": | ||||
|         elif self.check_type == CheckType.EVENT_LOG: | ||||
|             return f"{display}: {self.name}" | ||||
|         elif self.check_type == "script": | ||||
|         elif self.check_type == CheckType.SCRIPT: | ||||
|             return f"{display}: {self.script.name}" | ||||
|         else: | ||||
|             return "n/a" | ||||
| @@ -295,25 +269,22 @@ class Check(BaseAuditModel): | ||||
|         return CheckAuditSerializer(check).data | ||||
|  | ||||
|     def is_duplicate(self, check): | ||||
|         if self.check_type == "diskspace": | ||||
|         if self.check_type == CheckType.DISK_SPACE: | ||||
|             return self.disk == check.disk | ||||
|  | ||||
|         elif self.check_type == "script": | ||||
|         elif self.check_type == CheckType.SCRIPT: | ||||
|             return self.script == check.script | ||||
|  | ||||
|         elif self.check_type == "ping": | ||||
|         elif self.check_type == CheckType.PING: | ||||
|             return self.ip == check.ip | ||||
|  | ||||
|         elif self.check_type == "cpuload": | ||||
|         elif self.check_type in (CheckType.CPU_LOAD, CheckType.MEMORY): | ||||
|             return True | ||||
|  | ||||
|         elif self.check_type == "memory": | ||||
|             return True | ||||
|  | ||||
|         elif self.check_type == "winsvc": | ||||
|         elif self.check_type == CheckType.WINSVC: | ||||
|             return self.svc_name == check.svc_name | ||||
|  | ||||
|         elif self.check_type == "eventlog": | ||||
|         elif self.check_type == CheckType.EVENT_LOG: | ||||
|             return [self.log_name, self.event_id] == [check.log_name, check.event_id] | ||||
|  | ||||
|  | ||||
| @@ -335,7 +306,7 @@ class CheckResult(models.Model): | ||||
|         on_delete=models.CASCADE, | ||||
|     ) | ||||
|     status = models.CharField( | ||||
|         max_length=100, choices=CHECK_STATUS_CHOICES, default="pending" | ||||
|         max_length=100, choices=CheckStatus.choices, default=CheckStatus.PENDING | ||||
|     ) | ||||
|     # for memory, diskspace, script, and cpu checks where severity changes | ||||
|     alert_severity = models.CharField( | ||||
| @@ -365,10 +336,10 @@ class CheckResult(models.Model): | ||||
|  | ||||
|         # if check is a policy check clear cache on everything | ||||
|         if not self.alert_severity and self.assigned_check.check_type in [ | ||||
|             "cpuload", | ||||
|             "memory", | ||||
|             "diskspace", | ||||
|             "script", | ||||
|             CheckType.MEMORY, | ||||
|             CheckType.CPU_LOAD, | ||||
|             CheckType.DISK_SPACE, | ||||
|             CheckType.SCRIPT, | ||||
|         ]: | ||||
|             self.alert_severity = "warning" | ||||
|  | ||||
| @@ -379,10 +350,7 @@ class CheckResult(models.Model): | ||||
|  | ||||
|     @property | ||||
|     def history_info(self): | ||||
|         if ( | ||||
|             self.assigned_check.check_type == "cpuload" | ||||
|             or self.assigned_check.check_type == "memory" | ||||
|         ): | ||||
|         if self.assigned_check.check_type in (CheckType.CPU_LOAD, CheckType.MEMORY): | ||||
|             return ", ".join(str(f"{x}%") for x in self.history[-6:]) | ||||
|  | ||||
|     def get_or_create_alert_if_needed( | ||||
| @@ -397,68 +365,67 @@ class CheckResult(models.Model): | ||||
|             skip_create=not self.assigned_check.should_create_alert(alert_template), | ||||
|         ) | ||||
|  | ||||
|     def handle_check(self, data): | ||||
|     def handle_check(self, data, check: "Check", agent: "Agent"): | ||||
|         from alerts.models import Alert | ||||
|  | ||||
|         check = self.assigned_check | ||||
|  | ||||
|         update_fields = [] | ||||
|         # cpuload or mem checks | ||||
|         if check.check_type == "cpuload" or check.check_type == "memory": | ||||
|         if check.check_type in (CheckType.CPU_LOAD, CheckType.MEMORY): | ||||
|  | ||||
|             self.history.append(data["percent"]) | ||||
|  | ||||
|             if len(self.history) > 15: | ||||
|                 self.history = self.history[-15:] | ||||
|  | ||||
|             self.save(update_fields=["history"]) | ||||
|             update_fields.extend(["history"]) | ||||
|  | ||||
|             avg = int(mean(self.history)) | ||||
|  | ||||
|             if check.error_threshold and avg > check.error_threshold: | ||||
|                 self.status = "failing" | ||||
|                 self.status = CheckStatus.FAILING | ||||
|                 self.alert_severity = "error" | ||||
|             elif check.warning_threshold and avg > check.warning_threshold: | ||||
|                 self.status = "failing" | ||||
|                 self.status = CheckStatus.FAILING | ||||
|                 self.alert_severity = "warning" | ||||
|             else: | ||||
|                 self.status = "passing" | ||||
|                 self.status = CheckStatus.PASSING | ||||
|  | ||||
|             # add check history | ||||
|             check.add_check_history(data["percent"], self.agent.agent_id) | ||||
|             check.add_check_history(data["percent"], agent.agent_id) | ||||
|  | ||||
|         # diskspace checks | ||||
|         elif check.check_type == "diskspace": | ||||
|         elif check.check_type == CheckType.DISK_SPACE: | ||||
|             if data["exists"]: | ||||
|                 percent_used = round(data["percent_used"]) | ||||
|                 if ( | ||||
|                     check.error_threshold | ||||
|                     and (100 - percent_used) < check.error_threshold | ||||
|                 ): | ||||
|                     self.status = "failing" | ||||
|                     self.status = CheckStatus.FAILING | ||||
|                     self.alert_severity = "error" | ||||
|                 elif ( | ||||
|                     check.warning_threshold | ||||
|                     and (100 - percent_used) < check.warning_threshold | ||||
|                 ): | ||||
|                     self.status = "failing" | ||||
|                     self.status = CheckStatus.FAILING | ||||
|                     self.alert_severity = "warning" | ||||
|  | ||||
|                 else: | ||||
|                     self.status = "passing" | ||||
|                     self.status = CheckStatus.PASSING | ||||
|  | ||||
|                 self.more_info = data["more_info"] | ||||
|  | ||||
|                 # add check history | ||||
|                 check.add_check_history(100 - percent_used, self.agent.agent_id) | ||||
|                 check.add_check_history(100 - percent_used, agent.agent_id) | ||||
|             else: | ||||
|                 self.status = "failing" | ||||
|                 self.status = CheckStatus.FAILING | ||||
|                 self.alert_severity = "error" | ||||
|                 self.more_info = f"Disk {check.disk} does not exist" | ||||
|  | ||||
|             self.save(update_fields=["more_info"]) | ||||
|             update_fields.extend(["more_info"]) | ||||
|  | ||||
|         # script checks | ||||
|         elif check.check_type == "script": | ||||
|         elif check.check_type == CheckType.SCRIPT: | ||||
|             self.stdout = data["stdout"] | ||||
|             self.stderr = data["stderr"] | ||||
|             self.retcode = data["retcode"] | ||||
| @@ -466,18 +433,18 @@ class CheckResult(models.Model): | ||||
|  | ||||
|             if data["retcode"] in check.info_return_codes: | ||||
|                 self.alert_severity = "info" | ||||
|                 self.status = "failing" | ||||
|                 self.status = CheckStatus.FAILING | ||||
|             elif data["retcode"] in check.warning_return_codes: | ||||
|                 self.alert_severity = "warning" | ||||
|                 self.status = "failing" | ||||
|                 self.status = CheckStatus.FAILING | ||||
|             elif data["retcode"] != 0: | ||||
|                 self.status = "failing" | ||||
|                 self.status = CheckStatus.FAILING | ||||
|                 self.alert_severity = "error" | ||||
|             else: | ||||
|                 self.status = "passing" | ||||
|                 self.status = CheckStatus.PASSING | ||||
|  | ||||
|             self.save( | ||||
|                 update_fields=[ | ||||
|             update_fields.extend( | ||||
|                 [ | ||||
|                     "stdout", | ||||
|                     "stderr", | ||||
|                     "retcode", | ||||
| @@ -487,8 +454,8 @@ class CheckResult(models.Model): | ||||
|  | ||||
|             # add check history | ||||
|             check.add_check_history( | ||||
|                 1 if self.status == "failing" else 0, | ||||
|                 self.agent.agent_id, | ||||
|                 1 if self.status == CheckStatus.FAILING else 0, | ||||
|                 agent.agent_id, | ||||
|                 { | ||||
|                     "retcode": data["retcode"], | ||||
|                     "stdout": data["stdout"][:60], | ||||
| @@ -498,67 +465,73 @@ class CheckResult(models.Model): | ||||
|             ) | ||||
|  | ||||
|         # ping checks | ||||
|         elif check.check_type == "ping": | ||||
|         elif check.check_type == CheckType.PING: | ||||
|             self.status = data["status"] | ||||
|             self.more_info = data["output"] | ||||
|             self.save(update_fields=["more_info"]) | ||||
|             update_fields.extend(["more_info"]) | ||||
|  | ||||
|             check.add_check_history( | ||||
|                 1 if self.status == "failing" else 0, | ||||
|                 self.agent.agent_id, | ||||
|                 1 if self.status == CheckStatus.FAILING else 0, | ||||
|                 agent.agent_id, | ||||
|                 self.more_info[:60], | ||||
|             ) | ||||
|  | ||||
|         # windows service checks | ||||
|         elif check.check_type == "winsvc": | ||||
|         elif check.check_type == CheckType.WINSVC: | ||||
|             self.status = data["status"] | ||||
|             self.more_info = data["more_info"] | ||||
|             self.save(update_fields=["more_info"]) | ||||
|             update_fields.extend(["more_info"]) | ||||
|  | ||||
|             check.add_check_history( | ||||
|                 1 if self.status == "failing" else 0, | ||||
|                 self.agent.agent_id, | ||||
|                 1 if self.status == CheckStatus.FAILING else 0, | ||||
|                 agent.agent_id, | ||||
|                 self.more_info[:60], | ||||
|             ) | ||||
|  | ||||
|         elif check.check_type == "eventlog": | ||||
|         elif check.check_type == CheckType.EVENT_LOG: | ||||
|             log = data["log"] | ||||
|             if check.fail_when == "contains": | ||||
|             if check.fail_when == EvtLogFailWhen.CONTAINS: | ||||
|                 if log and len(log) >= check.number_of_events_b4_alert: | ||||
|                     self.status = "failing" | ||||
|                     self.status = CheckStatus.FAILING | ||||
|                 else: | ||||
|                     self.status = "passing" | ||||
|                     self.status = CheckStatus.PASSING | ||||
|  | ||||
|             elif check.fail_when == "not_contains": | ||||
|             elif check.fail_when == EvtLogFailWhen.NOT_CONTAINS: | ||||
|                 if log and len(log) >= check.number_of_events_b4_alert: | ||||
|                     self.status = "passing" | ||||
|                     self.status = CheckStatus.PASSING | ||||
|                 else: | ||||
|                     self.status = "failing" | ||||
|                     self.status = CheckStatus.FAILING | ||||
|  | ||||
|             self.extra_details = {"log": log} | ||||
|             self.save(update_fields=["extra_details"]) | ||||
|             update_fields.extend(["extra_details"]) | ||||
|  | ||||
|             check.add_check_history( | ||||
|                 1 if self.status == "failing" else 0, | ||||
|                 self.agent.agent_id, | ||||
|                 1 if self.status == CheckStatus.FAILING else 0, | ||||
|                 agent.agent_id, | ||||
|                 "Events Found:" + str(len(self.extra_details["log"])), | ||||
|             ) | ||||
|  | ||||
|         self.last_run = djangotime.now() | ||||
|         # handle status | ||||
|         if self.status == "failing": | ||||
|         if self.status == CheckStatus.FAILING: | ||||
|             self.fail_count += 1 | ||||
|             self.save(update_fields=["status", "fail_count", "alert_severity"]) | ||||
|             update_fields.extend(["status", "fail_count", "alert_severity", "last_run"]) | ||||
|             self.save(update_fields=update_fields) | ||||
|  | ||||
|             if self.fail_count >= check.fails_b4_alert: | ||||
|                 Alert.handle_alert_failure(self) | ||||
|  | ||||
|         elif self.status == "passing": | ||||
|         elif self.status == CheckStatus.PASSING: | ||||
|             self.fail_count = 0 | ||||
|             self.save(update_fields=["status", "fail_count", "alert_severity"]) | ||||
|             update_fields.extend(["status", "fail_count", "alert_severity", "last_run"]) | ||||
|             self.save(update_fields=update_fields) | ||||
|             if Alert.objects.filter( | ||||
|                 assigned_check=check, agent=self.agent, resolved=False | ||||
|                 assigned_check=check, agent=agent, resolved=False | ||||
|             ).exists(): | ||||
|                 Alert.handle_alert_resolve(self) | ||||
|         else: | ||||
|             update_fields.extend(["last_run"]) | ||||
|             self.save(update_fields=update_fields) | ||||
|  | ||||
|         return self.status | ||||
|  | ||||
| @@ -572,7 +545,7 @@ class CheckResult(models.Model): | ||||
|         else: | ||||
|             subject = f"{self} Failed" | ||||
|  | ||||
|         if self.assigned_check.check_type == "diskspace": | ||||
|         if self.assigned_check.check_type == CheckType.DISK_SPACE: | ||||
|             text = "" | ||||
|             if self.assigned_check.warning_threshold: | ||||
|                 text += f" Warning Threshold: {self.assigned_check.warning_threshold}%" | ||||
| @@ -591,21 +564,18 @@ class CheckResult(models.Model): | ||||
|             except: | ||||
|                 body = subject + f" - Disk {self.assigned_check.disk} does not exist" | ||||
|  | ||||
|         elif self.assigned_check.check_type == "script": | ||||
|         elif self.assigned_check.check_type == CheckType.SCRIPT: | ||||
|  | ||||
|             body = ( | ||||
|                 subject | ||||
|                 + f" - Return code: {self.retcode}\nStdout:{self.stdout}\nStderr: {self.stderr}" | ||||
|             ) | ||||
|  | ||||
|         elif self.assigned_check.check_type == "ping": | ||||
|         elif self.assigned_check.check_type == CheckType.PING: | ||||
|  | ||||
|             body = self.more_info | ||||
|  | ||||
|         elif ( | ||||
|             self.assigned_check.check_type == "cpuload" | ||||
|             or self.assigned_check.check_type == "memory" | ||||
|         ): | ||||
|         elif self.assigned_check.check_type in (CheckType.CPU_LOAD, CheckType.MEMORY): | ||||
|             text = "" | ||||
|             if self.assigned_check.warning_threshold: | ||||
|                 text += f" Warning Threshold: {self.assigned_check.warning_threshold}%" | ||||
| @@ -614,16 +584,16 @@ class CheckResult(models.Model): | ||||
|  | ||||
|             avg = int(mean(self.history)) | ||||
|  | ||||
|             if self.assigned_check.check_type == "cpuload": | ||||
|             if self.assigned_check.check_type == CheckType.CPU_LOAD: | ||||
|                 body = subject + f" - Average CPU utilization: {avg}%, {text}" | ||||
|  | ||||
|             elif self.assigned_check.check_type == "memory": | ||||
|             elif self.assigned_check.check_type == CheckType.MEMORY: | ||||
|                 body = subject + f" - Average memory usage: {avg}%, {text}" | ||||
|  | ||||
|         elif self.assigned_check.check_type == "winsvc": | ||||
|         elif self.assigned_check.check_type == CheckType.WINSVC: | ||||
|             body = subject + f" - Status: {self.more_info}" | ||||
|  | ||||
|         elif self.assigned_check.check_type == "eventlog": | ||||
|         elif self.assigned_check.check_type == CheckType.EVENT_LOG: | ||||
|  | ||||
|             if self.assigned_check.event_source and self.assigned_check.event_message: | ||||
|                 start = f"Event ID {self.assigned_check.event_id}, source {self.assigned_check.event_source}, containing string {self.assigned_check.event_message} " | ||||
| @@ -655,7 +625,7 @@ class CheckResult(models.Model): | ||||
|         else: | ||||
|             subject = f"{self} Failed" | ||||
|  | ||||
|         if self.assigned_check.check_type == "diskspace": | ||||
|         if self.assigned_check.check_type == CheckType.DISK_SPACE: | ||||
|             text = "" | ||||
|             if self.assigned_check.warning_threshold: | ||||
|                 text += f" Warning Threshold: {self.assigned_check.warning_threshold}%" | ||||
| @@ -673,14 +643,11 @@ class CheckResult(models.Model): | ||||
|             except: | ||||
|                 body = subject + f" - Disk {self.assigned_check.disk} does not exist" | ||||
|  | ||||
|         elif self.assigned_check.check_type == "script": | ||||
|         elif self.assigned_check.check_type == CheckType.SCRIPT: | ||||
|             body = subject + f" - Return code: {self.retcode}" | ||||
|         elif self.assigned_check.check_type == "ping": | ||||
|         elif self.assigned_check.check_type == CheckType.PING: | ||||
|             body = subject | ||||
|         elif ( | ||||
|             self.assigned_check.check_type == "cpuload" | ||||
|             or self.assigned_check.check_type == "memory" | ||||
|         ): | ||||
|         elif self.assigned_check.check_type in (CheckType.CPU_LOAD, CheckType.MEMORY): | ||||
|             text = "" | ||||
|             if self.assigned_check.warning_threshold: | ||||
|                 text += f" Warning Threshold: {self.assigned_check.warning_threshold}%" | ||||
| @@ -688,13 +655,13 @@ class CheckResult(models.Model): | ||||
|                 text += f" Error Threshold: {self.assigned_check.error_threshold}%" | ||||
|  | ||||
|             avg = int(mean(self.history)) | ||||
|             if self.assigned_check.check_type == "cpuload": | ||||
|             if self.assigned_check.check_type == CheckType.CPU_LOAD: | ||||
|                 body = subject + f" - Average CPU utilization: {avg}%, {text}" | ||||
|             elif self.assigned_check.check_type == "memory": | ||||
|             elif self.assigned_check.check_type == CheckType.MEMORY: | ||||
|                 body = subject + f" - Average memory usage: {avg}%, {text}" | ||||
|         elif self.assigned_check.check_type == "winsvc": | ||||
|         elif self.assigned_check.check_type == CheckType.WINSVC: | ||||
|             body = subject + f" - Status: {self.more_info}" | ||||
|         elif self.assigned_check.check_type == "eventlog": | ||||
|         elif self.assigned_check.check_type == CheckType.EVENT_LOG: | ||||
|             body = subject | ||||
|  | ||||
|         CORE.send_sms(body, alert_template=self.agent.alert_template) | ||||
|   | ||||
| @@ -1,8 +1,10 @@ | ||||
| import validators as _v | ||||
| from autotasks.models import AutomatedTask | ||||
| from rest_framework import serializers | ||||
|  | ||||
| from autotasks.models import AutomatedTask | ||||
| from scripts.models import Script | ||||
| from scripts.serializers import ScriptCheckSerializer | ||||
| from tacticalrmm.constants import CheckType | ||||
|  | ||||
| from .models import Check, CheckHistory, CheckResult | ||||
|  | ||||
| @@ -67,9 +69,11 @@ class CheckSerializer(serializers.ModelSerializer): | ||||
|  | ||||
|         # disk checks | ||||
|         # make sure no duplicate diskchecks exist for an agent/policy | ||||
|         if check_type == "diskspace": | ||||
|         if check_type == CheckType.DISK_SPACE: | ||||
|             if not self.instance:  # only on create | ||||
|                 checks = Check.objects.filter(**filter).filter(check_type="diskspace") | ||||
|                 checks = Check.objects.filter(**filter).filter( | ||||
|                     check_type=CheckType.DISK_SPACE | ||||
|                 ) | ||||
|                 for check in checks: | ||||
|                     if val["disk"] in check.disk: | ||||
|                         raise serializers.ValidationError( | ||||
| @@ -91,7 +95,7 @@ class CheckSerializer(serializers.ModelSerializer): | ||||
|                 ) | ||||
|  | ||||
|         # ping checks | ||||
|         if check_type == "ping": | ||||
|         if check_type == CheckType.PING: | ||||
|             if ( | ||||
|                 not _v.ipv4(val["ip"]) | ||||
|                 and not _v.ipv6(val["ip"]) | ||||
| @@ -101,8 +105,8 @@ class CheckSerializer(serializers.ModelSerializer): | ||||
|                     "Please enter a valid IP address or domain name" | ||||
|                 ) | ||||
|  | ||||
|         if check_type == "cpuload" and not self.instance: | ||||
|             if Check.objects.filter(**filter, check_type="cpuload").exists(): | ||||
|         if check_type == CheckType.CPU_LOAD and not self.instance: | ||||
|             if Check.objects.filter(**filter, check_type=CheckType.CPU_LOAD).exists(): | ||||
|                 raise serializers.ValidationError( | ||||
|                     "A cpuload check for this agent already exists" | ||||
|                 ) | ||||
| @@ -121,8 +125,8 @@ class CheckSerializer(serializers.ModelSerializer): | ||||
|                     f"Warning threshold must be less than Error Threshold" | ||||
|                 ) | ||||
|  | ||||
|         if check_type == "memory" and not self.instance: | ||||
|             if Check.objects.filter(**filter, check_type="memory").exists(): | ||||
|         if check_type == CheckType.MEMORY and not self.instance: | ||||
|             if Check.objects.filter(**filter, check_type=CheckType.MEMORY).exists(): | ||||
|                 raise serializers.ValidationError( | ||||
|                     "A memory check for this agent already exists" | ||||
|                 ) | ||||
| @@ -156,11 +160,12 @@ class CheckRunnerGetSerializer(serializers.ModelSerializer): | ||||
|     script_args = serializers.SerializerMethodField() | ||||
|  | ||||
|     def get_script_args(self, obj): | ||||
|         if obj.check_type != "script": | ||||
|         if obj.check_type != CheckType.SCRIPT: | ||||
|             return [] | ||||
|  | ||||
|         agent = self.context["agent"] if "agent" in self.context.keys() else obj.agent | ||||
|         return Script.parse_script_args( | ||||
|             agent=obj.agent, shell=obj.script.shell, args=obj.script_args | ||||
|             agent=agent, shell=obj.script.shell, args=obj.script_args | ||||
|         ) | ||||
|  | ||||
|     class Meta: | ||||
|   | ||||
| @@ -7,7 +7,6 @@ from django.utils import timezone as djangotime | ||||
|  | ||||
| from alerts.models import Alert | ||||
| from checks.models import CheckResult | ||||
|  | ||||
| from tacticalrmm.celery import app | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -1,11 +1,11 @@ | ||||
| from unittest.mock import patch | ||||
|  | ||||
| from checks.models import CheckHistory, CheckResult | ||||
| from django.conf import settings | ||||
| from django.test import modify_settings | ||||
| from django.utils import timezone as djangotime | ||||
| from model_bakery import baker | ||||
|  | ||||
| from checks.models import CheckHistory, CheckResult | ||||
| from tacticalrmm.constants import CheckStatus, CheckType, EvtLogFailWhen, EvtLogTypes | ||||
| from tacticalrmm.test import TacticalTestCase | ||||
|  | ||||
| from .serializers import CheckSerializer | ||||
| @@ -13,11 +13,6 @@ from .serializers import CheckSerializer | ||||
| base_url = "/checks" | ||||
|  | ||||
|  | ||||
| @modify_settings( | ||||
|     MIDDLEWARE={ | ||||
|         "remove": "tacticalrmm.middleware.LinuxMiddleware", | ||||
|     } | ||||
| ) | ||||
| class TestCheckViews(TacticalTestCase): | ||||
|     def setUp(self): | ||||
|         self.authenticate() | ||||
| @@ -84,7 +79,7 @@ class TestCheckViews(TacticalTestCase): | ||||
|  | ||||
|         agent_payload = { | ||||
|             "agent": agent.agent_id, | ||||
|             "check_type": "diskspace", | ||||
|             "check_type": CheckType.DISK_SPACE, | ||||
|             "disk": "C:", | ||||
|             "error_threshold": 55, | ||||
|             "warning_threshold": 0, | ||||
| @@ -93,7 +88,7 @@ class TestCheckViews(TacticalTestCase): | ||||
|  | ||||
|         policy_payload = { | ||||
|             "policy": policy.id, | ||||
|             "check_type": "diskspace", | ||||
|             "check_type": CheckType.DISK_SPACE, | ||||
|             "disk": "C:", | ||||
|             "error_threshold": 55, | ||||
|             "warning_threshold": 0, | ||||
| @@ -133,7 +128,7 @@ class TestCheckViews(TacticalTestCase): | ||||
|  | ||||
|         agent_payload = { | ||||
|             "agent": agent.agent_id, | ||||
|             "check_type": "cpuload", | ||||
|             "check_type": CheckType.CPU_LOAD, | ||||
|             "error_threshold": 66, | ||||
|             "warning_threshold": 0, | ||||
|             "fails_b4_alert": 9, | ||||
| @@ -141,7 +136,7 @@ class TestCheckViews(TacticalTestCase): | ||||
|  | ||||
|         policy_payload = { | ||||
|             "policy": policy.id, | ||||
|             "check_type": "cpuload", | ||||
|             "check_type": CheckType.CPU_LOAD, | ||||
|             "error_threshold": 66, | ||||
|             "warning_threshold": 0, | ||||
|             "fails_b4_alert": 9, | ||||
| @@ -180,7 +175,7 @@ class TestCheckViews(TacticalTestCase): | ||||
|  | ||||
|         agent_payload = { | ||||
|             "agent": agent.agent_id, | ||||
|             "check_type": "memory", | ||||
|             "check_type": CheckType.MEMORY, | ||||
|             "error_threshold": 78, | ||||
|             "warning_threshold": 0, | ||||
|             "fails_b4_alert": 1, | ||||
| @@ -188,7 +183,7 @@ class TestCheckViews(TacticalTestCase): | ||||
|  | ||||
|         policy_payload = { | ||||
|             "policy": policy.id, | ||||
|             "check_type": "memory", | ||||
|             "check_type": CheckType.MEMORY, | ||||
|             "error_threshold": 78, | ||||
|             "warning_threshold": 0, | ||||
|             "fails_b4_alert": 1, | ||||
| @@ -347,7 +342,7 @@ class TestCheckTasks(TacticalTestCase): | ||||
|  | ||||
|         check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) | ||||
|  | ||||
|         self.assertEqual(check_result.status, "failing") | ||||
|         self.assertEqual(check_result.status, CheckStatus.FAILING) | ||||
|         self.assertEqual(check_result.alert_severity, "error") | ||||
|  | ||||
|         # test passing | ||||
| @@ -365,7 +360,7 @@ class TestCheckTasks(TacticalTestCase): | ||||
|  | ||||
|         check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) | ||||
|  | ||||
|         self.assertEqual(check_result.status, "passing") | ||||
|         self.assertEqual(check_result.status, CheckStatus.PASSING) | ||||
|  | ||||
|         # test failing info | ||||
|         check.info_return_codes = [20, 30, 50] | ||||
| @@ -385,7 +380,7 @@ class TestCheckTasks(TacticalTestCase): | ||||
|  | ||||
|         check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) | ||||
|  | ||||
|         self.assertEqual(check_result.status, "failing") | ||||
|         self.assertEqual(check_result.status, CheckStatus.FAILING) | ||||
|         self.assertEqual(check_result.alert_severity, "info") | ||||
|  | ||||
|         # test failing warning | ||||
| @@ -406,7 +401,7 @@ class TestCheckTasks(TacticalTestCase): | ||||
|  | ||||
|         check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) | ||||
|  | ||||
|         self.assertEqual(check_result.status, "failing") | ||||
|         self.assertEqual(check_result.status, CheckStatus.FAILING) | ||||
|         self.assertEqual(check_result.alert_severity, "warning") | ||||
|  | ||||
|     def test_handle_diskspace_check(self): | ||||
| @@ -435,7 +430,7 @@ class TestCheckTasks(TacticalTestCase): | ||||
|  | ||||
|         check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) | ||||
|  | ||||
|         self.assertEqual(check_result.status, "failing") | ||||
|         self.assertEqual(check_result.status, CheckStatus.FAILING) | ||||
|         self.assertEqual(check_result.alert_severity, "warning") | ||||
|  | ||||
|         # test error failure | ||||
| @@ -454,7 +449,7 @@ class TestCheckTasks(TacticalTestCase): | ||||
|  | ||||
|         check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) | ||||
|  | ||||
|         self.assertEqual(check_result.status, "failing") | ||||
|         self.assertEqual(check_result.status, CheckStatus.FAILING) | ||||
|         self.assertEqual(check_result.alert_severity, "error") | ||||
|  | ||||
|         # test disk not exist | ||||
| @@ -465,7 +460,7 @@ class TestCheckTasks(TacticalTestCase): | ||||
|  | ||||
|         check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) | ||||
|  | ||||
|         self.assertEqual(check_result.status, "failing") | ||||
|         self.assertEqual(check_result.status, CheckStatus.FAILING) | ||||
|         self.assertEqual(check_result.alert_severity, "error") | ||||
|  | ||||
|         # test warning threshold 0 | ||||
| @@ -486,7 +481,7 @@ class TestCheckTasks(TacticalTestCase): | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|  | ||||
|         check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) | ||||
|         self.assertEqual(check_result.status, "failing") | ||||
|         self.assertEqual(check_result.status, CheckStatus.FAILING) | ||||
|         self.assertEqual(check_result.alert_severity, "error") | ||||
|  | ||||
|         # test error threshold 0 | ||||
| @@ -507,7 +502,7 @@ class TestCheckTasks(TacticalTestCase): | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|  | ||||
|         check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) | ||||
|         self.assertEqual(check_result.status, "failing") | ||||
|         self.assertEqual(check_result.status, CheckStatus.FAILING) | ||||
|         self.assertEqual(check_result.alert_severity, "warning") | ||||
|  | ||||
|         # test passing | ||||
| @@ -526,7 +521,7 @@ class TestCheckTasks(TacticalTestCase): | ||||
|  | ||||
|         check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) | ||||
|  | ||||
|         self.assertEqual(check_result.status, "passing") | ||||
|         self.assertEqual(check_result.status, CheckStatus.PASSING) | ||||
|  | ||||
|     def test_handle_cpuload_check(self): | ||||
|         url = "/api/v3/checkrunner/" | ||||
| @@ -545,7 +540,7 @@ class TestCheckTasks(TacticalTestCase): | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|  | ||||
|         check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) | ||||
|         self.assertEqual(check_result.status, "failing") | ||||
|         self.assertEqual(check_result.status, CheckStatus.FAILING) | ||||
|         self.assertEqual(check_result.alert_severity, "warning") | ||||
|  | ||||
|         # test failing error | ||||
| @@ -559,7 +554,7 @@ class TestCheckTasks(TacticalTestCase): | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|  | ||||
|         check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) | ||||
|         self.assertEqual(check_result.status, "failing") | ||||
|         self.assertEqual(check_result.status, CheckStatus.FAILING) | ||||
|         self.assertEqual(check_result.alert_severity, "error") | ||||
|  | ||||
|         # test passing | ||||
| @@ -573,7 +568,7 @@ class TestCheckTasks(TacticalTestCase): | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|  | ||||
|         check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) | ||||
|         self.assertEqual(check_result.status, "passing") | ||||
|         self.assertEqual(check_result.status, CheckStatus.PASSING) | ||||
|  | ||||
|         # test warning threshold 0 | ||||
|         check.warning_threshold = 0 | ||||
| @@ -588,7 +583,7 @@ class TestCheckTasks(TacticalTestCase): | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|  | ||||
|         check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) | ||||
|         self.assertEqual(check_result.status, "failing") | ||||
|         self.assertEqual(check_result.status, CheckStatus.FAILING) | ||||
|         self.assertEqual(check_result.alert_severity, "error") | ||||
|  | ||||
|         # test error threshold 0 | ||||
| @@ -605,7 +600,7 @@ class TestCheckTasks(TacticalTestCase): | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|  | ||||
|         check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) | ||||
|         self.assertEqual(check_result.status, "failing") | ||||
|         self.assertEqual(check_result.status, CheckStatus.FAILING) | ||||
|         self.assertEqual(check_result.alert_severity, "warning") | ||||
|  | ||||
|     def test_handle_memory_check(self): | ||||
| @@ -625,7 +620,7 @@ class TestCheckTasks(TacticalTestCase): | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|  | ||||
|         check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) | ||||
|         self.assertEqual(check_result.status, "failing") | ||||
|         self.assertEqual(check_result.status, CheckStatus.FAILING) | ||||
|         self.assertEqual(check_result.alert_severity, "warning") | ||||
|  | ||||
|         # test failing error | ||||
| @@ -639,7 +634,7 @@ class TestCheckTasks(TacticalTestCase): | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|  | ||||
|         check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) | ||||
|         self.assertEqual(check_result.status, "failing") | ||||
|         self.assertEqual(check_result.status, CheckStatus.FAILING) | ||||
|         self.assertEqual(check_result.alert_severity, "error") | ||||
|  | ||||
|         # test passing | ||||
| @@ -653,7 +648,7 @@ class TestCheckTasks(TacticalTestCase): | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|  | ||||
|         check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) | ||||
|         self.assertEqual(check_result.status, "passing") | ||||
|         self.assertEqual(check_result.status, CheckStatus.PASSING) | ||||
|  | ||||
|         # test warning threshold 0 | ||||
|         check.warning_threshold = 0 | ||||
| @@ -668,7 +663,7 @@ class TestCheckTasks(TacticalTestCase): | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|  | ||||
|         check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) | ||||
|         self.assertEqual(check_result.status, "failing") | ||||
|         self.assertEqual(check_result.status, CheckStatus.FAILING) | ||||
|         self.assertEqual(check_result.alert_severity, "error") | ||||
|  | ||||
|         # test error threshold 0 | ||||
| @@ -685,7 +680,7 @@ class TestCheckTasks(TacticalTestCase): | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|  | ||||
|         check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) | ||||
|         self.assertEqual(check_result.status, "failing") | ||||
|         self.assertEqual(check_result.status, CheckStatus.FAILING) | ||||
|         self.assertEqual(check_result.alert_severity, "warning") | ||||
|  | ||||
|     def test_handle_ping_check(self): | ||||
| @@ -699,7 +694,7 @@ class TestCheckTasks(TacticalTestCase): | ||||
|         data = { | ||||
|             "id": check.id, | ||||
|             "agent_id": self.agent.agent_id, | ||||
|             "status": "failing", | ||||
|             "status": CheckStatus.FAILING, | ||||
|             "output": "reply from a.com", | ||||
|         } | ||||
|  | ||||
| @@ -707,7 +702,7 @@ class TestCheckTasks(TacticalTestCase): | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|  | ||||
|         check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) | ||||
|         self.assertEqual(check_result.status, "failing") | ||||
|         self.assertEqual(check_result.status, CheckStatus.FAILING) | ||||
|         self.assertEqual(check.alert_severity, "info") | ||||
|  | ||||
|         # test failing warning | ||||
| @@ -718,7 +713,7 @@ class TestCheckTasks(TacticalTestCase): | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|  | ||||
|         check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) | ||||
|         self.assertEqual(check_result.status, "failing") | ||||
|         self.assertEqual(check_result.status, CheckStatus.FAILING) | ||||
|         self.assertEqual(check.alert_severity, "warning") | ||||
|  | ||||
|         # test failing error | ||||
| @@ -729,7 +724,7 @@ class TestCheckTasks(TacticalTestCase): | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|  | ||||
|         check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) | ||||
|         self.assertEqual(check_result.status, "failing") | ||||
|         self.assertEqual(check_result.status, CheckStatus.FAILING) | ||||
|         self.assertEqual(check.alert_severity, "error") | ||||
|  | ||||
|         # test failing error | ||||
| @@ -737,14 +732,14 @@ class TestCheckTasks(TacticalTestCase): | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|  | ||||
|         check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) | ||||
|         self.assertEqual(check_result.status, "failing") | ||||
|         self.assertEqual(check_result.status, CheckStatus.FAILING) | ||||
|         self.assertEqual(check.alert_severity, "error") | ||||
|  | ||||
|         # test passing | ||||
|         data = { | ||||
|             "id": check.id, | ||||
|             "agent_id": self.agent.agent_id, | ||||
|             "status": "passing", | ||||
|             "status": CheckStatus.PASSING, | ||||
|             "output": "reply from a.com", | ||||
|         } | ||||
|  | ||||
| @@ -752,7 +747,7 @@ class TestCheckTasks(TacticalTestCase): | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|  | ||||
|         check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) | ||||
|         self.assertEqual(check_result.status, "passing") | ||||
|         self.assertEqual(check_result.status, CheckStatus.PASSING) | ||||
|  | ||||
|     @patch("agents.models.Agent.nats_cmd") | ||||
|     def test_handle_winsvc_check(self, nats_cmd): | ||||
| @@ -766,7 +761,7 @@ class TestCheckTasks(TacticalTestCase): | ||||
|         data = { | ||||
|             "id": check.id, | ||||
|             "agent_id": self.agent.agent_id, | ||||
|             "status": "passing", | ||||
|             "status": CheckStatus.PASSING, | ||||
|             "more_info": "ok", | ||||
|         } | ||||
|  | ||||
| @@ -774,13 +769,13 @@ class TestCheckTasks(TacticalTestCase): | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|  | ||||
|         check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) | ||||
|         self.assertEqual(check_result.status, "passing") | ||||
|         self.assertEqual(check_result.status, CheckStatus.PASSING) | ||||
|  | ||||
|         # test failing | ||||
|         data = { | ||||
|             "id": check.id, | ||||
|             "agent_id": self.agent.agent_id, | ||||
|             "status": "failing", | ||||
|             "status": CheckStatus.FAILING, | ||||
|             "more_info": "ok", | ||||
|         } | ||||
|  | ||||
| @@ -788,7 +783,7 @@ class TestCheckTasks(TacticalTestCase): | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|  | ||||
|         check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) | ||||
|         self.assertEqual(check_result.status, "failing") | ||||
|         self.assertEqual(check_result.status, CheckStatus.FAILING) | ||||
|         self.assertEqual(check.alert_severity, "info") | ||||
|  | ||||
|     def test_handle_eventlog_check(self): | ||||
| @@ -796,8 +791,8 @@ class TestCheckTasks(TacticalTestCase): | ||||
|  | ||||
|         check = baker.make_recipe( | ||||
|             "checks.eventlog_check", | ||||
|             event_type="warning", | ||||
|             fail_when="contains", | ||||
|             event_type=EvtLogTypes.WARNING, | ||||
|             fail_when=EvtLogFailWhen.CONTAINS, | ||||
|             event_id=123, | ||||
|             alert_severity="warning", | ||||
|             agent=self.agent, | ||||
| @@ -842,8 +837,8 @@ class TestCheckTasks(TacticalTestCase): | ||||
|  | ||||
|         check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) | ||||
|  | ||||
|         self.assertEquals(check.alert_severity, "warning") | ||||
|         self.assertEquals(check_result.status, "failing") | ||||
|         self.assertEqual(check.alert_severity, "warning") | ||||
|         self.assertEqual(check_result.status, CheckStatus.FAILING) | ||||
|  | ||||
|         # test passing when contains | ||||
|         resp = self.client.patch(url, no_logs_data, format="json") | ||||
| @@ -851,10 +846,10 @@ class TestCheckTasks(TacticalTestCase): | ||||
|  | ||||
|         check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) | ||||
|  | ||||
|         self.assertEquals(check_result.status, "passing") | ||||
|         self.assertEqual(check_result.status, CheckStatus.PASSING) | ||||
|  | ||||
|         # test failing when not contains and message and source | ||||
|         check.fail_when = "not_contains" | ||||
|         check.fail_when = EvtLogFailWhen.NOT_CONTAINS | ||||
|         check.alert_severity = "error" | ||||
|         check.save() | ||||
|  | ||||
| @@ -863,8 +858,8 @@ class TestCheckTasks(TacticalTestCase): | ||||
|  | ||||
|         check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) | ||||
|  | ||||
|         self.assertEquals(check_result.status, "failing") | ||||
|         self.assertEquals(check.alert_severity, "error") | ||||
|         self.assertEqual(check_result.status, CheckStatus.FAILING) | ||||
|         self.assertEqual(check.alert_severity, "error") | ||||
|  | ||||
|         # test passing when contains with source and message | ||||
|         resp = self.client.patch(url, data, format="json") | ||||
| @@ -872,7 +867,7 @@ class TestCheckTasks(TacticalTestCase): | ||||
|  | ||||
|         check_result = CheckResult.objects.get(assigned_check=check, agent=self.agent) | ||||
|  | ||||
|         self.assertEquals(check_result.status, "passing") | ||||
|         self.assertEqual(check_result.status, CheckStatus.PASSING) | ||||
|  | ||||
|  | ||||
| class TestCheckPermissions(TacticalTestCase): | ||||
| @@ -945,7 +940,7 @@ class TestCheckPermissions(TacticalTestCase): | ||||
|  | ||||
|         policy_data = { | ||||
|             "policy": policy.id, | ||||
|             "check_type": "diskspace", | ||||
|             "check_type": CheckType.DISK_SPACE, | ||||
|             "disk": "C:", | ||||
|             "error_threshold": 55, | ||||
|             "warning_threshold": 0, | ||||
| @@ -954,7 +949,7 @@ class TestCheckPermissions(TacticalTestCase): | ||||
|  | ||||
|         agent_data = { | ||||
|             "agent": agent.agent_id, | ||||
|             "check_type": "diskspace", | ||||
|             "check_type": CheckType.DISK_SPACE, | ||||
|             "disk": "C:", | ||||
|             "error_threshold": 55, | ||||
|             "warning_threshold": 0, | ||||
| @@ -963,7 +958,7 @@ class TestCheckPermissions(TacticalTestCase): | ||||
|  | ||||
|         unauthorized_agent_data = { | ||||
|             "agent": unauthorized_agent.agent_id, | ||||
|             "check_type": "diskspace", | ||||
|             "check_type": CheckType.DISK_SPACE, | ||||
|             "disk": "C:", | ||||
|             "error_threshold": 55, | ||||
|             "warning_threshold": 0, | ||||
|   | ||||
| @@ -1,9 +1,6 @@ | ||||
| import asyncio | ||||
| from datetime import datetime as dt | ||||
|  | ||||
| from agents.models import Agent | ||||
| from alerts.models import Alert | ||||
| from automation.models import Policy | ||||
| from django.db.models import Q | ||||
| from django.shortcuts import get_object_or_404 | ||||
| from django.utils import timezone as djangotime | ||||
| @@ -13,8 +10,12 @@ from rest_framework.permissions import IsAuthenticated | ||||
| from rest_framework.response import Response | ||||
| from rest_framework.views import APIView | ||||
|  | ||||
| from agents.models import Agent | ||||
| from alerts.models import Alert | ||||
| from automation.models import Policy | ||||
| from tacticalrmm.constants import CheckStatus, CheckType | ||||
| from tacticalrmm.helpers import notify_error | ||||
| from tacticalrmm.permissions import _has_perm_on_agent | ||||
| from tacticalrmm.utils import notify_error | ||||
|  | ||||
| from .models import Check, CheckHistory, CheckResult | ||||
| from .permissions import ChecksPerms, RunChecksPerms | ||||
| @@ -48,7 +49,7 @@ class GetAddChecks(APIView): | ||||
|  | ||||
|         # set event id to 0 if wildcard because it needs to be an integer field for db | ||||
|         # will be ignored anyway by the agent when doing wildcard check | ||||
|         if data["check_type"] == "eventlog" and data["event_id_is_wildcard"]: | ||||
|         if data["check_type"] == CheckType.EVENT_LOG and data["event_id_is_wildcard"]: | ||||
|             data["event_id"] = 0 | ||||
|  | ||||
|         serializer = CheckSerializer(data=data, partial=True) | ||||
| @@ -81,7 +82,7 @@ class GetUpdateDeleteCheck(APIView): | ||||
|  | ||||
|         # set event id to 0 if wildcard because it needs to be an integer field for db | ||||
|         # will be ignored anyway by the agent when doing wildcard check | ||||
|         if check.check_type == "eventlog": | ||||
|         if check.check_type == CheckType.EVENT_LOG: | ||||
|             try: | ||||
|                 data["event_id_is_wildcard"] | ||||
|             except KeyError: | ||||
| @@ -116,7 +117,7 @@ class ResetCheck(APIView): | ||||
|         if result.agent and not _has_perm_on_agent(request.user, result.agent.agent_id): | ||||
|             raise PermissionDenied() | ||||
|  | ||||
|         result.status = "passing" | ||||
|         result.status = CheckStatus.PASSING | ||||
|         result.save() | ||||
|  | ||||
|         # resolve any alerts that are open | ||||
|   | ||||
| @@ -1,8 +1,9 @@ | ||||
| # Generated by Django 3.2.10 on 2021-12-26 05:47 | ||||
|  | ||||
| import clients.models | ||||
| from django.db import migrations, models | ||||
|  | ||||
| import clients.models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|   | ||||
| @@ -1,11 +1,12 @@ | ||||
| import uuid | ||||
| from typing import Dict | ||||
|  | ||||
| from django.contrib.postgres.fields import ArrayField | ||||
| from django.core.cache import cache | ||||
| from django.db import models | ||||
|  | ||||
| from agents.models import Agent | ||||
| from django.core.cache import cache | ||||
| from django.contrib.postgres.fields import ArrayField | ||||
| from django.db import models | ||||
| from logs.models import BaseAuditModel | ||||
| from typing import Dict | ||||
| from tacticalrmm.constants import AGENT_DEFER | ||||
| from tacticalrmm.models import PermissionQuerySet | ||||
|  | ||||
|   | ||||
| @@ -2,19 +2,19 @@ import datetime as dt | ||||
| import re | ||||
| import uuid | ||||
|  | ||||
| from agents.models import Agent | ||||
| from core.utils import get_core_settings | ||||
| from django.db.models import Count, Exists, OuterRef, Prefetch, prefetch_related_objects | ||||
| from django.shortcuts import get_object_or_404 | ||||
| from django.utils import timezone as djangotime | ||||
| from django.db.models import OuterRef, Exists, Count, Prefetch, prefetch_related_objects | ||||
| from knox.models import AuthToken | ||||
| from rest_framework.exceptions import PermissionDenied | ||||
| from rest_framework.permissions import AllowAny, IsAuthenticated | ||||
| from rest_framework.response import Response | ||||
| from rest_framework.views import APIView | ||||
| from knox.models import AuthToken | ||||
|  | ||||
| from agents.models import Agent | ||||
| from core.utils import get_core_settings | ||||
| from tacticalrmm.helpers import notify_error | ||||
| from tacticalrmm.permissions import _has_perm_on_client, _has_perm_on_site | ||||
| from tacticalrmm.utils import notify_error | ||||
|  | ||||
| from .models import Client, ClientCustomField, Deployment, Site, SiteCustomField | ||||
| from .permissions import ClientsPerms, DeploymentPerms, SitesPerms | ||||
| @@ -32,9 +32,8 @@ class GetAddClients(APIView): | ||||
|  | ||||
|     def get(self, request): | ||||
|         clients = ( | ||||
|             Client.objects.select_related( | ||||
|                 "workstation_policy", "server_policy", "alert_template" | ||||
|             ) | ||||
|             Client.objects.order_by("name") | ||||
|             .select_related("workstation_policy", "server_policy", "alert_template") | ||||
|             .filter_by_role(request.user)  # type: ignore | ||||
|             .prefetch_related( | ||||
|                 Prefetch( | ||||
| @@ -43,7 +42,8 @@ class GetAddClients(APIView): | ||||
|                 ), | ||||
|                 Prefetch( | ||||
|                     "sites", | ||||
|                     queryset=Site.objects.select_related("client") | ||||
|                     queryset=Site.objects.order_by("name") | ||||
|                     .select_related("client") | ||||
|                     .filter_by_role(request.user) | ||||
|                     .prefetch_related("custom_fields__field") | ||||
|                     .annotate( | ||||
| @@ -120,7 +120,8 @@ class GetUpdateDeleteClient(APIView): | ||||
|             [client], | ||||
|             Prefetch( | ||||
|                 "sites", | ||||
|                 queryset=Site.objects.select_related("client") | ||||
|                 queryset=Site.objects.order_by("name") | ||||
|                 .select_related("client") | ||||
|                 .filter_by_role(request.user) | ||||
|                 .prefetch_related("custom_fields__field") | ||||
|                 .annotate( | ||||
|   | ||||
| @@ -33,7 +33,7 @@ meshSystemBin="${meshDir}/meshagent" | ||||
| meshSvcName='meshagent.service' | ||||
| meshSysD="/lib/systemd/system/${meshSvcName}" | ||||
|  | ||||
| deb=(ubuntu debian raspbian kali) | ||||
| deb=(ubuntu debian raspbian kali linuxmint) | ||||
| rhe=(fedora rocky centos rhel amzn arch opensuse) | ||||
|  | ||||
| set_locale_deb() { | ||||
|   | ||||
| @@ -1,11 +1,12 @@ | ||||
| import asyncio | ||||
|  | ||||
| from agents.models import Agent | ||||
| from channels.db import database_sync_to_async | ||||
| from channels.generic.websocket import AsyncJsonWebsocketConsumer | ||||
| from django.contrib.auth.models import AnonymousUser | ||||
| from django.utils import timezone as djangotime | ||||
| from django.db.models import F | ||||
| from django.utils import timezone as djangotime | ||||
|  | ||||
| from agents.models import Agent | ||||
|  | ||||
|  | ||||
| class DashInfo(AsyncJsonWebsocketConsumer): | ||||
|   | ||||
| @@ -1,28 +1,24 @@ | ||||
| import asyncio | ||||
| from meshctrl.utils import get_auth_token | ||||
|  | ||||
| from django.core.management.base import BaseCommand | ||||
| from meshctrl.utils import get_auth_token | ||||
|  | ||||
| from core.utils import ( | ||||
|     get_mesh_device_id, | ||||
|     get_mesh_ws_url, | ||||
|     get_core_settings, | ||||
| ) | ||||
| from core.utils import get_core_settings, get_mesh_device_id, get_mesh_ws_url | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     help = "Mesh troubleshooting script" | ||||
|  | ||||
|     def _success(self, *args): | ||||
|     def _success(self, *args) -> None: | ||||
|         self.stdout.write(self.style.SUCCESS(" ".join(args))) | ||||
|  | ||||
|     def _error(self, *args): | ||||
|     def _error(self, *args) -> None: | ||||
|         self.stdout.write(self.style.ERROR(" ".join(args))) | ||||
|  | ||||
|     def _warning(self, *args): | ||||
|     def _warning(self, *args) -> None: | ||||
|         self.stdout.write(self.style.WARNING(" ".join(args))) | ||||
|  | ||||
|     def handle(self, *args, **kwargs): | ||||
|     def handle(self, *args, **kwargs) -> None: | ||||
|         core = get_core_settings() | ||||
|  | ||||
|         self._warning("Mesh site:", core.mesh_site) | ||||
|   | ||||
| @@ -2,9 +2,10 @@ import asyncio | ||||
| import json | ||||
|  | ||||
| import websockets | ||||
| from core.utils import get_mesh_ws_url | ||||
| from django.core.management.base import BaseCommand | ||||
|  | ||||
| from core.utils import get_mesh_ws_url | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     help = "Sets up initial mesh central configuration" | ||||
|   | ||||
| @@ -1,7 +1,8 @@ | ||||
| from core.models import CoreSettings | ||||
| from django.core.exceptions import ValidationError | ||||
| from django.core.management.base import BaseCommand | ||||
|  | ||||
| from core.models import CoreSettings | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     help = "Populates the global site settings on first install" | ||||
|   | ||||
| @@ -2,10 +2,11 @@ import asyncio | ||||
| import json | ||||
|  | ||||
| import websockets | ||||
| from core.utils import get_mesh_ws_url, get_core_settings | ||||
| from django.conf import settings | ||||
| from django.core.management.base import BaseCommand | ||||
|  | ||||
| from core.utils import get_core_settings, get_mesh_ws_url | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     help = "Sets up initial mesh central configuration" | ||||
|   | ||||
| @@ -1,18 +0,0 @@ | ||||
| from django.conf import settings | ||||
| from django.core.management.base import BaseCommand | ||||
| from scripts.models import Script | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     help = "loads scripts for demo" | ||||
|  | ||||
|     def handle(self, *args, **kwargs): | ||||
|         scripts_dir = settings.BASE_DIR.joinpath("tacticalrmm/test_data/demo_scripts") | ||||
|         scripts = Script.objects.filter(script_type="userdefined") | ||||
|         for script in scripts: | ||||
|             filepath = scripts_dir.joinpath(script.filename) | ||||
|             with open(filepath, "rb") as f: | ||||
|                 script.script_body = f.read().decode("utf-8") | ||||
|                 script.save(update_fields=["script_body"]) | ||||
|  | ||||
|         self.stdout.write(self.style.SUCCESS("Added userdefined scripts")) | ||||
| @@ -1,14 +1,13 @@ | ||||
| import base64 | ||||
|  | ||||
| from django.core.management.base import BaseCommand | ||||
|  | ||||
| from accounts.models import User | ||||
| from agents.models import Agent | ||||
| from alerts.models import Alert | ||||
| from autotasks.models import AutomatedTask | ||||
| from checks.models import Check, CheckHistory | ||||
| from django.core.management.base import BaseCommand | ||||
| from scripts.models import Script | ||||
|  | ||||
| from tacticalrmm.constants import AGENT_DEFER | ||||
| from tacticalrmm.constants import AGENT_DEFER, ScriptType | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
| @@ -27,7 +26,7 @@ class Command(BaseCommand): | ||||
|                 user.save() | ||||
|  | ||||
|         # convert script base64 field to text field | ||||
|         user_scripts = Script.objects.exclude(script_type="builtin").filter( | ||||
|         user_scripts = Script.objects.exclude(script_type=ScriptType.BUILT_IN).filter( | ||||
|             script_body="" | ||||
|         ) | ||||
|         for script in user_scripts: | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| from django.core.management.base import BaseCommand | ||||
|  | ||||
| from core.utils import clear_entire_cache | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -2,12 +2,12 @@ from django.core.management.base import BaseCommand | ||||
|  | ||||
| from agents.tasks import agent_outages_task, auto_self_agent_update_task | ||||
| from alerts.tasks import unsnooze_alerts | ||||
| from autotasks.tasks import remove_orphaned_win_tasks | ||||
| from core.tasks import ( | ||||
|     cache_db_fields_task, | ||||
|     core_maintenance_tasks, | ||||
|     handle_resolved_stuff, | ||||
| ) | ||||
| from autotasks.tasks import remove_orphaned_win_tasks | ||||
| from winupdate.tasks import auto_approve_updates_task, check_agent_update_schedule_task | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,18 @@ | ||||
| # Generated by Django 4.0.4 on 2022-04-23 14:26 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('core', '0033_coresettings_mesh_disable_auto_login'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name='customfield', | ||||
|             name='name', | ||||
|             field=models.CharField(max_length=100), | ||||
|         ), | ||||
|     ] | ||||
| @@ -1,19 +1,19 @@ | ||||
| import smtplib | ||||
| from email.message import EmailMessage | ||||
| from typing import TYPE_CHECKING, List, Optional, Union, cast | ||||
|  | ||||
| from typing import Optional, Union, List, cast, TYPE_CHECKING | ||||
| import pytz | ||||
| import requests | ||||
| from django.core.cache import cache | ||||
| from django.conf import settings | ||||
| from django.contrib.postgres.fields import ArrayField | ||||
| from django.core.cache import cache | ||||
| from django.core.exceptions import ValidationError | ||||
| from django.db import models | ||||
| from logs.models import LOG_LEVEL_CHOICES, BaseAuditModel, DebugLog | ||||
| from twilio.base.exceptions import TwilioRestException | ||||
| from twilio.rest import Client as TwClient | ||||
|  | ||||
| from tacticalrmm.constants import CORESETTINGS_CACHE_KEY | ||||
| from logs.models import BaseAuditModel, DebugLog | ||||
| from tacticalrmm.constants import CORESETTINGS_CACHE_KEY, DebugLogLevel | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|     from alerts.models import AlertTemplate | ||||
| @@ -57,7 +57,7 @@ class CoreSettings(BaseAuditModel): | ||||
|     debug_log_prune_days = models.PositiveIntegerField(default=30) | ||||
|     audit_log_prune_days = models.PositiveIntegerField(default=0) | ||||
|     agent_debug_level = models.CharField( | ||||
|         max_length=20, choices=LOG_LEVEL_CHOICES, default="info" | ||||
|         max_length=20, choices=DebugLogLevel.choices, default=DebugLogLevel.INFO | ||||
|     ) | ||||
|     clear_faults_days = models.IntegerField(default=0) | ||||
|     mesh_token = models.CharField(max_length=255, null=True, blank=True, default="") | ||||
| @@ -175,12 +175,12 @@ class CoreSettings(BaseAuditModel): | ||||
|         body: str, | ||||
|         alert_template: "Optional[AlertTemplate]" = None, | ||||
|         test: bool = False, | ||||
|     ) -> Union[bool, str]: | ||||
|     ) -> tuple[str, bool]: | ||||
|         if test and not self.email_is_configured: | ||||
|             return "There needs to be at least one email recipient configured" | ||||
|             return ("There needs to be at least one email recipient configured", False) | ||||
|         # return since email must be configured to continue | ||||
|         elif not self.email_is_configured: | ||||
|             return "SMTP messaging not configured." | ||||
|             return ("SMTP messaging not configured.", False) | ||||
|  | ||||
|         # override email from if alert_template is passed and is set | ||||
|         if alert_template and alert_template.email_from: | ||||
| @@ -194,7 +194,7 @@ class CoreSettings(BaseAuditModel): | ||||
|         elif self.email_alert_recipients: | ||||
|             email_recipients = ", ".join(cast(List[str], self.email_alert_recipients)) | ||||
|         else: | ||||
|             return "There needs to be at least one email recipient configured" | ||||
|             return ("There needs to be at least one email recipient configured", False) | ||||
|  | ||||
|         try: | ||||
|             msg = EmailMessage() | ||||
| @@ -221,18 +221,21 @@ class CoreSettings(BaseAuditModel): | ||||
|         except Exception as e: | ||||
|             DebugLog.error(message=f"Sending email failed with error: {e}") | ||||
|             if test: | ||||
|                 return str(e) | ||||
|         finally: | ||||
|             return True | ||||
|                 return (str(e), False) | ||||
|  | ||||
|         if test: | ||||
|             return ("Email test ok!", True) | ||||
|  | ||||
|         return ("ok", True) | ||||
|  | ||||
|     def send_sms( | ||||
|         self, | ||||
|         body: str, | ||||
|         alert_template: "Optional[AlertTemplate]" = None, | ||||
|         test: bool = False, | ||||
|     ) -> Union[str, bool]: | ||||
|     ) -> tuple[str, bool]: | ||||
|         if not self.sms_is_configured: | ||||
|             return "Sms alerting is not setup correctly." | ||||
|             return ("Sms alerting is not setup correctly.", False) | ||||
|  | ||||
|         # override email recipients if alert_template is passed and is set | ||||
|         if alert_template and alert_template.text_recipients: | ||||
| @@ -240,7 +243,7 @@ class CoreSettings(BaseAuditModel): | ||||
|         elif self.sms_alert_recipients: | ||||
|             text_recipients = cast(List[str], self.sms_alert_recipients) | ||||
|         else: | ||||
|             return "No sms recipients found" | ||||
|             return ("No sms recipients found", False) | ||||
|  | ||||
|         tw_client = TwClient(self.twilio_account_sid, self.twilio_auth_token) | ||||
|         for num in text_recipients: | ||||
| @@ -249,9 +252,12 @@ class CoreSettings(BaseAuditModel): | ||||
|             except TwilioRestException as e: | ||||
|                 DebugLog.error(message=f"SMS failed to send: {e}") | ||||
|                 if test: | ||||
|                     return str(e) | ||||
|                     return (str(e), False) | ||||
|  | ||||
|         return True | ||||
|         if test: | ||||
|             return ("SMS Test sent successfully!", True) | ||||
|  | ||||
|         return ("ok", True) | ||||
|  | ||||
|     @staticmethod | ||||
|     def serialize(core): | ||||
| @@ -284,7 +290,7 @@ class CustomField(BaseAuditModel): | ||||
|         blank=True, | ||||
|         default=list, | ||||
|     ) | ||||
|     name = models.CharField(max_length=30) | ||||
|     name = models.CharField(max_length=100) | ||||
|     required = models.BooleanField(blank=True, default=False) | ||||
|     default_value_string = models.TextField(null=True, blank=True) | ||||
|     default_value_bool = models.BooleanField(default=False) | ||||
|   | ||||
| @@ -1,22 +1,22 @@ | ||||
| from typing import Dict, Any, TYPE_CHECKING | ||||
| from typing import TYPE_CHECKING, Any, Dict | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.db.models import Prefetch | ||||
| from packaging import version as pyver | ||||
|  | ||||
| from agents.models import Agent | ||||
| from agents.tasks import clear_faults_task, prune_agent_history | ||||
| from alerts.models import Alert | ||||
| from alerts.tasks import prune_resolved_alerts | ||||
| from autotasks.models import TaskResult | ||||
| from checks.models import Check, CheckResult | ||||
| from checks.tasks import prune_check_history | ||||
| from clients.models import Client, Site | ||||
| from checks.models import Check, CheckResult | ||||
| from core.utils import get_core_settings | ||||
| from django.conf import settings | ||||
| from django.db.models import Prefetch, Exists, OuterRef | ||||
| from logs.models import PendingAction | ||||
| from logs.tasks import prune_audit_log, prune_debug_log | ||||
| from packaging import version as pyver | ||||
|  | ||||
| from tacticalrmm.celery import app | ||||
| from tacticalrmm.constants import AGENT_DEFER | ||||
| from tacticalrmm.constants import AGENT_DEFER, PAAction, PAStatus | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|     from django.db.models import QuerySet | ||||
| @@ -53,6 +53,23 @@ def core_maintenance_tasks() -> None: | ||||
|  | ||||
| @app.task | ||||
| def handle_resolved_stuff() -> None: | ||||
|  | ||||
|     # change agent update pending status to completed if agent has just updated | ||||
|     actions = ( | ||||
|         PendingAction.objects.select_related("agent") | ||||
|         .defer("agent__services", "agent__wmi_detail") | ||||
|         .filter(action_type=PAAction.AGENT_UPDATE, status=PAStatus.PENDING) | ||||
|     ) | ||||
|  | ||||
|     to_update = [ | ||||
|         action.id | ||||
|         for action in actions | ||||
|         if pyver.parse(action.agent.version) == pyver.parse(settings.LATEST_AGENT_VER) | ||||
|         and action.agent.status == "online" | ||||
|     ] | ||||
|  | ||||
|     PendingAction.objects.filter(pk__in=to_update).update(status=PAStatus.COMPLETED) | ||||
|  | ||||
|     agent_queryset = ( | ||||
|         Agent.objects.defer(*AGENT_DEFER) | ||||
|         .select_related( | ||||
| @@ -80,26 +97,11 @@ def handle_resolved_stuff() -> None: | ||||
|         ) | ||||
|     ) | ||||
|  | ||||
|     for agent in agent_queryset.annotate( | ||||
|         has_pending_actions=Exists( | ||||
|             PendingAction.objects.filter( | ||||
|                 pk=OuterRef("pk"), action_type="agent_update", status="pending" | ||||
|             ) | ||||
|         ) | ||||
|     ): | ||||
|     for agent in agent_queryset: | ||||
|         if ( | ||||
|             pyver.parse(agent.version) >= pyver.parse("1.6.0") | ||||
|             and agent.status == "online" | ||||
|         ): | ||||
|             # change agent update pending status to completed if agent has just updated | ||||
|             if ( | ||||
|                 pyver.parse(agent.version) == pyver.parse(settings.LATEST_AGENT_VER) | ||||
|                 and agent.has_pending_actions | ||||
|             ): | ||||
|                 agent.pendingactions.filter( | ||||
|                     action_type="agent_update", status="pending" | ||||
|                 ).update(status="completed") | ||||
|  | ||||
|             # sync scheduled tasks | ||||
|             for task in agent.get_tasks_with_policies(): | ||||
|                 if not task.task_result or task.task_result.sync_status == "initial": | ||||
|   | ||||
| @@ -3,15 +3,20 @@ from unittest.mock import patch | ||||
| import requests | ||||
| from channels.db import database_sync_to_async | ||||
| from channels.testing import WebsocketCommunicator | ||||
| from django.conf import settings | ||||
| from model_bakery import baker | ||||
| from rest_framework.authtoken.models import Token | ||||
|  | ||||
| from tacticalrmm.test import TacticalTestCase | ||||
| from agents.models import Agent | ||||
| from core.utils import get_core_settings | ||||
| from logs.models import PendingAction | ||||
| from tacticalrmm.constants import PAAction, PAStatus | ||||
| from tacticalrmm.test import TacticalTestCase | ||||
|  | ||||
| from .consumers import DashInfo | ||||
| from .models import CustomField, GlobalKVStore, URLAction | ||||
| from .serializers import CustomFieldSerializer, KeyStoreSerializer, URLActionSerializer | ||||
| from .tasks import core_maintenance_tasks | ||||
| from .tasks import core_maintenance_tasks, handle_resolved_stuff | ||||
|  | ||||
|  | ||||
| class TestCodeSign(TacticalTestCase): | ||||
| @@ -393,6 +398,29 @@ class TestCoreTasks(TacticalTestCase): | ||||
|  | ||||
|         self.check_not_authenticated("get", url) | ||||
|  | ||||
|     def test_resolved_pending_agentupdate_task(self): | ||||
|         online = baker.make_recipe("agents.online_agent", version="2.0.0", _quantity=20) | ||||
|         offline = baker.make_recipe( | ||||
|             "agents.offline_agent", version="2.0.0", _quantity=20 | ||||
|         ) | ||||
|         agents = online + offline | ||||
|         for agent in agents: | ||||
|             baker.make_recipe("logs.pending_agentupdate_action", agent=agent) | ||||
|  | ||||
|         Agent.objects.update(version=settings.LATEST_AGENT_VER) | ||||
|  | ||||
|         handle_resolved_stuff() | ||||
|  | ||||
|         complete = PendingAction.objects.filter( | ||||
|             action_type=PAAction.AGENT_UPDATE, status=PAStatus.COMPLETED | ||||
|         ).count() | ||||
|         old = PendingAction.objects.filter( | ||||
|             action_type=PAAction.AGENT_UPDATE, status=PAStatus.PENDING | ||||
|         ).count() | ||||
|  | ||||
|         self.assertEqual(complete, 20) | ||||
|         self.assertEqual(old, 20) | ||||
|  | ||||
|  | ||||
| class TestCorePermissions(TacticalTestCase): | ||||
|     def setUp(self): | ||||
|   | ||||
| @@ -1,14 +1,14 @@ | ||||
| import json | ||||
| import tempfile | ||||
| from base64 import b64encode | ||||
| from meshctrl.utils import get_auth_token | ||||
| from typing import TYPE_CHECKING, cast | ||||
|  | ||||
| from typing import cast, TYPE_CHECKING | ||||
| import requests | ||||
| import websockets | ||||
| from django.core.cache import cache | ||||
| from django.conf import settings | ||||
| from django.core.cache import cache | ||||
| from django.http import FileResponse | ||||
| from meshctrl.utils import get_auth_token | ||||
|  | ||||
| from tacticalrmm.constants import CORESETTINGS_CACHE_KEY, ROLE_CACHE_PREFIX | ||||
|  | ||||
| @@ -21,14 +21,14 @@ class CoreSettingsNotFound(Exception): | ||||
|  | ||||
|  | ||||
| def clear_entire_cache() -> None: | ||||
|     cache.delete(f"{ROLE_CACHE_PREFIX}*") | ||||
|     cache.delete_many_pattern(f"{ROLE_CACHE_PREFIX}*") | ||||
|     cache.delete(CORESETTINGS_CACHE_KEY) | ||||
|     cache.delete_many_pattern("site_*") | ||||
|     cache.delete_many_pattern("agent_*") | ||||
|  | ||||
|  | ||||
| def get_core_settings() -> "CoreSettings": | ||||
|     from core.models import CoreSettings, CORESETTINGS_CACHE_KEY | ||||
|     from core.models import CORESETTINGS_CACHE_KEY, CoreSettings | ||||
|  | ||||
|     coresettings = cache.get(CORESETTINGS_CACHE_KEY) | ||||
|  | ||||
|   | ||||
| @@ -2,20 +2,21 @@ import re | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.shortcuts import get_object_or_404 | ||||
| from core.utils import get_core_settings | ||||
| from logs.models import AuditLog | ||||
| from rest_framework.decorators import api_view, permission_classes | ||||
| from rest_framework.exceptions import PermissionDenied | ||||
| from rest_framework.permissions import IsAuthenticated | ||||
| from rest_framework.response import Response | ||||
| from rest_framework.views import APIView | ||||
|  | ||||
| from core.utils import get_core_settings | ||||
| from logs.models import AuditLog | ||||
| from tacticalrmm.constants import AuditActionType, PAStatus | ||||
| from tacticalrmm.helpers import notify_error | ||||
| from tacticalrmm.permissions import ( | ||||
|     _has_perm_on_agent, | ||||
|     _has_perm_on_client, | ||||
|     _has_perm_on_site, | ||||
| ) | ||||
| from tacticalrmm.utils import notify_error | ||||
|  | ||||
| from .models import CodeSignToken, CoreSettings, CustomField, GlobalKVStore, URLAction | ||||
| from .permissions import ( | ||||
| @@ -93,14 +94,14 @@ def dashboard_info(request): | ||||
| @permission_classes([IsAuthenticated, CoreSettingsPerms]) | ||||
| def email_test(request): | ||||
|     core = get_core_settings() | ||||
|     r = core.send_mail( | ||||
|  | ||||
|     msg, ok = core.send_mail( | ||||
|         subject="Test from Tactical RMM", body="This is a test message", test=True | ||||
|     ) | ||||
|     if not ok: | ||||
|         return notify_error(msg) | ||||
|  | ||||
|     if not isinstance(r, bool) and isinstance(r, str): | ||||
|         return notify_error(r) | ||||
|  | ||||
|     return Response("Email Test OK!") | ||||
|     return Response(msg) | ||||
|  | ||||
|  | ||||
| @api_view(["POST"]) | ||||
| @@ -132,12 +133,12 @@ def server_maintenance(request): | ||||
|         tables = request.data["prune_tables"] | ||||
|         records_count = 0 | ||||
|         if "audit_logs" in tables: | ||||
|             auditlogs = AuditLog.objects.filter(action="check_run") | ||||
|             auditlogs = AuditLog.objects.filter(action=AuditActionType.CHECK_RUN) | ||||
|             records_count += auditlogs.count() | ||||
|             auditlogs.delete() | ||||
|  | ||||
|         if "pending_actions" in tables: | ||||
|             pendingactions = PendingAction.objects.filter(status="completed") | ||||
|             pendingactions = PendingAction.objects.filter(status=PAStatus.COMPLETED) | ||||
|             records_count += pendingactions.count() | ||||
|             pendingactions.delete() | ||||
|  | ||||
| @@ -332,10 +333,10 @@ class RunURLAction(APIView): | ||||
|     permission_classes = [IsAuthenticated, URLActionPerms] | ||||
|  | ||||
|     def patch(self, request): | ||||
|         from agents.models import Agent | ||||
|         from clients.models import Client, Site | ||||
|         from requests.utils import requote_uri | ||||
|  | ||||
|         from agents.models import Agent | ||||
|         from clients.models import Client, Site | ||||
|         from tacticalrmm.utils import replace_db_values | ||||
|  | ||||
|         if "agent_id" in request.data.keys(): | ||||
| @@ -388,9 +389,8 @@ class TwilioSMSTest(APIView): | ||||
|                 "All fields are required, including at least 1 recipient" | ||||
|             ) | ||||
|  | ||||
|         r = core.send_sms("TacticalRMM Test SMS", test=True) | ||||
|         msg, ok = core.send_sms("TacticalRMM Test SMS", test=True) | ||||
|         if not ok: | ||||
|             return notify_error(msg) | ||||
|  | ||||
|         if not isinstance(r, bool) and isinstance(r, str): | ||||
|             return notify_error(r) | ||||
|  | ||||
|         return Response("SMS Test sent successfully!") | ||||
|         return Response(msg) | ||||
|   | ||||
| @@ -1 +0,0 @@ | ||||
| default_app_config = "logs.apps.LogsConfig" | ||||
|   | ||||
| @@ -2,27 +2,48 @@ from itertools import cycle | ||||
|  | ||||
| from model_bakery.recipe import Recipe | ||||
|  | ||||
| from tacticalrmm.constants import AuditActionType, AuditObjType, PAAction, PAStatus | ||||
|  | ||||
| object_types = [ | ||||
|     "user", | ||||
|     "script", | ||||
|     "agent", | ||||
|     "policy", | ||||
|     "winupdatepolicy", | ||||
|     "client", | ||||
|     "site", | ||||
|     "check", | ||||
|     "automatedtask", | ||||
|     "coresettings", | ||||
|     AuditObjType.USER, | ||||
|     AuditObjType.SCRIPT, | ||||
|     AuditObjType.AGENT, | ||||
|     AuditObjType.POLICY, | ||||
|     AuditObjType.WINUPDATE, | ||||
|     AuditObjType.CLIENT, | ||||
|     AuditObjType.SITE, | ||||
|     AuditObjType.CHECK, | ||||
|     AuditObjType.AUTOTASK, | ||||
|     AuditObjType.CORE, | ||||
| ] | ||||
|  | ||||
| object_actions = ["add", "modify", "view", "delete"] | ||||
| agent_actions = ["remote_session", "execute_script", "execute_command"] | ||||
| login_actions = ["failed_login", "login"] | ||||
| object_actions = [ | ||||
|     AuditActionType.ADD, | ||||
|     AuditActionType.MODIFY, | ||||
|     AuditActionType.VIEW, | ||||
|     AuditActionType.DELETE, | ||||
| ] | ||||
| agent_actions = [ | ||||
|     AuditActionType.REMOTE_SESSION, | ||||
|     AuditActionType.EXEC_SCRIPT, | ||||
|     AuditActionType.EXEC_COMMAND, | ||||
| ] | ||||
| login_actions = [AuditActionType.FAILED_LOGIN, AuditActionType.LOGIN] | ||||
|  | ||||
| agent_logs = Recipe("logs.AuditLog", action=cycle(agent_actions), object_type="agent") | ||||
| agent_logs = Recipe( | ||||
|     "logs.AuditLog", action=cycle(agent_actions), object_type=AuditObjType.AGENT | ||||
| ) | ||||
|  | ||||
| object_logs = Recipe( | ||||
|     "logs.AuditLog", action=cycle(object_actions), object_type=cycle(object_types) | ||||
| ) | ||||
|  | ||||
| login_logs = Recipe("logs.AuditLog", action=cycle(login_actions), object_type="user") | ||||
| login_logs = Recipe( | ||||
|     "logs.AuditLog", action=cycle(login_actions), object_type=AuditObjType.USER | ||||
| ) | ||||
|  | ||||
| pending_agentupdate_action = Recipe( | ||||
|     "logs.PendingAction", | ||||
|     action_type=PAAction.AGENT_UPDATE, | ||||
|     status=PAStatus.PENDING, | ||||
| ) | ||||
|   | ||||
| @@ -0,0 +1,21 @@ | ||||
| # Generated by Django 4.0.4 on 2022-04-25 06:59 | ||||
|  | ||||
| from django.db import migrations | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('logs', '0023_alter_pendingaction_action_type'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.RemoveField( | ||||
|             model_name='pendingaction', | ||||
|             name='cancelable', | ||||
|         ), | ||||
|         migrations.RemoveField( | ||||
|             model_name='pendingaction', | ||||
|             name='celery_id', | ||||
|         ), | ||||
|     ] | ||||
| @@ -1,80 +1,37 @@ | ||||
| from abc import abstractmethod | ||||
| from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, Union, cast | ||||
|  | ||||
| from django.db import models | ||||
|  | ||||
| from core.utils import get_core_settings | ||||
| from typing import Optional, Dict, Any, Union, cast, Tuple, TYPE_CHECKING | ||||
| from tacticalrmm.constants import ( | ||||
|     AuditActionType, | ||||
|     AuditObjType, | ||||
|     DebugLogLevel, | ||||
|     DebugLogType, | ||||
|     PAAction, | ||||
|     PAStatus, | ||||
| ) | ||||
| from tacticalrmm.middleware import get_debug_info, get_username | ||||
| from tacticalrmm.models import PermissionQuerySet | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|     from agents.models import Agent | ||||
|     from clients.models import Client, Site | ||||
|     from core.models import URLAction | ||||
|     from agents.models import Agent | ||||
|  | ||||
|  | ||||
| def get_debug_level() -> str: | ||||
|     return get_core_settings().agent_debug_level | ||||
|  | ||||
|  | ||||
| ACTION_TYPE_CHOICES = [ | ||||
|     ("schedreboot", "Scheduled Reboot"), | ||||
|     ("agentupdate", "Agent Update"), | ||||
|     ("chocoinstall", "Chocolatey Software Install"), | ||||
|     ("runcmd", "Run Command"), | ||||
|     ("runscript", "Run Script"), | ||||
|     ("runpatchscan", "Run Patch Scan"), | ||||
|     ("runpatchinstall", "Run Patch Install"), | ||||
| ] | ||||
|  | ||||
| AUDIT_ACTION_TYPE_CHOICES = [ | ||||
|     ("login", "User Login"), | ||||
|     ("failed_login", "Failed User Login"), | ||||
|     ("delete", "Delete Object"), | ||||
|     ("modify", "Modify Object"), | ||||
|     ("add", "Add Object"), | ||||
|     ("view", "View Object"), | ||||
|     ("check_run", "Check Run"), | ||||
|     ("task_run", "Task Run"), | ||||
|     ("agent_install", "Agent Install"), | ||||
|     ("remote_session", "Remote Session"), | ||||
|     ("execute_script", "Execute Script"), | ||||
|     ("execute_command", "Execute Command"), | ||||
|     ("bulk_action", "Bulk Action"), | ||||
|     ("url_action", "URL Action"), | ||||
| ] | ||||
|  | ||||
| AUDIT_OBJECT_TYPE_CHOICES = [ | ||||
|     ("user", "User"), | ||||
|     ("script", "Script"), | ||||
|     ("agent", "Agent"), | ||||
|     ("policy", "Policy"), | ||||
|     ("winupdatepolicy", "Patch Policy"), | ||||
|     ("client", "Client"), | ||||
|     ("site", "Site"), | ||||
|     ("check", "Check"), | ||||
|     ("automatedtask", "Automated Task"), | ||||
|     ("coresettings", "Core Settings"), | ||||
|     ("bulk", "Bulk"), | ||||
|     ("alerttemplate", "Alert Template"), | ||||
|     ("role", "Role"), | ||||
|     ("urlaction", "URL Action"), | ||||
|     ("keystore", "Global Key Store"), | ||||
|     ("customfield", "Custom Field"), | ||||
| ] | ||||
|  | ||||
| STATUS_CHOICES = [ | ||||
|     ("pending", "Pending"), | ||||
|     ("completed", "Completed"), | ||||
| ] | ||||
|  | ||||
|  | ||||
| class AuditLog(models.Model): | ||||
|     username = models.CharField(max_length=255) | ||||
|     agent = models.CharField(max_length=255, null=True, blank=True) | ||||
|     agent_id = models.CharField(max_length=255, blank=True, null=True) | ||||
|     entry_time = models.DateTimeField(auto_now_add=True) | ||||
|     action = models.CharField(max_length=100, choices=AUDIT_ACTION_TYPE_CHOICES) | ||||
|     object_type = models.CharField(max_length=100, choices=AUDIT_OBJECT_TYPE_CHOICES) | ||||
|     action = models.CharField(max_length=100, choices=AuditActionType.choices) | ||||
|     object_type = models.CharField(max_length=100, choices=AuditObjType.choices) | ||||
|     before_value = models.JSONField(null=True, blank=True) | ||||
|     after_value = models.JSONField(null=True, blank=True) | ||||
|     message = models.CharField(max_length=255, null=True, blank=True) | ||||
| @@ -101,8 +58,8 @@ class AuditLog(models.Model): | ||||
|             username=username, | ||||
|             agent=agent.hostname, | ||||
|             agent_id=agent.agent_id, | ||||
|             object_type="agent", | ||||
|             action="remote_session", | ||||
|             object_type=AuditObjType.AGENT, | ||||
|             action=AuditActionType.REMOTE_SESSION, | ||||
|             message=f"{username} used Mesh Central to initiate a remote session to {agent.hostname}.", | ||||
|             debug_info=debug_info, | ||||
|         ) | ||||
| @@ -119,8 +76,8 @@ class AuditLog(models.Model): | ||||
|             username=username, | ||||
|             agent=agent.hostname, | ||||
|             agent_id=agent.agent_id, | ||||
|             object_type="agent", | ||||
|             action="execute_command", | ||||
|             object_type=AuditObjType.AGENT, | ||||
|             action=AuditActionType.EXEC_COMMAND, | ||||
|             message=f"{username} issued {shell} command on {agent.hostname}.", | ||||
|             after_value=cmd, | ||||
|             debug_info=debug_info, | ||||
| @@ -138,9 +95,9 @@ class AuditLog(models.Model): | ||||
|         AuditLog.objects.create( | ||||
|             username=username, | ||||
|             object_type=object_type, | ||||
|             agent=before["hostname"] if object_type == "agent" else None, | ||||
|             agent_id=before["agent_id"] if object_type == "agent" else None, | ||||
|             action="modify", | ||||
|             agent=before["hostname"] if object_type == AuditObjType.AGENT else None, | ||||
|             agent_id=before["agent_id"] if object_type == AuditObjType.AGENT else None, | ||||
|             action=AuditActionType.MODIFY, | ||||
|             message=f"{username} modified {object_type} {name}", | ||||
|             before_value=before, | ||||
|             after_value=after, | ||||
| @@ -158,9 +115,9 @@ class AuditLog(models.Model): | ||||
|         AuditLog.objects.create( | ||||
|             username=username, | ||||
|             object_type=object_type, | ||||
|             agent=after["hostname"] if object_type == "agent" else None, | ||||
|             agent_id=after["agent_id"] if object_type == "agent" else None, | ||||
|             action="add", | ||||
|             agent=after["hostname"] if object_type == AuditObjType.AGENT else None, | ||||
|             agent_id=after["agent_id"] if object_type == AuditObjType.AGENT else None, | ||||
|             action=AuditActionType.ADD, | ||||
|             message=f"{username} added {object_type} {name}", | ||||
|             after_value=after, | ||||
|             debug_info=debug_info, | ||||
| @@ -177,9 +134,9 @@ class AuditLog(models.Model): | ||||
|         AuditLog.objects.create( | ||||
|             username=username, | ||||
|             object_type=object_type, | ||||
|             agent=before["hostname"] if object_type == "agent" else None, | ||||
|             agent_id=before["agent_id"] if object_type == "agent" else None, | ||||
|             action="delete", | ||||
|             agent=before["hostname"] if object_type == AuditObjType.AGENT else None, | ||||
|             agent_id=before["agent_id"] if object_type == AuditObjType.AGENT else None, | ||||
|             action=AuditActionType.DELETE, | ||||
|             message=f"{username} deleted {object_type} {name}", | ||||
|             before_value=before, | ||||
|             debug_info=debug_info, | ||||
| @@ -193,8 +150,8 @@ class AuditLog(models.Model): | ||||
|             agent=agent.hostname, | ||||
|             agent_id=agent.agent_id, | ||||
|             username=username, | ||||
|             object_type="agent", | ||||
|             action="execute_script", | ||||
|             object_type=AuditObjType.AGENT, | ||||
|             action=AuditActionType.EXEC_SCRIPT, | ||||
|             message=f'{username} ran script: "{script}" on {agent.hostname}', | ||||
|             debug_info=debug_info, | ||||
|         ) | ||||
| @@ -203,8 +160,8 @@ class AuditLog(models.Model): | ||||
|     def audit_user_failed_login(username: str, debug_info: Dict[Any, Any] = {}) -> None: | ||||
|         AuditLog.objects.create( | ||||
|             username=username, | ||||
|             object_type="user", | ||||
|             action="failed_login", | ||||
|             object_type=AuditObjType.USER, | ||||
|             action=AuditActionType.FAILED_LOGIN, | ||||
|             message=f"{username} failed to login: Credentials were rejected", | ||||
|             debug_info=debug_info, | ||||
|         ) | ||||
| @@ -215,8 +172,8 @@ class AuditLog(models.Model): | ||||
|     ) -> None: | ||||
|         AuditLog.objects.create( | ||||
|             username=username, | ||||
|             object_type="user", | ||||
|             action="failed_login", | ||||
|             object_type=AuditObjType.USER, | ||||
|             action=AuditActionType.FAILED_LOGIN, | ||||
|             message=f"{username} failed to login: Two Factor token rejected", | ||||
|             debug_info=debug_info, | ||||
|         ) | ||||
| @@ -227,8 +184,8 @@ class AuditLog(models.Model): | ||||
|     ) -> None: | ||||
|         AuditLog.objects.create( | ||||
|             username=username, | ||||
|             object_type="user", | ||||
|             action="login", | ||||
|             object_type=AuditObjType.USER, | ||||
|             action=AuditActionType.LOGIN, | ||||
|             message=f"{username} logged in successfully", | ||||
|             debug_info=debug_info, | ||||
|         ) | ||||
| @@ -249,7 +206,7 @@ class AuditLog(models.Model): | ||||
|             agent=name if isinstance(instance, Agent) else None, | ||||
|             agent_id=instance.agent_id if isinstance(instance, Agent) else None, | ||||
|             object_type=classname.lower(), | ||||
|             action="url_action", | ||||
|             action=AuditActionType.URL_ACTION, | ||||
|             message=f"{username} ran url action: {urlaction.pattern} on {classname}: {name}", | ||||
|             debug_info=debug_info, | ||||
|         ) | ||||
| @@ -291,30 +248,14 @@ class AuditLog(models.Model): | ||||
|  | ||||
|         AuditLog.objects.create( | ||||
|             username=username, | ||||
|             object_type="bulk", | ||||
|             action="bulk_action", | ||||
|             object_type=AuditObjType.BULK, | ||||
|             action=AuditActionType.BULK_ACTION, | ||||
|             message=f"{username} executed bulk {action} {target}", | ||||
|             debug_info=debug_info, | ||||
|             after_value=affected, | ||||
|         ) | ||||
|  | ||||
|  | ||||
| LOG_LEVEL_CHOICES = [ | ||||
|     ("info", "Info"), | ||||
|     ("warning", "Warning"), | ||||
|     ("error", "Error"), | ||||
|     ("critical", "Critical"), | ||||
| ] | ||||
|  | ||||
| LOG_TYPE_CHOICES = [ | ||||
|     ("agent_update", "Agent Update"), | ||||
|     ("agent_issues", "Agent Issues"), | ||||
|     ("win_updates", "Windows Updates"), | ||||
|     ("system_issues", "System Issues"), | ||||
|     ("scripting", "Scripting"), | ||||
| ] | ||||
|  | ||||
|  | ||||
| class DebugLog(models.Model): | ||||
|     objects = PermissionQuerySet.as_manager() | ||||
|  | ||||
| @@ -327,10 +268,10 @@ class DebugLog(models.Model): | ||||
|         blank=True, | ||||
|     ) | ||||
|     log_level = models.CharField( | ||||
|         max_length=50, choices=LOG_LEVEL_CHOICES, default="info" | ||||
|         max_length=50, choices=DebugLogLevel.choices, default=DebugLogLevel.INFO | ||||
|     ) | ||||
|     log_type = models.CharField( | ||||
|         max_length=50, choices=LOG_TYPE_CHOICES, default="system_issues" | ||||
|         max_length=50, choices=DebugLogType.choices, default=DebugLogType.SYSTEM_ISSUES | ||||
|     ) | ||||
|     message = models.TextField(null=True, blank=True) | ||||
|  | ||||
| @@ -339,11 +280,14 @@ class DebugLog(models.Model): | ||||
|         cls, | ||||
|         message: str, | ||||
|         agent: "Optional[Agent]" = None, | ||||
|         log_type: str = "system_issues", | ||||
|         log_type: str = DebugLogType.SYSTEM_ISSUES, | ||||
|     ) -> None: | ||||
|         if get_debug_level() in ["info"]: | ||||
|         if get_debug_level() in [DebugLogLevel.INFO]: | ||||
|             cls.objects.create( | ||||
|                 log_level="info", agent=agent, log_type=log_type, message=message | ||||
|                 log_level=DebugLogLevel.INFO, | ||||
|                 agent=agent, | ||||
|                 log_type=log_type, | ||||
|                 message=message, | ||||
|             ) | ||||
|  | ||||
|     @classmethod | ||||
| @@ -351,11 +295,14 @@ class DebugLog(models.Model): | ||||
|         cls, | ||||
|         message: str, | ||||
|         agent: "Optional[Agent]" = None, | ||||
|         log_type: str = "system_issues", | ||||
|         log_type: str = DebugLogType.SYSTEM_ISSUES, | ||||
|     ) -> None: | ||||
|         if get_debug_level() in ["info", "warning"]: | ||||
|         if get_debug_level() in [DebugLogLevel.INFO, DebugLogLevel.WARN]: | ||||
|             cls.objects.create( | ||||
|                 log_level="warning", agent=agent, log_type=log_type, message=message | ||||
|                 log_level=DebugLogLevel.INFO, | ||||
|                 agent=agent, | ||||
|                 log_type=log_type, | ||||
|                 message=message, | ||||
|             ) | ||||
|  | ||||
|     @classmethod | ||||
| @@ -363,11 +310,18 @@ class DebugLog(models.Model): | ||||
|         cls, | ||||
|         message: str, | ||||
|         agent: "Optional[Agent]" = None, | ||||
|         log_type: str = "system_issues", | ||||
|         log_type: str = DebugLogType.SYSTEM_ISSUES, | ||||
|     ) -> None: | ||||
|         if get_debug_level() in ["info", "warning", "error"]: | ||||
|         if get_debug_level() in [ | ||||
|             DebugLogLevel.INFO, | ||||
|             DebugLogLevel.WARN, | ||||
|             DebugLogLevel.ERROR, | ||||
|         ]: | ||||
|             cls.objects.create( | ||||
|                 log_level="error", agent=agent, log_type=log_type, message=message | ||||
|                 log_level=DebugLogLevel.ERROR, | ||||
|                 agent=agent, | ||||
|                 log_type=log_type, | ||||
|                 message=message, | ||||
|             ) | ||||
|  | ||||
|     @classmethod | ||||
| @@ -375,15 +329,24 @@ class DebugLog(models.Model): | ||||
|         cls, | ||||
|         message: str, | ||||
|         agent: "Optional[Agent]" = None, | ||||
|         log_type: str = "system_issues", | ||||
|         log_type: str = DebugLogType.SYSTEM_ISSUES, | ||||
|     ) -> None: | ||||
|         if get_debug_level() in ["info", "warning", "error", "critical"]: | ||||
|         if get_debug_level() in [ | ||||
|             DebugLogLevel.INFO, | ||||
|             DebugLogLevel.WARN, | ||||
|             DebugLogLevel.ERROR, | ||||
|             DebugLogLevel.CRITICAL, | ||||
|         ]: | ||||
|             cls.objects.create( | ||||
|                 log_level="critical", agent=agent, log_type=log_type, message=message | ||||
|                 log_level=DebugLogLevel.CRITICAL, | ||||
|                 agent=agent, | ||||
|                 log_type=log_type, | ||||
|                 message=message, | ||||
|             ) | ||||
|  | ||||
|  | ||||
| class PendingAction(models.Model): | ||||
|  | ||||
|     objects = PermissionQuerySet.as_manager() | ||||
|  | ||||
|     agent = models.ForeignKey( | ||||
| @@ -393,15 +356,13 @@ class PendingAction(models.Model): | ||||
|     ) | ||||
|     entry_time = models.DateTimeField(auto_now_add=True) | ||||
|     action_type = models.CharField( | ||||
|         max_length=255, choices=ACTION_TYPE_CHOICES, null=True, blank=True | ||||
|         max_length=255, choices=PAAction.choices, null=True, blank=True | ||||
|     ) | ||||
|     status = models.CharField( | ||||
|         max_length=255, | ||||
|         choices=STATUS_CHOICES, | ||||
|         default="pending", | ||||
|         choices=PAStatus.choices, | ||||
|         default=PAStatus.PENDING, | ||||
|     ) | ||||
|     cancelable = models.BooleanField(blank=True, default=False) | ||||
|     celery_id = models.CharField(null=True, blank=True, max_length=255) | ||||
|     details = models.JSONField(null=True, blank=True) | ||||
|  | ||||
|     def __str__(self) -> str: | ||||
| @@ -409,31 +370,31 @@ class PendingAction(models.Model): | ||||
|  | ||||
|     @property | ||||
|     def due(self) -> str: | ||||
|         if self.action_type == "schedreboot": | ||||
|         if self.action_type == PAAction.SCHED_REBOOT: | ||||
|             return cast(str, self.details["time"]) | ||||
|         elif self.action_type == "agentupdate": | ||||
|         elif self.action_type == PAAction.AGENT_UPDATE: | ||||
|             return "Next update cycle" | ||||
|         elif self.action_type == "chocoinstall": | ||||
|         elif self.action_type == PAAction.CHOCO_INSTALL: | ||||
|             return "ASAP" | ||||
|         else: | ||||
|             return "On next checkin" | ||||
|  | ||||
|     @property | ||||
|     def description(self) -> Optional[str]: | ||||
|         if self.action_type == "schedreboot": | ||||
|         if self.action_type == PAAction.SCHED_REBOOT: | ||||
|             return "Device pending reboot" | ||||
|  | ||||
|         elif self.action_type == "agentupdate": | ||||
|         elif self.action_type == PAAction.AGENT_UPDATE: | ||||
|             return f"Agent update to {self.details['version']}" | ||||
|  | ||||
|         elif self.action_type == "chocoinstall": | ||||
|         elif self.action_type == PAAction.CHOCO_INSTALL: | ||||
|             return f"{self.details['name']} software install" | ||||
|  | ||||
|         elif self.action_type in [ | ||||
|             "runcmd", | ||||
|             "runscript", | ||||
|             "runpatchscan", | ||||
|             "runpatchinstall", | ||||
|             PAAction.RUN_CMD, | ||||
|             PAAction.RUN_SCRIPT, | ||||
|             PAAction.RUN_PATCH_SCAN, | ||||
|             PAAction.RUN_PATCH_INSTALL, | ||||
|         ]: | ||||
|             return f"{self.action_type}" | ||||
|         else: | ||||
|   | ||||
| @@ -5,6 +5,8 @@ from django.db.models.signals import post_init | ||||
| from django.dispatch import receiver | ||||
| from django.utils import timezone as djangotime | ||||
|  | ||||
| from tacticalrmm.constants import PAAction, PAStatus | ||||
|  | ||||
| from .models import PendingAction | ||||
|  | ||||
|  | ||||
| @@ -12,7 +14,10 @@ from .models import PendingAction | ||||
| def handle_status(sender, instance: PendingAction, **kwargs): | ||||
|     if instance.pk: | ||||
|         # change status to completed once scheduled reboot date/time has expired | ||||
|         if instance.action_type == "schedreboot" and instance.status == "pending": | ||||
|         if ( | ||||
|             instance.action_type == PAAction.SCHED_REBOOT | ||||
|             and instance.status == PAStatus.PENDING | ||||
|         ): | ||||
|  | ||||
|             reboot_time = dt.datetime.strptime( | ||||
|                 instance.details["time"], "%Y-%m-%d %H:%M:%S" | ||||
| @@ -26,5 +31,5 @@ def handle_status(sender, instance: PendingAction, **kwargs): | ||||
|             reboot_time_utc = localized.astimezone(pytz.utc) | ||||
|  | ||||
|             if now > reboot_time_utc: | ||||
|                 instance.status = "completed" | ||||
|                 instance.status = PAStatus.COMPLETED | ||||
|                 instance.save(update_fields=["status"]) | ||||
|   | ||||
| @@ -4,6 +4,7 @@ from unittest.mock import patch | ||||
| from django.utils import timezone as djangotime | ||||
| from model_bakery import baker, seq | ||||
|  | ||||
| from tacticalrmm.constants import DebugLogLevel, DebugLogType, PAAction, PAStatus | ||||
| from tacticalrmm.test import TacticalTestCase | ||||
|  | ||||
| base_url = "/logs" | ||||
| @@ -168,15 +169,15 @@ class TestAuditViews(TacticalTestCase): | ||||
|         baker.make( | ||||
|             "logs.PendingAction", | ||||
|             agent=agent1, | ||||
|             action_type="chocoinstall", | ||||
|             action_type=PAAction.CHOCO_INSTALL, | ||||
|             details={"name": "googlechrome", "output": None, "installed": False}, | ||||
|             _quantity=12, | ||||
|         ) | ||||
|         baker.make( | ||||
|             "logs.PendingAction", | ||||
|             agent=agent2, | ||||
|             action_type="chocoinstall", | ||||
|             status="completed", | ||||
|             action_type=PAAction.CHOCO_INSTALL, | ||||
|             status=PAStatus.COMPLETED, | ||||
|             details={"name": "adobereader", "output": None, "installed": False}, | ||||
|             _quantity=14, | ||||
|         ) | ||||
| @@ -194,7 +195,7 @@ class TestAuditViews(TacticalTestCase): | ||||
|         action = baker.make( | ||||
|             "logs.PendingAction", | ||||
|             agent=agent, | ||||
|             action_type="schedreboot", | ||||
|             action_type=PAAction.SCHED_REBOOT, | ||||
|             details={ | ||||
|                 "time": "2021-01-13 18:20:00", | ||||
|                 "taskname": "TacticalRMM_SchedReboot_wYzCCDVXlc", | ||||
| @@ -220,7 +221,7 @@ class TestAuditViews(TacticalTestCase): | ||||
|         action2 = baker.make( | ||||
|             "logs.PendingAction", | ||||
|             agent=agent, | ||||
|             action_type="schedreboot", | ||||
|             action_type=PAAction.SCHED_REBOOT, | ||||
|             details={ | ||||
|                 "time": "2021-01-13 18:20:00", | ||||
|                 "taskname": "TacticalRMM_SchedReboot_wYzCCDVXlc", | ||||
| @@ -243,16 +244,16 @@ class TestAuditViews(TacticalTestCase): | ||||
|         agent = baker.make_recipe("agents.agent") | ||||
|         baker.make( | ||||
|             "logs.DebugLog", | ||||
|             log_level=cycle(["error", "info", "warning", "critical"]), | ||||
|             log_type="agent_issues", | ||||
|             log_level=cycle([i.value for i in DebugLogLevel]), | ||||
|             log_type=DebugLogType.AGENT_ISSUES, | ||||
|             agent=agent, | ||||
|             _quantity=4, | ||||
|         ) | ||||
|  | ||||
|         logs = baker.make( | ||||
|             "logs.DebugLog", | ||||
|             log_type="system_issues", | ||||
|             log_level=cycle(["error", "info", "warning", "critical"]), | ||||
|             log_type=DebugLogType.SYSTEM_ISSUES, | ||||
|             log_level=cycle([i.value for i in DebugLogLevel]), | ||||
|             _quantity=15, | ||||
|         ) | ||||
|  | ||||
| @@ -269,7 +270,10 @@ class TestAuditViews(TacticalTestCase): | ||||
|         self.assertEqual(len(resp.data), 1) | ||||
|  | ||||
|         # test time filter with other | ||||
|         data = {"logTypeFilter": "system_issues", "logLevelFilter": "error"} | ||||
|         data = { | ||||
|             "logTypeFilter": DebugLogType.SYSTEM_ISSUES.value, | ||||
|             "logLevelFilter": "error", | ||||
|         } | ||||
|         resp = self.client.patch(url, data, format="json") | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|         self.assertEqual(len(resp.data), 4) | ||||
| @@ -324,24 +328,24 @@ class TestAuditViews(TacticalTestCase): | ||||
|         agent2 = baker.make_recipe("agents.agent") | ||||
|         baker.make( | ||||
|             "logs.DebugLog", | ||||
|             log_level=cycle(["error", "info", "warning", "critical"]), | ||||
|             log_type="agent_issues", | ||||
|             log_level=cycle([i.value for i in DebugLogLevel]), | ||||
|             log_type=DebugLogType.AGENT_ISSUES, | ||||
|             agent=agent, | ||||
|             _quantity=4, | ||||
|         ) | ||||
|  | ||||
|         baker.make( | ||||
|             "logs.DebugLog", | ||||
|             log_level=cycle(["error", "info", "warning", "critical"]), | ||||
|             log_type="agent_issues", | ||||
|             log_level=cycle([i.value for i in DebugLogLevel]), | ||||
|             log_type=DebugLogType.AGENT_ISSUES, | ||||
|             agent=agent2, | ||||
|             _quantity=8, | ||||
|         ) | ||||
|  | ||||
|         baker.make( | ||||
|             "logs.DebugLog", | ||||
|             log_type="system_issues", | ||||
|             log_level=cycle(["error", "info", "warning", "critical"]), | ||||
|             log_type=DebugLogType.SYSTEM_ISSUES, | ||||
|             log_level=cycle([i.value for i in DebugLogLevel]), | ||||
|             _quantity=15, | ||||
|         ) | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| import asyncio | ||||
| from datetime import datetime as dt | ||||
|  | ||||
| from agents.models import Agent | ||||
| from django.core.paginator import Paginator | ||||
| from django.db.models import Q | ||||
| from django.shortcuts import get_object_or_404 | ||||
| @@ -11,9 +10,11 @@ from rest_framework.permissions import IsAuthenticated | ||||
| from rest_framework.response import Response | ||||
| from rest_framework.views import APIView | ||||
|  | ||||
| from tacticalrmm.constants import AGENT_DEFER | ||||
| from agents.models import Agent | ||||
| from tacticalrmm.constants import AGENT_DEFER, PAAction | ||||
| from tacticalrmm.helpers import notify_error | ||||
| from tacticalrmm.permissions import _audit_log_filter, _has_perm_on_agent | ||||
| from tacticalrmm.utils import get_default_timezone, notify_error | ||||
| from tacticalrmm.utils import get_default_timezone | ||||
|  | ||||
| from .models import AuditLog, DebugLog, PendingAction | ||||
| from .permissions import AuditLogPerms, DebugLogPerms, PendingActionPerms | ||||
| @@ -96,9 +97,14 @@ class PendingActions(APIView): | ||||
|     def get(self, request, agent_id=None): | ||||
|         if agent_id: | ||||
|             agent = get_object_or_404( | ||||
|                 Agent.objects.defer(*AGENT_DEFER), agent_id=agent_id | ||||
|                 Agent.objects.defer(*AGENT_DEFER).prefetch_related("pendingactions"), | ||||
|                 agent_id=agent_id, | ||||
|             ) | ||||
|             actions = ( | ||||
|                 PendingAction.objects.filter(agent=agent) | ||||
|                 .select_related("agent__site", "agent__site__client") | ||||
|                 .defer("agent__services", "agent__wmi_detail") | ||||
|             ) | ||||
|             actions = PendingAction.objects.filter(agent=agent) | ||||
|         else: | ||||
|             actions = ( | ||||
|                 PendingAction.objects.filter_by_role(request.user)  # type: ignore | ||||
| @@ -117,7 +123,7 @@ class PendingActions(APIView): | ||||
|         if not _has_perm_on_agent(request.user, action.agent.agent_id): | ||||
|             raise PermissionDenied() | ||||
|  | ||||
|         if action.action_type == "schedreboot": | ||||
|         if action.action_type == PAAction.SCHED_REBOOT: | ||||
|             nats_data = { | ||||
|                 "func": "delschedtask", | ||||
|                 "schedtaskpayload": {"name": action.details["taskname"]}, | ||||
|   | ||||
							
								
								
									
										7
									
								
								api/tacticalrmm/pytest.ini
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								api/tacticalrmm/pytest.ini
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| [pytest] | ||||
| DJANGO_SETTINGS_MODULE = tacticalrmm.settings | ||||
| python_files = tests.py test_*.py | ||||
| addopts = --capture=tee-sys -vv --cov --cov-config=.coveragerc --cov-report=xml | ||||
|  | ||||
| filterwarnings = | ||||
|     ignore::django.core.cache.CacheKeyWarning | ||||
| @@ -7,3 +7,7 @@ django-silk | ||||
| mypy | ||||
| django-stubs | ||||
| djangorestframework-stubs | ||||
| django-types | ||||
| djangorestframework-types | ||||
| celery-types | ||||
| msgpack-types | ||||
|   | ||||
| @@ -1,5 +1,8 @@ | ||||
| coverage | ||||
| coveralls | ||||
| model_bakery | ||||
| black | ||||
| tblib | ||||
| pytest | ||||
| pytest-django | ||||
| pytest-xdist | ||||
| pytest-cov | ||||
| codecov | ||||
| @@ -1,14 +1,14 @@ | ||||
| asgiref==3.5.0 | ||||
| asgiref==3.5.1 | ||||
| celery==5.2.6 | ||||
| certifi==2021.10.8 | ||||
| cffi==1.15.0 | ||||
| channels==3.0.4 | ||||
| channels_redis==3.4.0 | ||||
| chardet==4.0.0 | ||||
| cryptography==36.0.2 | ||||
| cryptography==37.0.2 | ||||
| daphne==3.0.2 | ||||
| Django==4.0.4 | ||||
| django-cors-headers==3.11.0 | ||||
| django-cors-headers==3.12.0 | ||||
| django-ipware==4.0.2 | ||||
| django-rest-knox==4.2.0 | ||||
| djangorestframework==3.13.1 | ||||
| @@ -19,20 +19,20 @@ psycopg2-binary==2.9.3 | ||||
| pycparser==2.21 | ||||
| pycryptodome==3.14.1 | ||||
| pyotp==2.6.0 | ||||
| pyparsing==3.0.8 | ||||
| pyparsing==3.0.9 | ||||
| pytz==2022.1 | ||||
| qrcode==7.3.1 | ||||
| redis==4.2.2 | ||||
| redis==4.3.1 | ||||
| hiredis==2.0.0 | ||||
| requests==2.27.1 | ||||
| six==1.16.0 | ||||
| sqlparse==0.4.2 | ||||
| twilio==7.8.2 | ||||
| twilio==7.9.0 | ||||
| urllib3==1.26.9 | ||||
| uWSGI==2.0.20 | ||||
| validators==0.18.2 | ||||
| validators==0.19.0 | ||||
| vine==5.0.0 | ||||
| websockets==10.2 | ||||
| websockets==10.3 | ||||
| zipp==3.8.0 | ||||
| drf_spectacular==0.22.0 | ||||
| drf_spectacular==0.22.1 | ||||
| meshctrl==0.1.15 | ||||
|   | ||||
| @@ -1,9 +1,61 @@ | ||||
| from model_bakery.recipe import Recipe | ||||
|  | ||||
| from tacticalrmm.constants import ScriptShell, ScriptType | ||||
| from tacticalrmm.demo_data import ( | ||||
|     check_storage_pool_health_ps1, | ||||
|     clear_print_spool_bat, | ||||
|     redhat_insights, | ||||
|     show_temp_dir_py, | ||||
| ) | ||||
|  | ||||
| script = Recipe( | ||||
|     "scripts.Script", | ||||
|     name="Test Script", | ||||
|     description="Test Desc", | ||||
|     shell="cmd", | ||||
|     script_type="userdefined", | ||||
|     shell=ScriptShell.CMD, | ||||
|     script_type=ScriptType.USER_DEFINED, | ||||
| ) | ||||
|  | ||||
| batch_script = Recipe( | ||||
|     "scripts.Script", | ||||
|     name="Test Batch Script", | ||||
|     description="Test Batch Desc", | ||||
|     shell=ScriptShell.CMD, | ||||
|     script_type=ScriptType.USER_DEFINED, | ||||
|     script_body=clear_print_spool_bat, | ||||
|     args=["one", "two"], | ||||
| ) | ||||
|  | ||||
| ps_script = Recipe( | ||||
|     "scripts.Script", | ||||
|     name="Test Powershell Script", | ||||
|     description="Test Powershell Desc", | ||||
|     shell=ScriptShell.POWERSHELL, | ||||
|     script_type=ScriptType.USER_DEFINED, | ||||
|     script_body=check_storage_pool_health_ps1, | ||||
|     args=["one"], | ||||
|     supported_platforms=["windows"], | ||||
| ) | ||||
|  | ||||
| py_script = Recipe( | ||||
|     "scripts.Script", | ||||
|     name="Test Python Script", | ||||
|     description="Test Python Desc", | ||||
|     shell=ScriptShell.PYTHON, | ||||
|     script_body=show_temp_dir_py, | ||||
|     script_type=ScriptType.USER_DEFINED, | ||||
|     supported_platforms=["windows", "linux"], | ||||
|     category="py stuff", | ||||
| ) | ||||
|  | ||||
| bash_script = Recipe( | ||||
|     "scripts.Script", | ||||
|     name="Test Bash Script", | ||||
|     description="Test Bash Desc", | ||||
|     shell=ScriptShell.SHELL, | ||||
|     script_body=redhat_insights, | ||||
|     script_type=ScriptType.USER_DEFINED, | ||||
|     args=["one"], | ||||
|     supported_platforms=["linux"], | ||||
|     category="RHSA", | ||||
| ) | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| from django.core.management.base import BaseCommand | ||||
|  | ||||
| from scripts.models import Script | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -6,22 +6,11 @@ from typing import List | ||||
| from django.contrib.postgres.fields import ArrayField | ||||
| from django.db import models | ||||
| from django.db.models.fields import CharField, TextField | ||||
|  | ||||
| from logs.models import BaseAuditModel | ||||
|  | ||||
| from tacticalrmm.constants import ScriptShell, ScriptType | ||||
| from tacticalrmm.utils import replace_db_values | ||||
|  | ||||
| SCRIPT_SHELLS = [ | ||||
|     ("powershell", "Powershell"), | ||||
|     ("cmd", "Batch (CMD)"), | ||||
|     ("python", "Python"), | ||||
|     ("shell", "Shell"), | ||||
| ] | ||||
|  | ||||
| SCRIPT_TYPES = [ | ||||
|     ("userdefined", "User Defined"), | ||||
|     ("builtin", "Built In"), | ||||
| ] | ||||
|  | ||||
|  | ||||
| class Script(BaseAuditModel): | ||||
|     guid = models.CharField(max_length=64, null=True, blank=True) | ||||
| @@ -29,10 +18,10 @@ class Script(BaseAuditModel): | ||||
|     description = models.TextField(null=True, blank=True, default="") | ||||
|     filename = models.CharField(max_length=255, null=True, blank=True) | ||||
|     shell = models.CharField( | ||||
|         max_length=100, choices=SCRIPT_SHELLS, default="powershell" | ||||
|         max_length=100, choices=ScriptShell.choices, default=ScriptShell.POWERSHELL | ||||
|     ) | ||||
|     script_type = models.CharField( | ||||
|         max_length=100, choices=SCRIPT_TYPES, default="userdefined" | ||||
|         max_length=100, choices=ScriptType.choices, default=ScriptType.USER_DEFINED | ||||
|     ) | ||||
|     args = ArrayField( | ||||
|         models.TextField(null=True, blank=True), | ||||
| @@ -108,7 +97,9 @@ class Script(BaseAuditModel): | ||||
|  | ||||
|         for script in info: | ||||
|             if os.path.exists(os.path.join(scripts_dir, script["filename"])): | ||||
|                 s = cls.objects.filter(script_type="builtin", guid=script["guid"]) | ||||
|                 s = cls.objects.filter( | ||||
|                     script_type=ScriptType.BUILT_IN, guid=script["guid"] | ||||
|                 ) | ||||
|  | ||||
|                 category = ( | ||||
|                     script["category"] if "category" in script.keys() else "Community" | ||||
| @@ -163,7 +154,7 @@ class Script(BaseAuditModel): | ||||
|                             name=script["name"], | ||||
|                             description=script["description"], | ||||
|                             shell=script["shell"], | ||||
|                             script_type="builtin", | ||||
|                             script_type=ScriptType.BUILT_IN, | ||||
|                             category=category, | ||||
|                             default_timeout=default_timeout, | ||||
|                             args=args, | ||||
| @@ -178,7 +169,7 @@ class Script(BaseAuditModel): | ||||
|  | ||||
|         # check for community scripts that were deleted from json and scripts folder | ||||
|         count, _ = ( | ||||
|             Script.objects.filter(script_type="builtin") | ||||
|             Script.objects.filter(script_type=ScriptType.BUILT_IN) | ||||
|             .exclude(guid__in=community_scripts_processed) | ||||
|             .delete() | ||||
|         ) | ||||
| @@ -210,7 +201,12 @@ class Script(BaseAuditModel): | ||||
|             if match: | ||||
|                 # only get the match between the () in regex | ||||
|                 string = match.group(1) | ||||
|                 value = replace_db_values(string=string, instance=agent, shell=shell) | ||||
|                 value = replace_db_values( | ||||
|                     string=string, | ||||
|                     instance=agent, | ||||
|                     shell=shell, | ||||
|                     quotes=True if shell != ScriptShell.CMD else False, | ||||
|                 ) | ||||
|  | ||||
|                 if value: | ||||
|                     temp_args.append(re.sub("\\{\\{.*\\}\\}", value, arg)) | ||||
| @@ -228,7 +224,9 @@ class ScriptSnippet(models.Model): | ||||
|     name = CharField(max_length=40, unique=True) | ||||
|     desc = CharField(max_length=50, blank=True, default="") | ||||
|     code = TextField(default="") | ||||
|     shell = CharField(max_length=15, choices=SCRIPT_SHELLS, default="powershell") | ||||
|     shell = CharField( | ||||
|         max_length=15, choices=ScriptShell.choices, default=ScriptShell.POWERSHELL | ||||
|     ) | ||||
|  | ||||
|     def __str__(self): | ||||
|         return self.name | ||||
|   | ||||
| @@ -1,9 +1,8 @@ | ||||
| import asyncio | ||||
|  | ||||
| from typing import List | ||||
|  | ||||
| from agents.models import Agent, AgentHistory | ||||
| from scripts.models import Script | ||||
|  | ||||
| from tacticalrmm.celery import app | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -3,6 +3,7 @@ from unittest.mock import patch | ||||
| from django.test import override_settings | ||||
| from model_bakery import baker | ||||
|  | ||||
| from tacticalrmm.constants import ScriptShell, ScriptType | ||||
| from tacticalrmm.test import TacticalTestCase | ||||
|  | ||||
| from .models import Script, ScriptSnippet | ||||
| @@ -36,7 +37,7 @@ class TestScriptViews(TacticalTestCase): | ||||
|         data = { | ||||
|             "name": "Name", | ||||
|             "description": "Description", | ||||
|             "shell": "powershell", | ||||
|             "shell": ScriptShell.POWERSHELL, | ||||
|             "category": "New", | ||||
|             "script_body": "Test Script", | ||||
|             "default_timeout": 99, | ||||
| @@ -80,7 +81,7 @@ class TestScriptViews(TacticalTestCase): | ||||
|         resp = self.client.put(url, data, format="json") | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|         script = Script.objects.get(pk=script.pk) | ||||
|         self.assertEquals(script.description, "Description Change") | ||||
|         self.assertEqual(script.description, "Description Change") | ||||
|  | ||||
|         # correct_hash = hmac.new( | ||||
|         #     settings.SECRET_KEY.encode(), data["script_body"].encode(), hashlib.sha256 | ||||
| @@ -93,7 +94,9 @@ class TestScriptViews(TacticalTestCase): | ||||
|             "description": "New Desc", | ||||
|             "script_body": "aasdfdsf", | ||||
|         }  # Test | ||||
|         builtin_script = baker.make_recipe("scripts.script", script_type="builtin") | ||||
|         builtin_script = baker.make_recipe( | ||||
|             "scripts.script", script_type=ScriptType.BUILT_IN | ||||
|         ) | ||||
|  | ||||
|         resp = self.client.put(f"/scripts/{builtin_script.pk}/", data, format="json") | ||||
|         self.assertEqual(resp.status_code, 400) | ||||
| @@ -136,7 +139,7 @@ class TestScriptViews(TacticalTestCase): | ||||
|             "code": "some_code", | ||||
|             "timeout": 90, | ||||
|             "args": [], | ||||
|             "shell": "powershell", | ||||
|             "shell": ScriptShell.POWERSHELL, | ||||
|         } | ||||
|  | ||||
|         resp = self.client.post(url, data, format="json") | ||||
| @@ -159,7 +162,7 @@ class TestScriptViews(TacticalTestCase): | ||||
|         self.assertFalse(Script.objects.filter(pk=script.pk).exists()) | ||||
|  | ||||
|         # test delete community script | ||||
|         script = baker.make_recipe("scripts.script", script_type="builtin") | ||||
|         script = baker.make_recipe("scripts.script", script_type=ScriptType.BUILT_IN) | ||||
|         url = f"/scripts/{script.pk}/" | ||||
|         resp = self.client.delete(url, format="json") | ||||
|         self.assertEqual(resp.status_code, 400) | ||||
| @@ -175,7 +178,9 @@ class TestScriptViews(TacticalTestCase): | ||||
|  | ||||
|         # test powershell file | ||||
|         script = baker.make( | ||||
|             "scripts.Script", script_body="Test Script Body", shell="powershell" | ||||
|             "scripts.Script", | ||||
|             script_body="Test Script Body", | ||||
|             shell=ScriptShell.POWERSHELL, | ||||
|         ) | ||||
|         url = f"/scripts/{script.pk}/download/" | ||||
|  | ||||
| @@ -187,7 +192,7 @@ class TestScriptViews(TacticalTestCase): | ||||
|  | ||||
|         # test batch file | ||||
|         script = baker.make( | ||||
|             "scripts.Script", script_body="Test Script Body", shell="cmd" | ||||
|             "scripts.Script", script_body="Test Script Body", shell=ScriptShell.CMD | ||||
|         ) | ||||
|         url = f"/scripts/{script.pk}/download/" | ||||
|  | ||||
| @@ -199,7 +204,7 @@ class TestScriptViews(TacticalTestCase): | ||||
|  | ||||
|         # test python file | ||||
|         script = baker.make( | ||||
|             "scripts.Script", script_body="Test Script Body", shell="python" | ||||
|             "scripts.Script", script_body="Test Script Body", shell=ScriptShell.PYTHON | ||||
|         ) | ||||
|         url = f"/scripts/{script.pk}/download/" | ||||
|  | ||||
| @@ -228,7 +233,7 @@ class TestScriptViews(TacticalTestCase): | ||||
|                 f"-Client '{agent.client.name}'", | ||||
|                 f"-Site '{agent.site.name}'", | ||||
|             ], | ||||
|             Script.parse_script_args(agent=agent, shell="python", args=args), | ||||
|             Script.parse_script_args(agent=agent, shell=ScriptShell.PYTHON, args=args), | ||||
|         ) | ||||
|  | ||||
|     def test_script_arg_replacement_custom_field(self): | ||||
| @@ -246,7 +251,7 @@ class TestScriptViews(TacticalTestCase): | ||||
|         # test default value | ||||
|         self.assertEqual( | ||||
|             ["-Parameter", "-Another 'DEFAULT'"], | ||||
|             Script.parse_script_args(agent=agent, shell="python", args=args), | ||||
|             Script.parse_script_args(agent=agent, shell=ScriptShell.PYTHON, args=args), | ||||
|         ) | ||||
|  | ||||
|         # test with set value | ||||
| @@ -258,7 +263,7 @@ class TestScriptViews(TacticalTestCase): | ||||
|         ) | ||||
|         self.assertEqual( | ||||
|             ["-Parameter", "-Another 'CUSTOM VALUE'"], | ||||
|             Script.parse_script_args(agent=agent, shell="python", args=args), | ||||
|             Script.parse_script_args(agent=agent, shell=ScriptShell.PYTHON, args=args), | ||||
|         ) | ||||
|  | ||||
|     def test_script_arg_replacement_client_custom_fields(self): | ||||
| @@ -276,7 +281,7 @@ class TestScriptViews(TacticalTestCase): | ||||
|         # test default value | ||||
|         self.assertEqual( | ||||
|             ["-Parameter", "-Another 'DEFAULT'"], | ||||
|             Script.parse_script_args(agent=agent, shell="python", args=args), | ||||
|             Script.parse_script_args(agent=agent, shell=ScriptShell.PYTHON, args=args), | ||||
|         ) | ||||
|  | ||||
|         # test with set value | ||||
| @@ -288,7 +293,7 @@ class TestScriptViews(TacticalTestCase): | ||||
|         ) | ||||
|         self.assertEqual( | ||||
|             ["-Parameter", "-Another 'CUSTOM VALUE'"], | ||||
|             Script.parse_script_args(agent=agent, shell="python", args=args), | ||||
|             Script.parse_script_args(agent=agent, shell=ScriptShell.PYTHON, args=args), | ||||
|         ) | ||||
|  | ||||
|     def test_script_arg_replacement_site_custom_fields(self): | ||||
| @@ -306,7 +311,7 @@ class TestScriptViews(TacticalTestCase): | ||||
|         # test default value | ||||
|         self.assertEqual( | ||||
|             ["-Parameter", "-Another 'DEFAULT'"], | ||||
|             Script.parse_script_args(agent=agent, shell="python", args=args), | ||||
|             Script.parse_script_args(agent=agent, shell=ScriptShell.PYTHON, args=args), | ||||
|         ) | ||||
|  | ||||
|         # test with set value | ||||
| @@ -318,7 +323,7 @@ class TestScriptViews(TacticalTestCase): | ||||
|         ) | ||||
|         self.assertEqual( | ||||
|             ["-Parameter", "-Another 'CUSTOM VALUE'"], | ||||
|             Script.parse_script_args(agent=agent, shell="python", args=args), | ||||
|             Script.parse_script_args(agent=agent, shell=ScriptShell.PYTHON, args=args), | ||||
|         ) | ||||
|  | ||||
|         # test with set but empty field value | ||||
| @@ -327,7 +332,7 @@ class TestScriptViews(TacticalTestCase): | ||||
|  | ||||
|         self.assertEqual( | ||||
|             ["-Parameter", "-Another 'DEFAULT'"], | ||||
|             Script.parse_script_args(agent=agent, shell="python", args=args), | ||||
|             Script.parse_script_args(agent=agent, shell=ScriptShell.PYTHON, args=args), | ||||
|         ) | ||||
|  | ||||
|         # test blank default and value | ||||
| @@ -336,7 +341,7 @@ class TestScriptViews(TacticalTestCase): | ||||
|  | ||||
|         self.assertEqual( | ||||
|             ["-Parameter", "-Another ''"], | ||||
|             Script.parse_script_args(agent=agent, shell="python", args=args), | ||||
|             Script.parse_script_args(agent=agent, shell=ScriptShell.PYTHON, args=args), | ||||
|         ) | ||||
|  | ||||
|     def test_script_arg_replacement_array_fields(self): | ||||
| @@ -354,7 +359,7 @@ class TestScriptViews(TacticalTestCase): | ||||
|         # test default value | ||||
|         self.assertEqual( | ||||
|             ["-Parameter", "-Another 'this,is,an,array'"], | ||||
|             Script.parse_script_args(agent=agent, shell="python", args=args), | ||||
|             Script.parse_script_args(agent=agent, shell=ScriptShell.PYTHON, args=args), | ||||
|         ) | ||||
|  | ||||
|         # test with set value and python shell | ||||
| @@ -366,7 +371,7 @@ class TestScriptViews(TacticalTestCase): | ||||
|         ) | ||||
|         self.assertEqual( | ||||
|             ["-Parameter", "-Another 'this,is,new'"], | ||||
|             Script.parse_script_args(agent=agent, shell="python", args=args), | ||||
|             Script.parse_script_args(agent=agent, shell=ScriptShell.PYTHON, args=args), | ||||
|         ) | ||||
|  | ||||
|     def test_script_arg_replacement_boolean_fields(self): | ||||
| @@ -384,7 +389,7 @@ class TestScriptViews(TacticalTestCase): | ||||
|         # test default value with python | ||||
|         self.assertEqual( | ||||
|             ["-Parameter", "-Another 1"], | ||||
|             Script.parse_script_args(agent=agent, shell="python", args=args), | ||||
|             Script.parse_script_args(agent=agent, shell=ScriptShell.PYTHON, args=args), | ||||
|         ) | ||||
|  | ||||
|         # test with set value and python shell | ||||
| @@ -396,19 +401,21 @@ class TestScriptViews(TacticalTestCase): | ||||
|         ) | ||||
|         self.assertEqual( | ||||
|             ["-Parameter", "-Another 0"], | ||||
|             Script.parse_script_args(agent=agent, shell="python", args=args), | ||||
|             Script.parse_script_args(agent=agent, shell=ScriptShell.PYTHON, args=args), | ||||
|         ) | ||||
|  | ||||
|         # test with set value and cmd shell | ||||
|         self.assertEqual( | ||||
|             ["-Parameter", "-Another 0"], | ||||
|             Script.parse_script_args(agent=agent, shell="cmd", args=args), | ||||
|             Script.parse_script_args(agent=agent, shell=ScriptShell.CMD, args=args), | ||||
|         ) | ||||
|  | ||||
|         # test with set value and powershell | ||||
|         self.assertEqual( | ||||
|             ["-Parameter", "-Another $False"], | ||||
|             Script.parse_script_args(agent=agent, shell="powershell", args=args), | ||||
|             Script.parse_script_args( | ||||
|                 agent=agent, shell=ScriptShell.POWERSHELL, args=args | ||||
|             ), | ||||
|         ) | ||||
|  | ||||
|         # test with True value powershell | ||||
| @@ -417,7 +424,9 @@ class TestScriptViews(TacticalTestCase): | ||||
|  | ||||
|         self.assertEqual( | ||||
|             ["-Parameter", "-Another $True"], | ||||
|             Script.parse_script_args(agent=agent, shell="powershell", args=args), | ||||
|             Script.parse_script_args( | ||||
|                 agent=agent, shell=ScriptShell.POWERSHELL, args=args | ||||
|             ), | ||||
|         ) | ||||
|  | ||||
|  | ||||
| @@ -443,7 +452,7 @@ class TestScriptSnippetViews(TacticalTestCase): | ||||
|         data = { | ||||
|             "name": "Name", | ||||
|             "description": "Description", | ||||
|             "shell": "powershell", | ||||
|             "shell": ScriptShell.POWERSHELL, | ||||
|             "code": "Test", | ||||
|         } | ||||
|  | ||||
| @@ -467,7 +476,7 @@ class TestScriptSnippetViews(TacticalTestCase): | ||||
|         resp = self.client.put(url, data, format="json") | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|         snippet = ScriptSnippet.objects.get(pk=snippet.pk) | ||||
|         self.assertEquals(snippet.name, "New Name") | ||||
|         self.assertEqual(snippet.name, "New Name") | ||||
|  | ||||
|         self.check_not_authenticated("put", url) | ||||
|  | ||||
|   | ||||
| @@ -1,13 +1,14 @@ | ||||
| import asyncio | ||||
|  | ||||
| from agents.permissions import RunScriptPerms | ||||
| from django.shortcuts import get_object_or_404 | ||||
| from rest_framework.decorators import api_view, permission_classes | ||||
| from rest_framework.permissions import IsAuthenticated | ||||
| from rest_framework.response import Response | ||||
| from rest_framework.views import APIView | ||||
|  | ||||
| from tacticalrmm.utils import notify_error | ||||
| from agents.permissions import RunScriptPerms | ||||
| from tacticalrmm.constants import ScriptShell, ScriptType | ||||
| from tacticalrmm.helpers import notify_error | ||||
|  | ||||
| from .models import Script, ScriptSnippet | ||||
| from .permissions import ScriptsPerms | ||||
| @@ -27,7 +28,7 @@ class GetAddScripts(APIView): | ||||
|         showHiddenScripts = request.GET.get("showHiddenScripts", False) | ||||
|  | ||||
|         if not showCommunityScripts or showCommunityScripts == "false": | ||||
|             scripts = Script.objects.filter(script_type="userdefined") | ||||
|             scripts = Script.objects.filter(script_type=ScriptType.USER_DEFINED) | ||||
|         else: | ||||
|             scripts = Script.objects.all() | ||||
|  | ||||
| @@ -61,7 +62,7 @@ class GetUpdateDeleteScript(APIView): | ||||
|  | ||||
|         data = request.data | ||||
|  | ||||
|         if script.script_type == "builtin": | ||||
|         if script.script_type == ScriptType.BUILT_IN: | ||||
|             # allow only favoriting builtin scripts | ||||
|             if "favorite" in data: | ||||
|                 # overwrite request data | ||||
| @@ -83,7 +84,7 @@ class GetUpdateDeleteScript(APIView): | ||||
|         script = get_object_or_404(Script, pk=pk) | ||||
|  | ||||
|         # this will never trigger but check anyway | ||||
|         if script.script_type == "builtin": | ||||
|         if script.script_type == ScriptType.BUILT_IN: | ||||
|             return notify_error("Community scripts cannot be deleted") | ||||
|  | ||||
|         script.delete() | ||||
| @@ -172,16 +173,21 @@ def download(request, pk): | ||||
|     if with_snippets == "false": | ||||
|         with_snippets = False | ||||
|  | ||||
|     if script.shell == "powershell": | ||||
|         filename = f"{script.name}.ps1" | ||||
|     elif script.shell == "cmd": | ||||
|         filename = f"{script.name}.bat" | ||||
|     else: | ||||
|         filename = f"{script.name}.py" | ||||
|     match script.shell: | ||||
|         case ScriptShell.POWERSHELL: | ||||
|             ext = ".ps1" | ||||
|         case ScriptShell.CMD: | ||||
|             ext = ".bat" | ||||
|         case ScriptShell.PYTHON: | ||||
|             ext = ".py" | ||||
|         case ScriptShell.SHELL: | ||||
|             ext = ".sh" | ||||
|         case _: | ||||
|             ext = "" | ||||
|  | ||||
|     return Response( | ||||
|         { | ||||
|             "filename": filename, | ||||
|             "filename": f"{script.name}{ext}", | ||||
|             "code": script.code if with_snippets else script.code_no_snippets, | ||||
|         } | ||||
|     ) | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user