Compare commits

...

138 Commits

Author SHA1 Message Date
wh1te909
da185875bb Release 0.15.1 2022-10-19 22:51:33 +00:00
wh1te909
af16912541 bump versions [skip ci] 2022-10-19 22:50:59 +00:00
wh1te909
1bf9e2a5e6 update reqs 2022-10-19 06:50:01 +00:00
wh1te909
5a572651ff add token expired check 2022-10-19 06:45:53 +00:00
wh1te909
5a191e387f update tests 2022-10-19 04:02:21 +00:00
wh1te909
18f29f5790 update reqs 2022-10-18 19:54:48 +00:00
wh1te909
054a73e0f8 fix tests 2022-10-18 05:07:00 +00:00
wh1te909
14824db7b0 fix tests 2022-10-18 05:02:35 +00:00
wh1te909
721c48ea88 python 3.10.8 2022-10-18 04:54:04 +00:00
wh1te909
ed7bfcfb58 daphne/channels 4 2022-10-18 04:52:04 +00:00
wh1te909
773a40a126 bump docker nats 2022-10-18 04:50:48 +00:00
wh1te909
961252ef26 rework uwsgi conf 2022-10-18 00:23:10 +00:00
wh1te909
a2650f3c47 update setuptools 2022-10-18 00:21:11 +00:00
wh1te909
d71ee194e1 add optional nats monitoring 2022-10-18 00:12:57 +00:00
wh1te909
22e1a4cf41 update reqs 2022-10-14 07:41:16 +00:00
wh1te909
a50bf901d3 add check for wget fixes #1317 2022-10-14 07:39:27 +00:00
wh1te909
c9469635b5 remove unneeded chmod fixes #1307 2022-10-14 07:21:43 +00:00
Dan
36df3278e5 Merge pull request #1311 from dinger1986/develop
Update agent_linux.sh
2022-10-11 16:14:06 -07:00
dinger1986
cb2258aaa8 Update agent_linux.sh 2022-10-10 23:53:24 +01:00
wh1te909
0391d9eb7e update reqs 2022-10-10 17:07:00 +00:00
Dan
12698b4c20 Merge pull request #1310 from dinger1986/develop
Update agent_linux.sh
2022-10-10 10:01:50 -07:00
dinger1986
f7b9d459ab Update agent_linux.sh 2022-10-10 17:51:35 +01:00
wh1te909
65ab14e68b cleanup socket fixes #1210 2022-10-07 17:15:37 +00:00
Dan
93a5dd5de4 Merge pull request #1213 from stavros-k/json
Updating meshcentral's config casing to match upstreams json schema.
2022-10-06 09:38:16 -07:00
Dan
61807bdaaa Merge pull request #1301 from af7567/policyselect2
break from loop once a valid policy is found. remove old unused list
2022-10-06 09:36:59 -07:00
Dan
a1a5d1adba Merge pull request #1295 from silversword411/develop
typo
2022-10-06 09:36:20 -07:00
silversword411
9dd4aefea5 Merge branch 'amidaware:develop' into develop 2022-10-06 07:52:47 -04:00
Adam
db4540089a remove parentheses from if statement to fix codestyle test
Signed-off-by: Adam <adam@csparker.co.uk>
2022-10-03 13:17:48 +01:00
Adam
24c899c91a break from loop once a valid policy is found. remove old unused list
Signed-off-by: Adam <adam@csparker.co.uk>
2022-10-03 12:05:32 +01:00
wh1te909
ade1a73966 code cleanup and optimizations 2022-10-02 21:42:46 +00:00
wh1te909
fb9ec2b040 update reqs 2022-10-02 21:42:03 +00:00
wh1te909
3a683812e9 change tempdir 2022-10-02 21:37:04 +00:00
wh1te909
6d317603c9 fix script snippets fixes #1298 2022-09-27 23:21:26 +00:00
silversword411
5a3d2d196c typo 2022-09-25 17:21:17 -04:00
wh1te909
70e75a355c Release 0.15.0 2022-09-24 03:09:12 +00:00
wh1te909
4f885c9a79 bump versions 2022-09-24 03:08:44 +00:00
wh1te909
b519d2afac update readme for macOS support 2022-09-24 03:07:38 +00:00
wh1te909
6b61e3b76b bump docker nats 2022-09-24 02:52:12 +00:00
wh1te909
30b9c72c31 bump nats 2022-09-24 01:51:59 +00:00
wh1te909
385bf74f6e change dl cmd 2022-09-24 01:51:52 +00:00
wh1te909
be5615e530 bump web dev 2022-09-23 23:07:48 +00:00
wh1te909
d81a03c093 mac agent 2022-09-23 22:57:29 +00:00
wh1te909
f8249c8267 switch to corp funding 2022-09-23 22:10:05 +00:00
wh1te909
5a1cbdcd3b update reqs 2022-09-23 06:59:44 +00:00
wh1te909
e0c99d87bd update template 2022-09-23 06:59:26 +00:00
wh1te909
548250029d add missing field to serializer fixes #1283 2022-09-13 19:18:16 +00:00
wh1te909
66a354dbdc back to dev [skip ci] 2022-09-12 07:03:05 +00:00
wh1te909
834e602686 Release 0.14.8 2022-09-12 06:57:39 +00:00
wh1te909
1f693ca4f6 bump versions 2022-09-12 06:50:55 +00:00
wh1te909
97a0bc6045 optimize query to use less ram 2022-09-12 05:33:07 +00:00
wh1te909
8b75cdfefd update bin 2022-09-12 01:44:21 +00:00
wh1te909
917aecf1ff update deps and go 1.19 2022-09-12 01:42:24 +00:00
wh1te909
663dcd0396 fix tests 2022-09-11 20:51:57 +00:00
wh1te909
8f2dffb1ad update reqs 2022-09-11 20:29:45 +00:00
wh1te909
20228e3d19 add postgres ready check 2022-09-11 20:23:18 +00:00
wh1te909
81c6cc11b3 update supported version 2022-09-11 01:37:45 +00:00
wh1te909
2ccacbe5f3 fix for newer mesh 2022-09-11 01:33:00 +00:00
Dan
a5345e8468 Merge pull request #1277 from af7567/winupdatesfix
When checking if patches are to be installed, don't exit the function after finding a patched machine.
2022-09-06 12:02:26 -07:00
Dan
8f5d62bb81 Merge pull request #1273 from silversword411/develop
Adding client and site to mgmt command find_software
2022-09-06 11:59:07 -07:00
Adam
28f6838560 continue to next agent rather than return from function, otherwise other agents won't receive updates
Signed-off-by: Adam <adam@csparker.co.uk>
2022-09-04 18:09:58 +01:00
silversword411
c86aacb31c format tweak 2022-09-02 17:24:33 -04:00
silversword411
f62f5192d6 Adding client and site to mgmt command find_software 2022-09-02 14:28:45 -04:00
wh1te909
b14ea1fe3e fix maintenance mode and assigning policy via agent context menu not saving 2022-08-24 08:29:13 +00:00
wh1te909
552633a00b fix configs 2022-08-24 08:27:50 +00:00
wh1te909
7faba2a690 add migrations for updated pytz 2022-08-24 07:33:55 +00:00
wh1te909
db910aff06 back to dev 2022-08-23 06:02:48 +00:00
wh1te909
72126052ad Release 0.14.7 2022-08-23 05:59:29 +00:00
wh1te909
75d9f6a7e7 bump versions 2022-08-23 05:09:55 +00:00
wh1te909
de677294c6 refactor 2022-08-23 00:54:07 +00:00
wh1te909
da1e6b8259 update reqs 2022-08-22 06:47:52 +00:00
wh1te909
a9633b3990 update reqs 2022-08-20 20:09:53 +00:00
sadnub
7ac9af1cc1 fix sed command to do an inplace update of nginx.conf 2022-08-17 11:52:54 -04:00
Dan
00d8b8cd61 Merge pull request #1244 from dinger1986/develop
Update troubleshoot_server.sh
2022-08-17 08:43:13 -07:00
dinger1986
af7ff7f5cf Update troubleshoot_server.sh
Change SSL check as wasnt working and add output
2022-08-17 11:42:05 +01:00
wh1te909
2a20719130 fix demo script check history graph 2022-08-17 06:57:05 +00:00
sadnub
f481940180 fix tests 2022-08-14 21:56:09 -04:00
sadnub
ca8824d1e3 fix clients not filtering by role in policy overview 2022-08-14 21:49:38 -04:00
sadnub
f4be199b77 fix checks/tasks return cache values for other plats 2022-08-14 19:45:11 -04:00
sadnub
6bcef8334e fix nginx entypoint 2022-08-14 10:26:55 -04:00
wh1te909
3955eff683 more dev setup 2022-08-14 08:17:08 +00:00
wh1te909
aa0f6ecd75 python 3.10.6 2022-08-14 08:14:59 +00:00
wh1te909
ef4a94ed78 more ansible 2022-08-12 17:21:25 +00:00
wh1te909
b5c803ce65 update reqs 2022-08-12 17:19:04 +00:00
wh1te909
d4325ed82e more ansible dev 2022-08-12 06:15:21 +00:00
wh1te909
3805fb8f26 expose more fields in search amidaware/tacticalrmm-web@93dbc74e33 closes #652 2022-08-12 01:23:33 +00:00
wh1te909
d4d938c655 update paths 2022-08-12 01:06:56 +00:00
wh1te909
1c6911e361 start moving to nested serializers 2022-08-10 07:15:23 +00:00
wh1te909
a1b364f337 drop support for agent < 2.0.0 2022-08-10 07:12:51 +00:00
wh1te909
ece5c3da86 update reqs 2022-08-10 00:49:20 +00:00
wh1te909
5d1ae6047b back to dev 2022-08-10 00:38:13 +00:00
wh1te909
5605c72253 Release 0.14.6 2022-08-09 21:47:20 +00:00
wh1te909
66bbcf0733 fix tests 2022-08-09 21:35:23 +00:00
wh1te909
acc23ea7bb bump versions 2022-08-09 21:18:41 +00:00
wh1te909
663bd0c9f0 remove dead code 2022-08-09 21:16:18 +00:00
wh1te909
39b1025dfa fix tests 2022-08-05 17:35:40 +00:00
sadnub
d2875e90b2 fix docker dev 2022-08-05 12:10:52 -04:00
wh1te909
ff461d1d02 fixes #1174 amidaware/tacticalrmm-web@76f330fb9c 2022-08-05 07:22:46 +00:00
wh1te909
58164ea2d3 dev 2022-08-05 05:57:38 +00:00
wh1te909
1bf4834004 Django 4.1 2022-08-04 23:43:57 +00:00
wh1te909
bf58d78281 fix return tuple formatting 2022-08-04 23:40:35 +00:00
wh1te909
0dc749bb3d Release 0.14.5 2022-08-01 22:57:01 +00:00
wh1te909
a8aedfde55 bump version 2022-08-01 22:56:21 +00:00
wh1te909
b174a89032 Release 0.14.4 2022-08-01 18:09:18 +00:00
wh1te909
9b92d1b673 bump version 2022-08-01 17:50:33 +00:00
wh1te909
febc9aed11 feat: run as user amidaware/tacticalrmm-web@137a5648ce amidaware/rmmagent@50cebb950d 2022-07-31 22:23:19 +00:00
wh1te909
de2462677e fix working dir 2022-07-31 21:31:58 +00:00
wh1te909
8bd94d46eb fix empty statement 2022-07-28 17:28:49 +00:00
wh1te909
d43cefe28f add file associations for yaml [skip ci] 2022-07-27 07:31:54 +00:00
wh1te909
b82874e261 back to develop 2022-07-27 07:30:53 +00:00
wh1te909
8554cb5d6c Release 0.14.3 2022-07-27 07:18:55 +00:00
wh1te909
f901614056 bump version 2022-07-27 06:11:41 +00:00
wh1te909
b555d217ab remove check 2022-07-27 06:10:33 +00:00
wh1te909
775c600234 docker nginx changes 2022-07-27 04:19:17 +00:00
wh1te909
128f2570b8 catch exception if mesh is down 2022-07-27 02:01:29 +00:00
wh1te909
3cd53e79b4 add signing key 2022-07-27 01:59:55 +00:00
wh1te909
ebba84ffda switch to official nginx repo to get latest version 2022-07-26 08:09:49 +00:00
wh1te909
1e1a42fe98 update web ver 2022-07-26 08:08:43 +00:00
wh1te909
8a744a440d update reqs 2022-07-26 07:47:21 +00:00
wh1te909
f4fc3c7d55 unused var 2022-07-26 04:39:11 +00:00
wh1te909
0594d121de add agent ver to status closes #1224 2022-07-24 01:18:54 +00:00
wh1te909
12c85d6234 start ansible role to deploy dev environment 2022-07-20 07:16:47 +00:00
Stavros kois
87d05223af apply json fix to docker aswell 2022-07-18 22:08:06 +03:00
Stavros kois
babf6366e8 chore(install script): use same casing as the json schema 2022-07-18 21:13:20 +03:00
wh1te909
5e37728f66 remove devskum 2022-07-18 17:22:24 +00:00
wh1te909
e8e19fede7 don't allow dates in past #1174 2022-07-18 08:04:15 +00:00
wh1te909
e565dbfa66 invalidate cache on policy script change 2022-07-12 20:16:07 +00:00
wh1te909
d180d6820c back to develop 2022-07-10 03:35:15 +00:00
wh1te909
7f252e9b7c Release 0.14.2 2022-07-10 03:34:02 +00:00
wh1te909
41db8681f8 no sudo 2022-07-10 00:38:09 +00:00
wh1te909
26cd58fd6d bump version 2022-07-10 00:16:32 +00:00
wh1te909
63c7e1aa9d update reqs 2022-07-10 00:16:13 +00:00
wh1te909
d5a6063e5e remove extra space 2022-07-09 09:31:05 +00:00
wh1te909
00affdbdec update supported version 2022-07-09 09:30:58 +00:00
wh1te909
db3f0bbd4f increase nginx open file limit 2022-07-09 08:09:24 +00:00
wh1te909
020a59cb97 remove un-needed expose 2022-07-09 08:08:09 +00:00
wh1te909
ff4fa6402d back to dev 2022-07-08 06:40:55 +00:00
wh1te909
80f7555499 Release 0.14.1 2022-07-08 06:40:15 +00:00
wh1te909
10cc187c5d bump versions 2022-07-08 06:38:20 +00:00
118 changed files with 3330 additions and 822 deletions

View File

@@ -1,11 +1,11 @@
# pulls community scripts from git repo # pulls community scripts from git repo
FROM python:3.10-slim AS GET_SCRIPTS_STAGE FROM python:3.10.8-slim AS GET_SCRIPTS_STAGE
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y --no-install-recommends git && \ apt-get install -y --no-install-recommends git && \
git clone https://github.com/amidaware/community-scripts.git /community-scripts git clone https://github.com/amidaware/community-scripts.git /community-scripts
FROM python:3.10-slim FROM python:3.10.8-slim
ENV TACTICAL_DIR /opt/tactical ENV TACTICAL_DIR /opt/tactical
ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready

View File

@@ -22,22 +22,6 @@ services:
aliases: aliases:
- tactical-backend - tactical-backend
app-dev:
container_name: trmm-app-dev
image: node:16-alpine
restart: always
command: /bin/sh -c "npm install --cache ~/.npm && npm run serve"
user: 1000:1000
working_dir: /workspace/web
volumes:
- ..:/workspace:cached
ports:
- "8080:${APP_PORT}"
networks:
dev:
aliases:
- tactical-frontend
# nats # nats
nats-dev: nats-dev:
container_name: trmm-nats-dev container_name: trmm-nats-dev

View File

@@ -15,10 +15,7 @@ set -e
: "${MESH_PASS:=meshcentralpass}" : "${MESH_PASS:=meshcentralpass}"
: "${MESH_HOST:=tactical-meshcentral}" : "${MESH_HOST:=tactical-meshcentral}"
: "${API_HOST:=tactical-backend}" : "${API_HOST:=tactical-backend}"
: "${APP_HOST:=tactical-frontend}"
: "${REDIS_HOST:=tactical-redis}" : "${REDIS_HOST:=tactical-redis}"
: "${HTTP_PROTOCOL:=http}"
: "${APP_PORT:=8080}"
: "${API_PORT:=8000}" : "${API_PORT:=8000}"
: "${CERT_PRIV_PATH:=${TACTICAL_DIR}/certs/privkey.pem}" : "${CERT_PRIV_PATH:=${TACTICAL_DIR}/certs/privkey.pem}"
@@ -142,16 +139,6 @@ if [ "$1" = 'tactical-init-dev' ]; then
django_setup django_setup
# create .env file for frontend
webenv="$(cat << EOF
PROD_URL = "${HTTP_PROTOCOL}://${API_HOST}"
DEV_URL = "${HTTP_PROTOCOL}://${API_HOST}"
DEV_PORT = ${APP_PORT}
DOCKER_BUILD = 1
EOF
)"
echo "${webenv}" | tee "${WORKSPACE_DIR}"/web/.env > /dev/null
# chown everything to tactical user # chown everything to tactical user
chown -R "${TACTICAL_USER}":"${TACTICAL_USER}" "${WORKSPACE_DIR}" chown -R "${TACTICAL_USER}":"${TACTICAL_USER}" "${WORKSPACE_DIR}"
chown -R "${TACTICAL_USER}":"${TACTICAL_USER}" "${TACTICAL_DIR}" chown -R "${TACTICAL_USER}":"${TACTICAL_USER}" "${TACTICAL_DIR}"

View File

@@ -1,41 +1,3 @@
# To ensure app dependencies are ported from your virtual environment/host machine into your container, run 'pip freeze > requirements.txt' in the terminal to overwrite this file -r /workspace/api/tacticalrmm/requirements.txt
asgiref==3.5.0 -r /workspace/api/tacticalrmm/requirements-dev.txt
celery==5.2.6 -r /workspace/api/tacticalrmm/requirements-test.txt
channels==3.0.4
channels_redis==3.4.0
daphne==3.0.2
Django==4.0.4
django-cors-headers==3.11.0
django-ipware==4.0.2
django-rest-knox==4.2.0
djangorestframework==3.13.1
future==0.18.2
msgpack==1.0.3
nats-py==2.1.0
packaging==21.3
psycopg2-binary==2.9.3
pycryptodome==3.14.1
pyotp==2.6.0
pytz==2022.1
qrcode==7.3.1
redis==4.2.2
requests==2.27.1
twilio==7.8.1
urllib3==1.26.9
validators==0.18.2
websockets==10.2
drf_spectacular==0.22.0
meshctrl==0.1.15
hiredis==2.0.0
# dev
black==22.3.0
django-extensions==3.1.5
isort==5.10.1
mypy==0.942
types-pytz==2021.3.6
model-bakery==1.5.0
coverage==6.3.2
django-silk==4.3.0
django-stubs==1.10.1
djangorestframework-stubs==1.5.0

4
.github/FUNDING.yml vendored
View File

@@ -1,9 +1,9 @@
# These are supported funding model platforms # These are supported funding model platforms
github: wh1te909 github: amidaware
patreon: # Replace with a single Patreon username patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username open_collective: # Replace with a single Open Collective username
ko_fi: tacticalrmm ko_fi: # tacticalrmm
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username liberapay: # Replace with a single Liberapay username

View File

@@ -14,22 +14,23 @@ jobs:
name: Tests name: Tests
strategy: strategy:
matrix: matrix:
python-version: ['3.10.4'] python-version: ["3.10.8"]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: harmon758/postgresql-action@v1 - uses: harmon758/postgresql-action@v1
with: with:
postgresql version: '14' postgresql version: "14"
postgresql db: 'pipeline' postgresql db: "pipeline"
postgresql user: 'pipeline' postgresql user: "pipeline"
postgresql password: 'pipeline123456' postgresql password: "pipeline123456"
- name: Setup Python ${{ matrix.python-version }} - name: Setup Python ${{ matrix.python-version }}
uses: actions/setup-python@v3 uses: actions/setup-python@v4
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true
- name: Install redis - name: Install redis
run: | run: |
@@ -49,7 +50,7 @@ jobs:
pip install -r requirements.txt -r requirements-test.txt pip install -r requirements.txt -r requirements-test.txt
- name: Codestyle black - name: Codestyle black
working-directory: api/tacticalrmm working-directory: api
run: | run: |
black --exclude migrations/ --check tacticalrmm black --exclude migrations/ --check tacticalrmm
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then

View File

@@ -1,34 +0,0 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
name: DevSkim
on:
push:
branches: [ develop ]
pull_request:
branches: [ develop ]
schedule:
- cron: '19 5 * * 0'
jobs:
lint:
name: DevSkim
runs-on: ubuntu-20.04
permissions:
actions: read
contents: read
security-events: write
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Run DevSkim scanner
uses: microsoft/DevSkim-Action@v1
- name: Upload DevSkim scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v1
with:
sarif_file: devskim-results.sarif

1
.gitignore vendored
View File

@@ -55,3 +55,4 @@ coverage.lcov
daphne.sock.lock daphne.sock.lock
.pytest_cache .pytest_cache
coverage.xml coverage.xml
setup_dev.yml

15
.vscode/settings.json vendored
View File

@@ -1,7 +1,10 @@
{ {
"python.defaultInterpreterPath": "api/tacticalrmm/env/bin/python", "python.defaultInterpreterPath": "api/env/bin/python",
"python.languageServer": "Pylance", "python.languageServer": "Pylance",
"python.analysis.extraPaths": ["api/tacticalrmm", "api/env"], "python.analysis.extraPaths": [
"api/tacticalrmm",
"api/env"
],
"python.analysis.diagnosticSeverityOverrides": { "python.analysis.diagnosticSeverityOverrides": {
"reportUnusedImport": "error", "reportUnusedImport": "error",
"reportDuplicateImport": "error", "reportDuplicateImport": "error",
@@ -22,11 +25,17 @@
"**env/**" "**env/**"
], ],
"python.formatting.provider": "black", "python.formatting.provider": "black",
"mypy.targets": ["api/tacticalrmm"], "mypy.targets": [
"api/tacticalrmm"
],
"mypy.runUsingActiveInterpreter": true, "mypy.runUsingActiveInterpreter": true,
"editor.bracketPairColorization.enabled": true, "editor.bracketPairColorization.enabled": true,
"editor.guides.bracketPairs": true, "editor.guides.bracketPairs": true,
"editor.formatOnSave": true, "editor.formatOnSave": true,
"files.associations": {
"**/ansible/**/*.yml": "ansible",
"**/docker/**/docker-compose*.yml": "dockercompose"
},
"files.watcherExclude": { "files.watcherExclude": {
"files.watcherExclude": { "files.watcherExclude": {
"**/.git/objects/**": true, "**/.git/objects/**": true,

View File

@@ -35,6 +35,9 @@ Demo database resets every hour. A lot of features are disabled for obvious reas
## Linux agent versions supported ## Linux agent versions supported
- Any distro with systemd which includes but is not limited to: Debian (10, 11), Ubuntu x86_64 (18.04, 20.04, 22.04), Synology 7, centos, freepbx and more! - Any distro with systemd which includes but is not limited to: Debian (10, 11), Ubuntu x86_64 (18.04, 20.04, 22.04), Synology 7, centos, freepbx and more!
## Mac agent versions supported
- 64 bit Intel and Apple Silicon (M1, M2)
## Installation / Backup / Restore / Usage ## Installation / Backup / Restore / Usage
### Refer to the [documentation](https://docs.tacticalrmm.com) ### Refer to the [documentation](https://docs.tacticalrmm.com)

View File

@@ -2,10 +2,7 @@
## Supported Versions ## Supported Versions
| Version | Supported | [Latest](https://github.com/amidaware/tacticalrmm/releases/latest) release
| ------- | ------------------ |
| 0.12.2 | :white_check_mark: |
| < 0.12.2 | :x: |
## Reporting a Vulnerability ## Reporting a Vulnerability

3
ansible/README.md Normal file
View File

@@ -0,0 +1,3 @@
### tacticalrmm ansible WIP
ansible role to setup a Debian 11 VM for tacticalrmm local development

View File

@@ -0,0 +1,40 @@
---
user: "tactical"
python_ver: "3.10.8"
go_ver: "1.18.5"
backend_repo: "https://github.com/amidaware/tacticalrmm.git"
frontend_repo: "https://github.com/amidaware/tacticalrmm-web.git"
scripts_repo: "https://github.com/amidaware/community-scripts.git"
backend_dir: "/opt/trmm"
frontend_dir: "/opt/trmm-web"
scripts_dir: "/opt/trmm-community-scripts"
trmm_dir: "{{ backend_dir }}/api/tacticalrmm/tacticalrmm"
mesh_dir: "/opt/meshcentral"
settings_file: "{{ trmm_dir }}/settings.py"
local_settings_file: "{{ trmm_dir }}/local_settings.py"
fullchain_dest: /etc/ssl/certs/fullchain.pem
privkey_dest: /etc/ssl/certs/privkey.pem
base_pkgs:
- build-essential
- curl
- wget
- dirmngr
- gnupg
- openssl
- gcc
- g++
- make
- ca-certificates
- git
python_pkgs:
- zlib1g-dev
- libncurses5-dev
- libgdbm-dev
- libnss3-dev
- libssl-dev
- libreadline-dev
- libffi-dev
- libsqlite3-dev
- libbz2-dev

View File

@@ -0,0 +1,31 @@
worker_rlimit_nofile 1000000;
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 4096;
}
http {
sendfile on;
server_tokens off;
tcp_nopush on;
types_hash_max_size 2048;
server_names_hash_bucket_size 64;
include /etc/nginx/mime.types;
default_type application/octet-stream;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers EECDH+AESGCM:EDH+AESGCM;
ssl_ecdh_curve secp384r1;
ssl_stapling on;
ssl_stapling_verify on;
add_header X-Content-Type-Options nosniff;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
gzip on;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}

View File

@@ -0,0 +1,2 @@
deb https://nginx.org/packages/debian/ bullseye nginx
deb-src https://nginx.org/packages/debian/ bullseye nginx

View File

@@ -0,0 +1,20 @@
" This file loads the default vim options at the beginning and prevents
" that they are being loaded again later. All other options that will be set,
" are added, or overwrite the default settings. Add as many options as you
" whish at the end of this file.
" Load the defaults
source $VIMRUNTIME/defaults.vim
" Prevent the defaults from being loaded again later, if the user doesn't
" have a local vimrc (~/.vimrc)
let skip_defaults_vim = 1
" Set more options (overwrites settings from /usr/share/vim/vim80/defaults.vim)
" Add as many options as you whish
" Set the mouse mode to 'r'
if has('mouse')
set mouse=r
endif

View File

@@ -0,0 +1,633 @@
---
- name: set mouse mode for vim
tags: vim
become: yes
ansible.builtin.copy:
src: vimrc.local
dest: /etc/vim/vimrc.local
owner: "root"
group: "root"
mode: "0644"
- name: set max_user_watches
tags: sysctl
become: yes
ansible.builtin.lineinfile:
path: /etc/sysctl.conf
line: fs.inotify.max_user_watches=524288
- name: reload sysctl
tags: sysctl
become: yes
ansible.builtin.command:
cmd: sysctl -p
- name: install base packages
tags: base
become: yes
ansible.builtin.apt:
pkg: "{{ item }}"
state: present
update_cache: yes
with_items:
- "{{ base_pkgs }}"
- name: download and install golang
tags: golang
become: yes
ansible.builtin.unarchive:
src: "https://go.dev/dl/go{{ go_ver }}.linux-amd64.tar.gz"
dest: /usr/local
remote_src: yes
- name: add golang to path
become: yes
tags: golang
ansible.builtin.copy:
dest: /etc/profile.d/golang.sh
content: "PATH=$PATH:/usr/local/go/bin"
- name: install python prereqs
tags: python
become: yes
ansible.builtin.apt:
pkg: "{{ item }}"
state: present
with_items:
- "{{ python_pkgs }}"
- name: get cpu core count
tags: python
ansible.builtin.command: nproc
register: numprocs
- name: Create python tmpdir
tags: python
ansible.builtin.tempfile:
state: directory
suffix: python
register: python_tmp
- name: download and extract python
tags: python
ansible.builtin.unarchive:
src: "https://www.python.org/ftp/python/{{ python_ver }}/Python-{{ python_ver }}.tgz"
dest: "{{ python_tmp.path }}"
remote_src: yes
- name: compile python
tags: python
ansible.builtin.shell:
chdir: "{{ python_tmp.path }}/Python-{{ python_ver }}"
cmd: |
./configure --enable-optimizations
make -j {{ numprocs.stdout }}
- name: alt install python
tags: python
become: yes
ansible.builtin.shell:
chdir: "{{ python_tmp.path }}/Python-{{ python_ver }}"
cmd: |
make altinstall
- name: install redis
tags: redis
become: yes
ansible.builtin.apt:
pkg: redis
state: present
- name: create postgres repo
tags: postgres
become: yes
ansible.builtin.copy:
content: "deb http://apt.postgresql.org/pub/repos/apt bullseye-pgdg main"
dest: /etc/apt/sources.list.d/pgdg.list
owner: root
group: root
mode: "0644"
- name: import postgres repo signing key
tags: postgres
become: yes
ansible.builtin.apt_key:
url: https://www.postgresql.org/media/keys/ACCC4CF8.asc
state: present
- name: install postgresql
tags: postgres
become: yes
ansible.builtin.apt:
pkg: postgresql-14
state: present
update_cache: yes
- name: ensure postgres enabled and started
tags: postgres
become: yes
ansible.builtin.service:
name: postgresql
enabled: yes
state: started
- name: setup database
tags: postgres
become: yes
become_user: postgres
ansible.builtin.shell:
cmd: |
psql -c "CREATE DATABASE tacticalrmm"
psql -c "CREATE USER {{ db_user }} WITH PASSWORD '{{ db_passwd }}'"
psql -c "ALTER ROLE {{ db_user }} SET client_encoding TO 'utf8'"
psql -c "ALTER ROLE {{ db_user }} SET default_transaction_isolation TO 'read committed'"
psql -c "ALTER ROLE {{ db_user }} SET timezone TO 'UTC'"
psql -c "ALTER ROLE {{ db_user }} CREATEDB"
psql -c "GRANT ALL PRIVILEGES ON DATABASE tacticalrmm TO {{ db_user }}"
- name: create repo dirs
become: yes
tags: git
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ user }}"
group: "{{ user }}"
mode: "0755"
with_items:
- "{{ backend_dir }}"
- "{{ frontend_dir }}"
- "{{ scripts_dir }}"
- name: git clone repos
tags: git
ansible.builtin.git:
repo: "{{ item.repo }}"
dest: "{{ item.dest }}"
version: "{{ item.version }}"
with_items:
- {
repo: "{{ backend_repo }}",
dest: "{{ backend_dir }}",
version: develop,
}
- {
repo: "{{ frontend_repo }}",
dest: "{{ frontend_dir }}",
version: develop,
}
- { repo: "{{ scripts_repo }}", dest: "{{ scripts_dir }}", version: main }
- name: get nats_server_ver
tags: nats
ansible.builtin.shell: grep "^NATS_SERVER_VER" {{ settings_file }} | awk -F'[= "]' '{print $5}'
register: nats_server_ver
- name: Create nats tmpdir
tags: nats
ansible.builtin.tempfile:
state: directory
suffix: nats
register: nats_tmp
- name: download and extract nats
tags: nats
ansible.builtin.unarchive:
src: "https://github.com/nats-io/nats-server/releases/download/v{{ nats_server_ver.stdout }}/nats-server-v{{ nats_server_ver.stdout }}-linux-amd64.tar.gz"
dest: "{{ nats_tmp.path }}"
remote_src: yes
- name: install nats
tags: nats
become: yes
ansible.builtin.copy:
remote_src: yes
src: "{{ nats_tmp.path }}/nats-server-v{{ nats_server_ver.stdout }}-linux-amd64/nats-server"
dest: /usr/local/bin/nats-server
owner: "{{ user }}"
group: "{{ user }}"
mode: "0755"
- name: Create nodejs tmpdir
tags: nodejs
ansible.builtin.tempfile:
state: directory
suffix: nodejs
register: nodejs_tmp
- name: download nodejs setup
tags: nodejs
ansible.builtin.get_url:
url: https://deb.nodesource.com/setup_16.x
dest: "{{ nodejs_tmp.path }}/setup_node.sh"
mode: "0755"
- name: run node setup script
tags: nodejs
become: yes
ansible.builtin.command:
cmd: "{{ nodejs_tmp.path }}/setup_node.sh"
- name: install nodejs
tags: nodejs
become: yes
ansible.builtin.apt:
pkg: nodejs
state: present
update_cache: yes
- name: update npm
tags: nodejs
become: yes
ansible.builtin.shell:
cmd: npm install -g npm
- name: install quasar cli
tags: quasar
become: yes
ansible.builtin.shell:
cmd: npm install -g @quasar/cli
- name: install frontend
tags: quasar
ansible.builtin.shell:
chdir: "{{ frontend_dir }}"
cmd: npm install
- name: add quasar env
tags: quasar
ansible.builtin.template:
src: quasar.env.j2
dest: "{{ frontend_dir }}/.env"
owner: "{{ user }}"
group: "{{ user }}"
mode: "0644"
- name: remove tempdirs
tags: cleanup
become: yes
ignore_errors: yes
ansible.builtin.file:
path: "{{ item }}"
state: absent
with_items:
- "{{ nats_tmp.path }}"
- "{{ python_tmp.path }}"
- "{{ nodejs_tmp.path }}"
- name: deploy fullchain
tags: certs
become: yes
ansible.builtin.copy:
src: "{{ fullchain_src }}"
dest: "{{ fullchain_dest }}"
owner: "{{ user }}"
group: "{{ user }}"
mode: "0440"
- name: deploy privkey
tags: certs
become: yes
ansible.builtin.copy:
src: "{{ privkey_src }}"
dest: "{{ privkey_dest }}"
owner: "{{ user }}"
group: "{{ user }}"
mode: "0440"
- name: import nginx signing key
tags: nginx
become: yes
ansible.builtin.apt_key:
url: https://nginx.org/packages/keys/nginx_signing.key
state: present
- name: add nginx repo
tags: nginx
become: yes
ansible.builtin.copy:
src: nginx.repo
dest: /etc/apt/sources.list.d/nginx.list
owner: "root"
group: "root"
mode: "0644"
- name: install nginx
tags: nginx
become: yes
ansible.builtin.apt:
pkg: nginx
state: present
update_cache: yes
- name: set nginx default conf
tags: nginx
become: yes
ansible.builtin.copy:
src: nginx-default.conf
dest: /etc/nginx/nginx.conf
owner: "root"
group: "root"
mode: "0644"
- name: create nginx dirs
become: yes
tags: nginx
ansible.builtin.file:
state: directory
path: "{{ item }}"
mode: "0755"
with_items:
- /etc/nginx/sites-available
- /etc/nginx/sites-enabled
- name: deploy nginx sites
become: yes
tags: nginx
ansible.builtin.template:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
mode: "0644"
owner: root
group: root
with_items:
- { src: backend.nginx.j2, dest: /etc/nginx/sites-available/backend.conf }
- { src: mesh.nginx.j2, dest: /etc/nginx/sites-available/mesh.conf }
- name: enable nginx sites
become: yes
tags: nginx
ansible.builtin.file:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
mode: "0644"
owner: root
group: root
state: link
with_items:
- {
src: /etc/nginx/sites-available/backend.conf,
dest: /etc/nginx/sites-enabled/backend.conf,
}
- {
src: /etc/nginx/sites-available/mesh.conf,
dest: /etc/nginx/sites-enabled/mesh.conf,
}
- name: ensure nginx enabled and restarted
tags: nginx
become: yes
ansible.builtin.service:
name: nginx
enabled: yes
state: restarted
- name: copy nats-api bin
tags: nats-api
become: yes
ansible.builtin.copy:
remote_src: yes
src: "{{ backend_dir }}/natsapi/bin/nats-api"
dest: /usr/local/bin/nats-api
owner: "{{ user }}"
group: "{{ user }}"
mode: "0755"
- name: get setuptools_ver
tags: pip
ansible.builtin.shell: grep "^SETUPTOOLS_VER" {{ settings_file }} | awk -F'[= "]' '{print $5}'
register: setuptools_ver
- name: get wheel_ver
tags: pip
ansible.builtin.shell: grep "^WHEEL_VER" {{ settings_file }} | awk -F'[= "]' '{print $5}'
register: wheel_ver
- name: setup virtual env
tags: pip
ansible.builtin.shell:
chdir: "{{ backend_dir }}/api"
cmd: python3.10 -m venv env
- name: update pip to latest
tags: pip
ansible.builtin.pip:
virtualenv: "{{ backend_dir }}/api/env"
name: pip
state: latest
- name: install setuptools and wheel
tags: pip
ansible.builtin.pip:
virtualenv: "{{ backend_dir }}/api/env"
name: "{{ item }}"
with_items:
- "setuptools=={{ setuptools_ver.stdout }}"
- "wheel=={{ wheel_ver.stdout }}"
- name: install python packages
tags: pip
ansible.builtin.pip:
virtualenv: "{{ backend_dir }}/api/env"
chdir: "{{ backend_dir }}/api/tacticalrmm"
requirements: "{{ item }}"
with_items:
- requirements.txt
- requirements-dev.txt
- requirements-test.txt
- name: deploy django local settings
tags: django
ansible.builtin.template:
src: local_settings.j2
dest: "{{ local_settings_file }}"
mode: "0644"
owner: "{{ user }}"
group: "{{ user }}"
- name: setup django
tags: django
ansible.builtin.shell:
chdir: "{{ backend_dir }}/api/tacticalrmm"
cmd: |
. ../env/bin/activate
python manage.py migrate --no-input
python manage.py collectstatic --no-input
python manage.py create_natsapi_conf
python manage.py load_chocos
python manage.py load_community_scripts
echo "from accounts.models import User; User.objects.create_superuser('{{ django_user }}', '{{ github_email }}', '{{ django_password }}') if not User.objects.filter(username='{{ django_user }}').exists() else 0;" | python manage.py shell
python manage.py create_installer_user
- name: deploy services
tags: services
become: yes
ansible.builtin.template:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
mode: "0644"
owner: "root"
group: "root"
with_items:
- { src: nats-api.systemd.j2, dest: /etc/systemd/system/nats-api.service }
- { src: nats-server.systemd.j2, dest: /etc/systemd/system/nats.service }
- { src: mesh.systemd.j2, dest: /etc/systemd/system/meshcentral.service }
- name: import mongodb repo signing key
tags: mongo
become: yes
ansible.builtin.apt_key:
url: https://www.mongodb.org/static/pgp/server-4.4.asc
state: present
- name: setup mongodb repo
tags: mongo
become: yes
ansible.builtin.copy:
content: "deb https://repo.mongodb.org/apt/debian buster/mongodb-org/4.4 main"
dest: /etc/apt/sources.list.d/mongodb-org-4.4.list
owner: root
group: root
mode: "0644"
- name: install mongodb
tags: mongo
become: yes
ansible.builtin.apt:
pkg: mongodb-org
state: present
update_cache: yes
- name: ensure mongodb enabled and started
tags: mongo
become: yes
ansible.builtin.service:
name: mongod
enabled: yes
state: started
- name: get mesh_ver
tags: mesh
ansible.builtin.shell: grep "^MESH_VER" {{ settings_file }} | awk -F'[= "]' '{print $5}'
register: mesh_ver
- name: create meshcentral data directory
tags: mesh
become: yes
ansible.builtin.file:
path: "{{ mesh_dir }}/meshcentral-data"
state: directory
owner: "{{ user }}"
group: "{{ user }}"
mode: "0755"
- name: install meshcentral
tags: mesh
ansible.builtin.command:
chdir: "{{ mesh_dir }}"
cmd: "npm install meshcentral@{{ mesh_ver.stdout }}"
- name: deploy mesh config
tags: mesh
ansible.builtin.template:
src: mesh.cfg.j2
dest: "{{ mesh_dir }}/meshcentral-data/config.json"
mode: "0644"
owner: "{{ user }}"
group: "{{ user }}"
- name: start meshcentral
tags: mesh
become: yes
ansible.builtin.systemd:
name: meshcentral.service
state: started
enabled: yes
daemon_reload: yes
- name: wait for meshcentral to be ready
tags: mesh
uri:
url: "https://{{ mesh }}"
return_content: yes
validate_certs: yes
status_code: 200
register: mesh_status
until: mesh_status.status == 200
retries: 20
delay: 3
- name: get meshcentral login token key
tags: mesh_key
ansible.builtin.command:
chdir: "{{ mesh_dir }}"
cmd: node node_modules/meshcentral --logintokenkey
register: mesh_token_key
- name: add mesh key to django settings file
tags: mesh_key
ansible.builtin.lineinfile:
path: "{{ local_settings_file }}"
line: 'MESH_TOKEN_KEY = "{{ mesh_token_key.stdout }}"'
- name: stop meshcentral service
tags: mesh_user
become: yes
ansible.builtin.service:
name: meshcentral.service
state: stopped
- name: create mesh user
tags: mesh_user
ansible.builtin.shell:
chdir: "{{ mesh_dir }}"
cmd: |
node node_modules/meshcentral --createaccount {{ mesh_user }} --pass {{ mesh_password }} --email {{ github_email }}
node node_modules/meshcentral --adminaccount {{ mesh_user }}
- name: start meshcentral service
tags: mesh_user
become: yes
ansible.builtin.service:
name: meshcentral.service
state: started
- name: wait for meshcentral to be ready
tags: mesh_user
uri:
url: "https://{{ mesh }}"
return_content: yes
validate_certs: yes
status_code: 200
register: mesh_status
until: mesh_status.status == 200
retries: 20
delay: 3
- name: create mesh device group
tags: mesh_user
ansible.builtin.shell:
chdir: "{{ mesh_dir }}"
cmd: |
node node_modules/meshcentral/meshctrl.js --url wss://{{ mesh }}:443 --loginuser {{ mesh_user }} --loginpass {{ mesh_password }} AddDeviceGroup --name TacticalRMM
- name: finish up django
tags: mesh_user
ansible.builtin.shell:
chdir: "{{ backend_dir }}/api/tacticalrmm"
cmd: |
. ../env/bin/activate
python manage.py initial_db_setup
python manage.py reload_nats
- name: restart services
tags: services
become: yes
ansible.builtin.systemd:
daemon_reload: yes
enabled: yes
state: restarted
name: "{{ item }}.service"
with_items:
- nats
- nats-api

View File

@@ -0,0 +1,20 @@
server {
listen 443 ssl reuseport;
listen [::]:443 ssl;
server_name {{ api }};
client_max_body_size 300M;
ssl_certificate {{ fullchain_dest }};
ssl_certificate_key {{ privkey_dest }};
location ~ ^/natsws {
proxy_pass http://127.0.0.1:9235;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-Host $host:$server_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

View File

@@ -0,0 +1,25 @@
SECRET_KEY = "{{ django_secret }}"
DEBUG = True
ALLOWED_HOSTS = ['{{ api }}']
ADMIN_URL = "admin/"
CORS_ORIGIN_WHITELIST = [
"http://{{ rmm }}:8080",
"https://{{ rmm }}:8080",
]
CORS_ORIGIN_ALLOW_ALL = True
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'tacticalrmm',
'USER': '{{ db_user }}',
'PASSWORD': '{{ db_passwd }}',
'HOST': 'localhost',
'PORT': '5432',
}
}
REDIS_HOST = "localhost"
ADMIN_ENABLED = True
CERT_FILE = "{{ fullchain_src }}"
KEY_FILE = "{{ privkey_src }}"
MESH_USERNAME = "{{ mesh_user }}"
MESH_SITE = "https://{{ mesh }}"

View File

@@ -0,0 +1,33 @@
{
"settings": {
"Cert": "{{ mesh }}",
"MongoDb": "mongodb://127.0.0.1:27017",
"MongoDbName": "meshcentral",
"WANonly": true,
"Minify": 1,
"Port": 4430,
"AliasPort": 443,
"RedirPort": 800,
"AllowLoginToken": true,
"AllowFraming": true,
"AgentPong": 300,
"AllowHighQualityDesktop": true,
"TlsOffload": "127.0.0.1",
"agentCoreDump": false,
"Compression": true,
"WsCompression": true,
"AgentWsCompression": true,
"MaxInvalidLogin": { "time": 5, "count": 5, "coolofftime": 30 }
},
"domains": {
"": {
"Title": "Tactical RMM",
"Title2": "Tactical RMM",
"NewAccounts": false,
"CertUrl": "https://{{ mesh }}:443/",
"GeoLocation": true,
"CookieIpCheck": false,
"mstsc": true
}
}
}

View File

@@ -0,0 +1,22 @@
server {
listen 443 ssl;
listen [::]:443 ssl;
proxy_send_timeout 330s;
proxy_read_timeout 330s;
server_name {{ mesh }};
ssl_certificate {{ fullchain_dest }};
ssl_certificate_key {{ privkey_dest }};
ssl_session_cache shared:WEBSSL:10m;
location / {
proxy_pass http://127.0.0.1:4430/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-Host $host:$server_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

View File

@@ -0,0 +1,17 @@
[Unit]
Description=MeshCentral Server
After=network.target mongod.service nginx.service
[Service]
Type=simple
LimitNOFILE=1000000
ExecStart=/usr/bin/node node_modules/meshcentral
Environment=NODE_ENV=production
WorkingDirectory={{ mesh_dir }}
User={{ user }}
Group={{ user }}
Restart=always
RestartSec=10s
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,14 @@
[Unit]
Description=TacticalRMM Nats Api
After=nats.service
[Service]
Type=simple
ExecStart=/usr/local/bin/nats-api -config {{ backend_dir }}/api/tacticalrmm/nats-api.conf
User={{ user }}
Group={{ user }}
Restart=always
RestartSec=5s
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,18 @@
[Unit]
Description=NATS Server
After=network.target
[Service]
PrivateTmp=true
Type=simple
ExecStart=/usr/local/bin/nats-server -c {{ backend_dir }}/api/tacticalrmm/nats-rmm.conf
ExecReload=/usr/bin/kill -s HUP $MAINPID
ExecStop=/usr/bin/kill -s SIGINT $MAINPID
User={{ user }}
Group={{ user }}
Restart=always
RestartSec=5s
LimitNOFILE=1000000
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,4 @@
DEV_URL = "http://{{ api }}:8000"
DEV_HOST = "{{ rmm }}"
DEV_PORT = "8080"
USE_HTTPS = false

View File

@@ -0,0 +1,20 @@
---
- hosts: "{{ target }}"
vars:
ansible_user: tactical
fullchain_src: /path/to/fullchain.pem
privkey_src: /path/to/privkey.pem
api: "api.example.com"
rmm: "rmm.example.com"
mesh: "mesh.example.com"
github_username: "changeme"
github_email: "changeme@example.com"
mesh_user: "changeme"
mesh_password: "changeme"
db_user: "changeme"
db_passwd: "changeme"
django_secret: "changeme"
django_user: "changeme"
django_password: "changeme"
roles:
- trmm_dev

View File

@@ -1,5 +1,6 @@
import os import os
import subprocess import subprocess
from contextlib import suppress
import pyotp import pyotp
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
@@ -25,7 +26,7 @@ class Command(BaseCommand):
nginx = "/etc/nginx/sites-available/frontend.conf" nginx = "/etc/nginx/sites-available/frontend.conf"
found = None found = None
if os.path.exists(nginx): if os.path.exists(nginx):
try: with suppress(Exception):
with open(nginx, "r") as f: with open(nginx, "r") as f:
for line in f: for line in f:
if "server_name" in line: if "server_name" in line:
@@ -35,8 +36,6 @@ class Command(BaseCommand):
if found: if found:
rep = found.replace("server_name", "").replace(";", "") rep = found.replace("server_name", "").replace(";", "")
domain = "".join(rep.split()) domain = "".join(rep.split())
except:
pass
code = pyotp.random_base32() code = pyotp.random_base32()
user.totp_key = code user.totp_key = code

View File

@@ -31,7 +31,7 @@ class RolesPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool: def has_permission(self, r, view) -> bool:
if r.method == "GET": if r.method == "GET":
return _has_perm(r, "can_list_roles") return _has_perm(r, "can_list_roles")
else:
return _has_perm(r, "can_manage_roles") return _has_perm(r, "can_manage_roles")

View File

@@ -0,0 +1,18 @@
from typing import TYPE_CHECKING
from django.conf import settings
if TYPE_CHECKING:
from django.http import HttpRequest
from accounts.models import User
def is_root_user(*, request: "HttpRequest", user: "User") -> bool:
root = (
hasattr(settings, "ROOT_USER")
and request.user != user
and user.username == settings.ROOT_USER
)
demo = (
getattr(settings, "DEMO", False) and request.user.username == settings.ROOT_USER
)
return root or demo

View File

@@ -22,18 +22,7 @@ from .serializers import (
UserSerializer, UserSerializer,
UserUISerializer, UserUISerializer,
) )
from accounts.utils import is_root_user
def _is_root_user(request, user) -> bool:
root = (
hasattr(settings, "ROOT_USER")
and request.user != user
and user.username == settings.ROOT_USER
)
demo = (
getattr(settings, "DEMO", False) and request.user.username == settings.ROOT_USER
)
return root or demo
class CheckCreds(KnoxLoginView): class CheckCreds(KnoxLoginView):
@@ -93,7 +82,7 @@ class LoginView(KnoxLoginView):
login(request, user) login(request, user)
# save ip information # save ip information
client_ip, is_routable = get_client_ip(request) client_ip, _ = get_client_ip(request)
user.last_login_ip = client_ip user.last_login_ip = client_ip
user.save() user.save()
@@ -159,7 +148,7 @@ class GetUpdateDeleteUser(APIView):
def put(self, request, pk): def put(self, request, pk):
user = get_object_or_404(User, pk=pk) user = get_object_or_404(User, pk=pk)
if _is_root_user(request, user): if is_root_user(request=request, user=user):
return notify_error("The root user cannot be modified from the UI") return notify_error("The root user cannot be modified from the UI")
serializer = UserSerializer(instance=user, data=request.data, partial=True) serializer = UserSerializer(instance=user, data=request.data, partial=True)
@@ -170,7 +159,7 @@ class GetUpdateDeleteUser(APIView):
def delete(self, request, pk): def delete(self, request, pk):
user = get_object_or_404(User, pk=pk) user = get_object_or_404(User, pk=pk)
if _is_root_user(request, user): if is_root_user(request=request, user=user):
return notify_error("The root user cannot be deleted from the UI") return notify_error("The root user cannot be deleted from the UI")
user.delete() user.delete()
@@ -183,7 +172,7 @@ class UserActions(APIView):
# reset password # reset password
def post(self, request): def post(self, request):
user = get_object_or_404(User, pk=request.data["id"]) user = get_object_or_404(User, pk=request.data["id"])
if _is_root_user(request, user): if is_root_user(request=request, user=user):
return notify_error("The root user cannot be modified from the UI") return notify_error("The root user cannot be modified from the UI")
user.set_password(request.data["password"]) user.set_password(request.data["password"])
@@ -194,7 +183,7 @@ class UserActions(APIView):
# reset two factor token # reset two factor token
def put(self, request): def put(self, request):
user = get_object_or_404(User, pk=request.data["id"]) user = get_object_or_404(User, pk=request.data["id"])
if _is_root_user(request, user): if is_root_user(request=request, user=user):
return notify_error("The root user cannot be modified from the UI") return notify_error("The root user cannot be modified from the UI")
user.totp_key = "" user.totp_key = ""

View File

@@ -64,7 +64,7 @@ class Command(BaseCommand):
try: try:
agent.delete() agent.delete()
except Exception as e: except Exception as e:
err = f"Failed to delete agent {agent.hostname}: {str(e)}" err = f"Failed to delete agent {agent.hostname}: {e}"
self.stdout.write(self.style.ERROR(err)) self.stdout.write(self.style.ERROR(err))
else: else:
deleted_count += 1 deleted_count += 1

View File

@@ -568,6 +568,12 @@ class Command(BaseCommand):
check5_history.y = 1 check5_history.y = 1
else: else:
check5_history.y = 0 check5_history.y = 0
check5_history.results = {
"retcode": 0,
"stdout": None,
"stderr": None,
"execution_time": "4.0000",
}
check5_history.save() check5_history.save()
check6 = Check() check6 = Check()
@@ -595,6 +601,12 @@ class Command(BaseCommand):
check6_history.agent_id = agent.agent_id check6_history.agent_id = agent.agent_id
check6_history.x = django_now - djangotime.timedelta(minutes=i * 2) check6_history.x = django_now - djangotime.timedelta(minutes=i * 2)
check6_history.y = 0 check6_history.y = 0
check6_history.results = {
"retcode": 0,
"stdout": None,
"stderr": None,
"execution_time": "4.0000",
}
check6_history.save() check6_history.save()
nla_task = AutomatedTask() nla_task = AutomatedTask()
@@ -712,6 +724,12 @@ class Command(BaseCommand):
check7_history.agent_id = agent.agent_id check7_history.agent_id = agent.agent_id
check7_history.x = django_now - djangotime.timedelta(minutes=i * 2) check7_history.x = django_now - djangotime.timedelta(minutes=i * 2)
check7_history.y = 0 check7_history.y = 0
check7_history.results = {
"retcode": 0,
"stdout": spooler_stdout,
"stderr": None,
"execution_time": "3.1337",
}
check7_history.save() check7_history.save()
if agent.plat == AgentPlat.WINDOWS: if agent.plat == AgentPlat.WINDOWS:

View File

@@ -0,0 +1,631 @@
# Generated by Django 4.1 on 2022-08-24 07:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("agents", "0054_alter_agent_goarch"),
]
operations = [
migrations.AlterField(
model_name="agent",
name="time_zone",
field=models.CharField(
blank=True,
choices=[
("Africa/Abidjan", "Africa/Abidjan"),
("Africa/Accra", "Africa/Accra"),
("Africa/Addis_Ababa", "Africa/Addis_Ababa"),
("Africa/Algiers", "Africa/Algiers"),
("Africa/Asmara", "Africa/Asmara"),
("Africa/Asmera", "Africa/Asmera"),
("Africa/Bamako", "Africa/Bamako"),
("Africa/Bangui", "Africa/Bangui"),
("Africa/Banjul", "Africa/Banjul"),
("Africa/Bissau", "Africa/Bissau"),
("Africa/Blantyre", "Africa/Blantyre"),
("Africa/Brazzaville", "Africa/Brazzaville"),
("Africa/Bujumbura", "Africa/Bujumbura"),
("Africa/Cairo", "Africa/Cairo"),
("Africa/Casablanca", "Africa/Casablanca"),
("Africa/Ceuta", "Africa/Ceuta"),
("Africa/Conakry", "Africa/Conakry"),
("Africa/Dakar", "Africa/Dakar"),
("Africa/Dar_es_Salaam", "Africa/Dar_es_Salaam"),
("Africa/Djibouti", "Africa/Djibouti"),
("Africa/Douala", "Africa/Douala"),
("Africa/El_Aaiun", "Africa/El_Aaiun"),
("Africa/Freetown", "Africa/Freetown"),
("Africa/Gaborone", "Africa/Gaborone"),
("Africa/Harare", "Africa/Harare"),
("Africa/Johannesburg", "Africa/Johannesburg"),
("Africa/Juba", "Africa/Juba"),
("Africa/Kampala", "Africa/Kampala"),
("Africa/Khartoum", "Africa/Khartoum"),
("Africa/Kigali", "Africa/Kigali"),
("Africa/Kinshasa", "Africa/Kinshasa"),
("Africa/Lagos", "Africa/Lagos"),
("Africa/Libreville", "Africa/Libreville"),
("Africa/Lome", "Africa/Lome"),
("Africa/Luanda", "Africa/Luanda"),
("Africa/Lubumbashi", "Africa/Lubumbashi"),
("Africa/Lusaka", "Africa/Lusaka"),
("Africa/Malabo", "Africa/Malabo"),
("Africa/Maputo", "Africa/Maputo"),
("Africa/Maseru", "Africa/Maseru"),
("Africa/Mbabane", "Africa/Mbabane"),
("Africa/Mogadishu", "Africa/Mogadishu"),
("Africa/Monrovia", "Africa/Monrovia"),
("Africa/Nairobi", "Africa/Nairobi"),
("Africa/Ndjamena", "Africa/Ndjamena"),
("Africa/Niamey", "Africa/Niamey"),
("Africa/Nouakchott", "Africa/Nouakchott"),
("Africa/Ouagadougou", "Africa/Ouagadougou"),
("Africa/Porto-Novo", "Africa/Porto-Novo"),
("Africa/Sao_Tome", "Africa/Sao_Tome"),
("Africa/Timbuktu", "Africa/Timbuktu"),
("Africa/Tripoli", "Africa/Tripoli"),
("Africa/Tunis", "Africa/Tunis"),
("Africa/Windhoek", "Africa/Windhoek"),
("America/Adak", "America/Adak"),
("America/Anchorage", "America/Anchorage"),
("America/Anguilla", "America/Anguilla"),
("America/Antigua", "America/Antigua"),
("America/Araguaina", "America/Araguaina"),
(
"America/Argentina/Buenos_Aires",
"America/Argentina/Buenos_Aires",
),
("America/Argentina/Catamarca", "America/Argentina/Catamarca"),
(
"America/Argentina/ComodRivadavia",
"America/Argentina/ComodRivadavia",
),
("America/Argentina/Cordoba", "America/Argentina/Cordoba"),
("America/Argentina/Jujuy", "America/Argentina/Jujuy"),
("America/Argentina/La_Rioja", "America/Argentina/La_Rioja"),
("America/Argentina/Mendoza", "America/Argentina/Mendoza"),
(
"America/Argentina/Rio_Gallegos",
"America/Argentina/Rio_Gallegos",
),
("America/Argentina/Salta", "America/Argentina/Salta"),
("America/Argentina/San_Juan", "America/Argentina/San_Juan"),
("America/Argentina/San_Luis", "America/Argentina/San_Luis"),
("America/Argentina/Tucuman", "America/Argentina/Tucuman"),
("America/Argentina/Ushuaia", "America/Argentina/Ushuaia"),
("America/Aruba", "America/Aruba"),
("America/Asuncion", "America/Asuncion"),
("America/Atikokan", "America/Atikokan"),
("America/Atka", "America/Atka"),
("America/Bahia", "America/Bahia"),
("America/Bahia_Banderas", "America/Bahia_Banderas"),
("America/Barbados", "America/Barbados"),
("America/Belem", "America/Belem"),
("America/Belize", "America/Belize"),
("America/Blanc-Sablon", "America/Blanc-Sablon"),
("America/Boa_Vista", "America/Boa_Vista"),
("America/Bogota", "America/Bogota"),
("America/Boise", "America/Boise"),
("America/Buenos_Aires", "America/Buenos_Aires"),
("America/Cambridge_Bay", "America/Cambridge_Bay"),
("America/Campo_Grande", "America/Campo_Grande"),
("America/Cancun", "America/Cancun"),
("America/Caracas", "America/Caracas"),
("America/Catamarca", "America/Catamarca"),
("America/Cayenne", "America/Cayenne"),
("America/Cayman", "America/Cayman"),
("America/Chicago", "America/Chicago"),
("America/Chihuahua", "America/Chihuahua"),
("America/Coral_Harbour", "America/Coral_Harbour"),
("America/Cordoba", "America/Cordoba"),
("America/Costa_Rica", "America/Costa_Rica"),
("America/Creston", "America/Creston"),
("America/Cuiaba", "America/Cuiaba"),
("America/Curacao", "America/Curacao"),
("America/Danmarkshavn", "America/Danmarkshavn"),
("America/Dawson", "America/Dawson"),
("America/Dawson_Creek", "America/Dawson_Creek"),
("America/Denver", "America/Denver"),
("America/Detroit", "America/Detroit"),
("America/Dominica", "America/Dominica"),
("America/Edmonton", "America/Edmonton"),
("America/Eirunepe", "America/Eirunepe"),
("America/El_Salvador", "America/El_Salvador"),
("America/Ensenada", "America/Ensenada"),
("America/Fort_Nelson", "America/Fort_Nelson"),
("America/Fort_Wayne", "America/Fort_Wayne"),
("America/Fortaleza", "America/Fortaleza"),
("America/Glace_Bay", "America/Glace_Bay"),
("America/Godthab", "America/Godthab"),
("America/Goose_Bay", "America/Goose_Bay"),
("America/Grand_Turk", "America/Grand_Turk"),
("America/Grenada", "America/Grenada"),
("America/Guadeloupe", "America/Guadeloupe"),
("America/Guatemala", "America/Guatemala"),
("America/Guayaquil", "America/Guayaquil"),
("America/Guyana", "America/Guyana"),
("America/Halifax", "America/Halifax"),
("America/Havana", "America/Havana"),
("America/Hermosillo", "America/Hermosillo"),
("America/Indiana/Indianapolis", "America/Indiana/Indianapolis"),
("America/Indiana/Knox", "America/Indiana/Knox"),
("America/Indiana/Marengo", "America/Indiana/Marengo"),
("America/Indiana/Petersburg", "America/Indiana/Petersburg"),
("America/Indiana/Tell_City", "America/Indiana/Tell_City"),
("America/Indiana/Vevay", "America/Indiana/Vevay"),
("America/Indiana/Vincennes", "America/Indiana/Vincennes"),
("America/Indiana/Winamac", "America/Indiana/Winamac"),
("America/Indianapolis", "America/Indianapolis"),
("America/Inuvik", "America/Inuvik"),
("America/Iqaluit", "America/Iqaluit"),
("America/Jamaica", "America/Jamaica"),
("America/Jujuy", "America/Jujuy"),
("America/Juneau", "America/Juneau"),
("America/Kentucky/Louisville", "America/Kentucky/Louisville"),
("America/Kentucky/Monticello", "America/Kentucky/Monticello"),
("America/Knox_IN", "America/Knox_IN"),
("America/Kralendijk", "America/Kralendijk"),
("America/La_Paz", "America/La_Paz"),
("America/Lima", "America/Lima"),
("America/Los_Angeles", "America/Los_Angeles"),
("America/Louisville", "America/Louisville"),
("America/Lower_Princes", "America/Lower_Princes"),
("America/Maceio", "America/Maceio"),
("America/Managua", "America/Managua"),
("America/Manaus", "America/Manaus"),
("America/Marigot", "America/Marigot"),
("America/Martinique", "America/Martinique"),
("America/Matamoros", "America/Matamoros"),
("America/Mazatlan", "America/Mazatlan"),
("America/Mendoza", "America/Mendoza"),
("America/Menominee", "America/Menominee"),
("America/Merida", "America/Merida"),
("America/Metlakatla", "America/Metlakatla"),
("America/Mexico_City", "America/Mexico_City"),
("America/Miquelon", "America/Miquelon"),
("America/Moncton", "America/Moncton"),
("America/Monterrey", "America/Monterrey"),
("America/Montevideo", "America/Montevideo"),
("America/Montreal", "America/Montreal"),
("America/Montserrat", "America/Montserrat"),
("America/Nassau", "America/Nassau"),
("America/New_York", "America/New_York"),
("America/Nipigon", "America/Nipigon"),
("America/Nome", "America/Nome"),
("America/Noronha", "America/Noronha"),
("America/North_Dakota/Beulah", "America/North_Dakota/Beulah"),
("America/North_Dakota/Center", "America/North_Dakota/Center"),
(
"America/North_Dakota/New_Salem",
"America/North_Dakota/New_Salem",
),
("America/Nuuk", "America/Nuuk"),
("America/Ojinaga", "America/Ojinaga"),
("America/Panama", "America/Panama"),
("America/Pangnirtung", "America/Pangnirtung"),
("America/Paramaribo", "America/Paramaribo"),
("America/Phoenix", "America/Phoenix"),
("America/Port-au-Prince", "America/Port-au-Prince"),
("America/Port_of_Spain", "America/Port_of_Spain"),
("America/Porto_Acre", "America/Porto_Acre"),
("America/Porto_Velho", "America/Porto_Velho"),
("America/Puerto_Rico", "America/Puerto_Rico"),
("America/Punta_Arenas", "America/Punta_Arenas"),
("America/Rainy_River", "America/Rainy_River"),
("America/Rankin_Inlet", "America/Rankin_Inlet"),
("America/Recife", "America/Recife"),
("America/Regina", "America/Regina"),
("America/Resolute", "America/Resolute"),
("America/Rio_Branco", "America/Rio_Branco"),
("America/Rosario", "America/Rosario"),
("America/Santa_Isabel", "America/Santa_Isabel"),
("America/Santarem", "America/Santarem"),
("America/Santiago", "America/Santiago"),
("America/Santo_Domingo", "America/Santo_Domingo"),
("America/Sao_Paulo", "America/Sao_Paulo"),
("America/Scoresbysund", "America/Scoresbysund"),
("America/Shiprock", "America/Shiprock"),
("America/Sitka", "America/Sitka"),
("America/St_Barthelemy", "America/St_Barthelemy"),
("America/St_Johns", "America/St_Johns"),
("America/St_Kitts", "America/St_Kitts"),
("America/St_Lucia", "America/St_Lucia"),
("America/St_Thomas", "America/St_Thomas"),
("America/St_Vincent", "America/St_Vincent"),
("America/Swift_Current", "America/Swift_Current"),
("America/Tegucigalpa", "America/Tegucigalpa"),
("America/Thule", "America/Thule"),
("America/Thunder_Bay", "America/Thunder_Bay"),
("America/Tijuana", "America/Tijuana"),
("America/Toronto", "America/Toronto"),
("America/Tortola", "America/Tortola"),
("America/Vancouver", "America/Vancouver"),
("America/Virgin", "America/Virgin"),
("America/Whitehorse", "America/Whitehorse"),
("America/Winnipeg", "America/Winnipeg"),
("America/Yakutat", "America/Yakutat"),
("America/Yellowknife", "America/Yellowknife"),
("Antarctica/Casey", "Antarctica/Casey"),
("Antarctica/Davis", "Antarctica/Davis"),
("Antarctica/DumontDUrville", "Antarctica/DumontDUrville"),
("Antarctica/Macquarie", "Antarctica/Macquarie"),
("Antarctica/Mawson", "Antarctica/Mawson"),
("Antarctica/McMurdo", "Antarctica/McMurdo"),
("Antarctica/Palmer", "Antarctica/Palmer"),
("Antarctica/Rothera", "Antarctica/Rothera"),
("Antarctica/South_Pole", "Antarctica/South_Pole"),
("Antarctica/Syowa", "Antarctica/Syowa"),
("Antarctica/Troll", "Antarctica/Troll"),
("Antarctica/Vostok", "Antarctica/Vostok"),
("Arctic/Longyearbyen", "Arctic/Longyearbyen"),
("Asia/Aden", "Asia/Aden"),
("Asia/Almaty", "Asia/Almaty"),
("Asia/Amman", "Asia/Amman"),
("Asia/Anadyr", "Asia/Anadyr"),
("Asia/Aqtau", "Asia/Aqtau"),
("Asia/Aqtobe", "Asia/Aqtobe"),
("Asia/Ashgabat", "Asia/Ashgabat"),
("Asia/Ashkhabad", "Asia/Ashkhabad"),
("Asia/Atyrau", "Asia/Atyrau"),
("Asia/Baghdad", "Asia/Baghdad"),
("Asia/Bahrain", "Asia/Bahrain"),
("Asia/Baku", "Asia/Baku"),
("Asia/Bangkok", "Asia/Bangkok"),
("Asia/Barnaul", "Asia/Barnaul"),
("Asia/Beirut", "Asia/Beirut"),
("Asia/Bishkek", "Asia/Bishkek"),
("Asia/Brunei", "Asia/Brunei"),
("Asia/Calcutta", "Asia/Calcutta"),
("Asia/Chita", "Asia/Chita"),
("Asia/Choibalsan", "Asia/Choibalsan"),
("Asia/Chongqing", "Asia/Chongqing"),
("Asia/Chungking", "Asia/Chungking"),
("Asia/Colombo", "Asia/Colombo"),
("Asia/Dacca", "Asia/Dacca"),
("Asia/Damascus", "Asia/Damascus"),
("Asia/Dhaka", "Asia/Dhaka"),
("Asia/Dili", "Asia/Dili"),
("Asia/Dubai", "Asia/Dubai"),
("Asia/Dushanbe", "Asia/Dushanbe"),
("Asia/Famagusta", "Asia/Famagusta"),
("Asia/Gaza", "Asia/Gaza"),
("Asia/Harbin", "Asia/Harbin"),
("Asia/Hebron", "Asia/Hebron"),
("Asia/Ho_Chi_Minh", "Asia/Ho_Chi_Minh"),
("Asia/Hong_Kong", "Asia/Hong_Kong"),
("Asia/Hovd", "Asia/Hovd"),
("Asia/Irkutsk", "Asia/Irkutsk"),
("Asia/Istanbul", "Asia/Istanbul"),
("Asia/Jakarta", "Asia/Jakarta"),
("Asia/Jayapura", "Asia/Jayapura"),
("Asia/Jerusalem", "Asia/Jerusalem"),
("Asia/Kabul", "Asia/Kabul"),
("Asia/Kamchatka", "Asia/Kamchatka"),
("Asia/Karachi", "Asia/Karachi"),
("Asia/Kashgar", "Asia/Kashgar"),
("Asia/Kathmandu", "Asia/Kathmandu"),
("Asia/Katmandu", "Asia/Katmandu"),
("Asia/Khandyga", "Asia/Khandyga"),
("Asia/Kolkata", "Asia/Kolkata"),
("Asia/Krasnoyarsk", "Asia/Krasnoyarsk"),
("Asia/Kuala_Lumpur", "Asia/Kuala_Lumpur"),
("Asia/Kuching", "Asia/Kuching"),
("Asia/Kuwait", "Asia/Kuwait"),
("Asia/Macao", "Asia/Macao"),
("Asia/Macau", "Asia/Macau"),
("Asia/Magadan", "Asia/Magadan"),
("Asia/Makassar", "Asia/Makassar"),
("Asia/Manila", "Asia/Manila"),
("Asia/Muscat", "Asia/Muscat"),
("Asia/Nicosia", "Asia/Nicosia"),
("Asia/Novokuznetsk", "Asia/Novokuznetsk"),
("Asia/Novosibirsk", "Asia/Novosibirsk"),
("Asia/Omsk", "Asia/Omsk"),
("Asia/Oral", "Asia/Oral"),
("Asia/Phnom_Penh", "Asia/Phnom_Penh"),
("Asia/Pontianak", "Asia/Pontianak"),
("Asia/Pyongyang", "Asia/Pyongyang"),
("Asia/Qatar", "Asia/Qatar"),
("Asia/Qostanay", "Asia/Qostanay"),
("Asia/Qyzylorda", "Asia/Qyzylorda"),
("Asia/Rangoon", "Asia/Rangoon"),
("Asia/Riyadh", "Asia/Riyadh"),
("Asia/Saigon", "Asia/Saigon"),
("Asia/Sakhalin", "Asia/Sakhalin"),
("Asia/Samarkand", "Asia/Samarkand"),
("Asia/Seoul", "Asia/Seoul"),
("Asia/Shanghai", "Asia/Shanghai"),
("Asia/Singapore", "Asia/Singapore"),
("Asia/Srednekolymsk", "Asia/Srednekolymsk"),
("Asia/Taipei", "Asia/Taipei"),
("Asia/Tashkent", "Asia/Tashkent"),
("Asia/Tbilisi", "Asia/Tbilisi"),
("Asia/Tehran", "Asia/Tehran"),
("Asia/Tel_Aviv", "Asia/Tel_Aviv"),
("Asia/Thimbu", "Asia/Thimbu"),
("Asia/Thimphu", "Asia/Thimphu"),
("Asia/Tokyo", "Asia/Tokyo"),
("Asia/Tomsk", "Asia/Tomsk"),
("Asia/Ujung_Pandang", "Asia/Ujung_Pandang"),
("Asia/Ulaanbaatar", "Asia/Ulaanbaatar"),
("Asia/Ulan_Bator", "Asia/Ulan_Bator"),
("Asia/Urumqi", "Asia/Urumqi"),
("Asia/Ust-Nera", "Asia/Ust-Nera"),
("Asia/Vientiane", "Asia/Vientiane"),
("Asia/Vladivostok", "Asia/Vladivostok"),
("Asia/Yakutsk", "Asia/Yakutsk"),
("Asia/Yangon", "Asia/Yangon"),
("Asia/Yekaterinburg", "Asia/Yekaterinburg"),
("Asia/Yerevan", "Asia/Yerevan"),
("Atlantic/Azores", "Atlantic/Azores"),
("Atlantic/Bermuda", "Atlantic/Bermuda"),
("Atlantic/Canary", "Atlantic/Canary"),
("Atlantic/Cape_Verde", "Atlantic/Cape_Verde"),
("Atlantic/Faeroe", "Atlantic/Faeroe"),
("Atlantic/Faroe", "Atlantic/Faroe"),
("Atlantic/Jan_Mayen", "Atlantic/Jan_Mayen"),
("Atlantic/Madeira", "Atlantic/Madeira"),
("Atlantic/Reykjavik", "Atlantic/Reykjavik"),
("Atlantic/South_Georgia", "Atlantic/South_Georgia"),
("Atlantic/St_Helena", "Atlantic/St_Helena"),
("Atlantic/Stanley", "Atlantic/Stanley"),
("Australia/ACT", "Australia/ACT"),
("Australia/Adelaide", "Australia/Adelaide"),
("Australia/Brisbane", "Australia/Brisbane"),
("Australia/Broken_Hill", "Australia/Broken_Hill"),
("Australia/Canberra", "Australia/Canberra"),
("Australia/Currie", "Australia/Currie"),
("Australia/Darwin", "Australia/Darwin"),
("Australia/Eucla", "Australia/Eucla"),
("Australia/Hobart", "Australia/Hobart"),
("Australia/LHI", "Australia/LHI"),
("Australia/Lindeman", "Australia/Lindeman"),
("Australia/Lord_Howe", "Australia/Lord_Howe"),
("Australia/Melbourne", "Australia/Melbourne"),
("Australia/NSW", "Australia/NSW"),
("Australia/North", "Australia/North"),
("Australia/Perth", "Australia/Perth"),
("Australia/Queensland", "Australia/Queensland"),
("Australia/South", "Australia/South"),
("Australia/Sydney", "Australia/Sydney"),
("Australia/Tasmania", "Australia/Tasmania"),
("Australia/Victoria", "Australia/Victoria"),
("Australia/West", "Australia/West"),
("Australia/Yancowinna", "Australia/Yancowinna"),
("Brazil/Acre", "Brazil/Acre"),
("Brazil/DeNoronha", "Brazil/DeNoronha"),
("Brazil/East", "Brazil/East"),
("Brazil/West", "Brazil/West"),
("CET", "CET"),
("CST6CDT", "CST6CDT"),
("Canada/Atlantic", "Canada/Atlantic"),
("Canada/Central", "Canada/Central"),
("Canada/Eastern", "Canada/Eastern"),
("Canada/Mountain", "Canada/Mountain"),
("Canada/Newfoundland", "Canada/Newfoundland"),
("Canada/Pacific", "Canada/Pacific"),
("Canada/Saskatchewan", "Canada/Saskatchewan"),
("Canada/Yukon", "Canada/Yukon"),
("Chile/Continental", "Chile/Continental"),
("Chile/EasterIsland", "Chile/EasterIsland"),
("Cuba", "Cuba"),
("EET", "EET"),
("EST", "EST"),
("EST5EDT", "EST5EDT"),
("Egypt", "Egypt"),
("Eire", "Eire"),
("Etc/GMT", "Etc/GMT"),
("Etc/GMT+0", "Etc/GMT+0"),
("Etc/GMT+1", "Etc/GMT+1"),
("Etc/GMT+10", "Etc/GMT+10"),
("Etc/GMT+11", "Etc/GMT+11"),
("Etc/GMT+12", "Etc/GMT+12"),
("Etc/GMT+2", "Etc/GMT+2"),
("Etc/GMT+3", "Etc/GMT+3"),
("Etc/GMT+4", "Etc/GMT+4"),
("Etc/GMT+5", "Etc/GMT+5"),
("Etc/GMT+6", "Etc/GMT+6"),
("Etc/GMT+7", "Etc/GMT+7"),
("Etc/GMT+8", "Etc/GMT+8"),
("Etc/GMT+9", "Etc/GMT+9"),
("Etc/GMT-0", "Etc/GMT-0"),
("Etc/GMT-1", "Etc/GMT-1"),
("Etc/GMT-10", "Etc/GMT-10"),
("Etc/GMT-11", "Etc/GMT-11"),
("Etc/GMT-12", "Etc/GMT-12"),
("Etc/GMT-13", "Etc/GMT-13"),
("Etc/GMT-14", "Etc/GMT-14"),
("Etc/GMT-2", "Etc/GMT-2"),
("Etc/GMT-3", "Etc/GMT-3"),
("Etc/GMT-4", "Etc/GMT-4"),
("Etc/GMT-5", "Etc/GMT-5"),
("Etc/GMT-6", "Etc/GMT-6"),
("Etc/GMT-7", "Etc/GMT-7"),
("Etc/GMT-8", "Etc/GMT-8"),
("Etc/GMT-9", "Etc/GMT-9"),
("Etc/GMT0", "Etc/GMT0"),
("Etc/Greenwich", "Etc/Greenwich"),
("Etc/UCT", "Etc/UCT"),
("Etc/UTC", "Etc/UTC"),
("Etc/Universal", "Etc/Universal"),
("Etc/Zulu", "Etc/Zulu"),
("Europe/Amsterdam", "Europe/Amsterdam"),
("Europe/Andorra", "Europe/Andorra"),
("Europe/Astrakhan", "Europe/Astrakhan"),
("Europe/Athens", "Europe/Athens"),
("Europe/Belfast", "Europe/Belfast"),
("Europe/Belgrade", "Europe/Belgrade"),
("Europe/Berlin", "Europe/Berlin"),
("Europe/Bratislava", "Europe/Bratislava"),
("Europe/Brussels", "Europe/Brussels"),
("Europe/Bucharest", "Europe/Bucharest"),
("Europe/Budapest", "Europe/Budapest"),
("Europe/Busingen", "Europe/Busingen"),
("Europe/Chisinau", "Europe/Chisinau"),
("Europe/Copenhagen", "Europe/Copenhagen"),
("Europe/Dublin", "Europe/Dublin"),
("Europe/Gibraltar", "Europe/Gibraltar"),
("Europe/Guernsey", "Europe/Guernsey"),
("Europe/Helsinki", "Europe/Helsinki"),
("Europe/Isle_of_Man", "Europe/Isle_of_Man"),
("Europe/Istanbul", "Europe/Istanbul"),
("Europe/Jersey", "Europe/Jersey"),
("Europe/Kaliningrad", "Europe/Kaliningrad"),
("Europe/Kiev", "Europe/Kiev"),
("Europe/Kirov", "Europe/Kirov"),
("Europe/Kyiv", "Europe/Kyiv"),
("Europe/Lisbon", "Europe/Lisbon"),
("Europe/Ljubljana", "Europe/Ljubljana"),
("Europe/London", "Europe/London"),
("Europe/Luxembourg", "Europe/Luxembourg"),
("Europe/Madrid", "Europe/Madrid"),
("Europe/Malta", "Europe/Malta"),
("Europe/Mariehamn", "Europe/Mariehamn"),
("Europe/Minsk", "Europe/Minsk"),
("Europe/Monaco", "Europe/Monaco"),
("Europe/Moscow", "Europe/Moscow"),
("Europe/Nicosia", "Europe/Nicosia"),
("Europe/Oslo", "Europe/Oslo"),
("Europe/Paris", "Europe/Paris"),
("Europe/Podgorica", "Europe/Podgorica"),
("Europe/Prague", "Europe/Prague"),
("Europe/Riga", "Europe/Riga"),
("Europe/Rome", "Europe/Rome"),
("Europe/Samara", "Europe/Samara"),
("Europe/San_Marino", "Europe/San_Marino"),
("Europe/Sarajevo", "Europe/Sarajevo"),
("Europe/Saratov", "Europe/Saratov"),
("Europe/Simferopol", "Europe/Simferopol"),
("Europe/Skopje", "Europe/Skopje"),
("Europe/Sofia", "Europe/Sofia"),
("Europe/Stockholm", "Europe/Stockholm"),
("Europe/Tallinn", "Europe/Tallinn"),
("Europe/Tirane", "Europe/Tirane"),
("Europe/Tiraspol", "Europe/Tiraspol"),
("Europe/Ulyanovsk", "Europe/Ulyanovsk"),
("Europe/Uzhgorod", "Europe/Uzhgorod"),
("Europe/Vaduz", "Europe/Vaduz"),
("Europe/Vatican", "Europe/Vatican"),
("Europe/Vienna", "Europe/Vienna"),
("Europe/Vilnius", "Europe/Vilnius"),
("Europe/Volgograd", "Europe/Volgograd"),
("Europe/Warsaw", "Europe/Warsaw"),
("Europe/Zagreb", "Europe/Zagreb"),
("Europe/Zaporozhye", "Europe/Zaporozhye"),
("Europe/Zurich", "Europe/Zurich"),
("GB", "GB"),
("GB-Eire", "GB-Eire"),
("GMT", "GMT"),
("GMT+0", "GMT+0"),
("GMT-0", "GMT-0"),
("GMT0", "GMT0"),
("Greenwich", "Greenwich"),
("HST", "HST"),
("Hongkong", "Hongkong"),
("Iceland", "Iceland"),
("Indian/Antananarivo", "Indian/Antananarivo"),
("Indian/Chagos", "Indian/Chagos"),
("Indian/Christmas", "Indian/Christmas"),
("Indian/Cocos", "Indian/Cocos"),
("Indian/Comoro", "Indian/Comoro"),
("Indian/Kerguelen", "Indian/Kerguelen"),
("Indian/Mahe", "Indian/Mahe"),
("Indian/Maldives", "Indian/Maldives"),
("Indian/Mauritius", "Indian/Mauritius"),
("Indian/Mayotte", "Indian/Mayotte"),
("Indian/Reunion", "Indian/Reunion"),
("Iran", "Iran"),
("Israel", "Israel"),
("Jamaica", "Jamaica"),
("Japan", "Japan"),
("Kwajalein", "Kwajalein"),
("Libya", "Libya"),
("MET", "MET"),
("MST", "MST"),
("MST7MDT", "MST7MDT"),
("Mexico/BajaNorte", "Mexico/BajaNorte"),
("Mexico/BajaSur", "Mexico/BajaSur"),
("Mexico/General", "Mexico/General"),
("NZ", "NZ"),
("NZ-CHAT", "NZ-CHAT"),
("Navajo", "Navajo"),
("PRC", "PRC"),
("PST8PDT", "PST8PDT"),
("Pacific/Apia", "Pacific/Apia"),
("Pacific/Auckland", "Pacific/Auckland"),
("Pacific/Bougainville", "Pacific/Bougainville"),
("Pacific/Chatham", "Pacific/Chatham"),
("Pacific/Chuuk", "Pacific/Chuuk"),
("Pacific/Easter", "Pacific/Easter"),
("Pacific/Efate", "Pacific/Efate"),
("Pacific/Enderbury", "Pacific/Enderbury"),
("Pacific/Fakaofo", "Pacific/Fakaofo"),
("Pacific/Fiji", "Pacific/Fiji"),
("Pacific/Funafuti", "Pacific/Funafuti"),
("Pacific/Galapagos", "Pacific/Galapagos"),
("Pacific/Gambier", "Pacific/Gambier"),
("Pacific/Guadalcanal", "Pacific/Guadalcanal"),
("Pacific/Guam", "Pacific/Guam"),
("Pacific/Honolulu", "Pacific/Honolulu"),
("Pacific/Johnston", "Pacific/Johnston"),
("Pacific/Kanton", "Pacific/Kanton"),
("Pacific/Kiritimati", "Pacific/Kiritimati"),
("Pacific/Kosrae", "Pacific/Kosrae"),
("Pacific/Kwajalein", "Pacific/Kwajalein"),
("Pacific/Majuro", "Pacific/Majuro"),
("Pacific/Marquesas", "Pacific/Marquesas"),
("Pacific/Midway", "Pacific/Midway"),
("Pacific/Nauru", "Pacific/Nauru"),
("Pacific/Niue", "Pacific/Niue"),
("Pacific/Norfolk", "Pacific/Norfolk"),
("Pacific/Noumea", "Pacific/Noumea"),
("Pacific/Pago_Pago", "Pacific/Pago_Pago"),
("Pacific/Palau", "Pacific/Palau"),
("Pacific/Pitcairn", "Pacific/Pitcairn"),
("Pacific/Pohnpei", "Pacific/Pohnpei"),
("Pacific/Ponape", "Pacific/Ponape"),
("Pacific/Port_Moresby", "Pacific/Port_Moresby"),
("Pacific/Rarotonga", "Pacific/Rarotonga"),
("Pacific/Saipan", "Pacific/Saipan"),
("Pacific/Samoa", "Pacific/Samoa"),
("Pacific/Tahiti", "Pacific/Tahiti"),
("Pacific/Tarawa", "Pacific/Tarawa"),
("Pacific/Tongatapu", "Pacific/Tongatapu"),
("Pacific/Truk", "Pacific/Truk"),
("Pacific/Wake", "Pacific/Wake"),
("Pacific/Wallis", "Pacific/Wallis"),
("Pacific/Yap", "Pacific/Yap"),
("Poland", "Poland"),
("Portugal", "Portugal"),
("ROC", "ROC"),
("ROK", "ROK"),
("Singapore", "Singapore"),
("Turkey", "Turkey"),
("UCT", "UCT"),
("US/Alaska", "US/Alaska"),
("US/Aleutian", "US/Aleutian"),
("US/Arizona", "US/Arizona"),
("US/Central", "US/Central"),
("US/East-Indiana", "US/East-Indiana"),
("US/Eastern", "US/Eastern"),
("US/Hawaii", "US/Hawaii"),
("US/Indiana-Starke", "US/Indiana-Starke"),
("US/Michigan", "US/Michigan"),
("US/Mountain", "US/Mountain"),
("US/Pacific", "US/Pacific"),
("US/Samoa", "US/Samoa"),
("UTC", "UTC"),
("Universal", "Universal"),
("W-SU", "W-SU"),
("WET", "WET"),
("Zulu", "Zulu"),
],
max_length=255,
null=True,
),
),
]

View File

@@ -1,6 +1,7 @@
import asyncio import asyncio
import re import re
from collections import Counter from collections import Counter
from contextlib import suppress
from distutils.version import LooseVersion from distutils.version import LooseVersion
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Union, cast from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Union, cast
@@ -130,7 +131,7 @@ class Agent(BaseAuditModel):
# return the default timezone unless the timezone is explicity set per agent # return the default timezone unless the timezone is explicity set per agent
if self.time_zone: if self.time_zone:
return self.time_zone return self.time_zone
else:
return get_core_settings().default_time_zone return get_core_settings().default_time_zone
@property @property
@@ -232,12 +233,12 @@ class Agent(BaseAuditModel):
alert_severity = ( alert_severity = (
check.check_result.alert_severity check.check_result.alert_severity
if check.check_type if check.check_type
in [ in (
CheckType.MEMORY, CheckType.MEMORY,
CheckType.CPU_LOAD, CheckType.CPU_LOAD,
CheckType.DISK_SPACE, CheckType.DISK_SPACE,
CheckType.SCRIPT, CheckType.SCRIPT,
] )
else check.alert_severity else check.alert_severity
) )
if alert_severity == AlertSeverity.ERROR: if alert_severity == AlertSeverity.ERROR:
@@ -333,7 +334,7 @@ class Agent(BaseAuditModel):
if len(ret) == 1: if len(ret) == 1:
return cast(str, ret[0]) return cast(str, ret[0])
else:
return ", ".join(ret) if ret else "error getting local ips" return ", ".join(ret) if ret else "error getting local ips"
@property @property
@@ -344,7 +345,7 @@ class Agent(BaseAuditModel):
except: except:
return "error getting make/model" return "error getting make/model"
try: with suppress(Exception):
comp_sys = self.wmi_detail["comp_sys"][0] comp_sys = self.wmi_detail["comp_sys"][0]
comp_sys_prod = self.wmi_detail["comp_sys_prod"][0] comp_sys_prod = self.wmi_detail["comp_sys_prod"][0]
make = [x["Vendor"] for x in comp_sys_prod if "Vendor" in x][0] make = [x["Vendor"] for x in comp_sys_prod if "Vendor" in x][0]
@@ -361,14 +362,10 @@ class Agent(BaseAuditModel):
model = sysfam model = sysfam
return f"{make} {model}" return f"{make} {model}"
except:
pass
try: with suppress(Exception):
comp_sys_prod = self.wmi_detail["comp_sys_prod"][0] comp_sys_prod = self.wmi_detail["comp_sys_prod"][0]
return cast(str, [x["Version"] for x in comp_sys_prod if "Version" in x][0]) return cast(str, [x["Version"] for x in comp_sys_prod if "Version" in x][0])
except:
pass
return "unknown make/model" return "unknown make/model"
@@ -479,7 +476,7 @@ class Agent(BaseAuditModel):
models.prefetch_related_objects( models.prefetch_related_objects(
[ [
policy policy
for policy in [self.policy, site_policy, client_policy, default_policy] for policy in (self.policy, site_policy, client_policy, default_policy)
if policy if policy
], ],
"excluded_agents", "excluded_agents",
@@ -532,12 +529,17 @@ class Agent(BaseAuditModel):
wait: bool = False, wait: bool = False,
run_on_any: bool = False, run_on_any: bool = False,
history_pk: int = 0, history_pk: int = 0,
run_as_user: bool = False,
) -> Any: ) -> Any:
from scripts.models import Script from scripts.models import Script
script = Script.objects.get(pk=scriptpk) script = Script.objects.get(pk=scriptpk)
# always override if set on script model
if script.run_as_user:
run_as_user = True
parsed_args = script.parse_script_args(self, script.shell, args) parsed_args = script.parse_script_args(self, script.shell, args)
data = { data = {
@@ -548,6 +550,7 @@ class Agent(BaseAuditModel):
"code": script.code, "code": script.code,
"shell": script.shell, "shell": script.shell,
}, },
"run_as_user": run_as_user,
} }
if history_pk != 0: if history_pk != 0:
@@ -583,7 +586,7 @@ class Agent(BaseAuditModel):
def approve_updates(self) -> None: def approve_updates(self) -> None:
patch_policy = self.get_patch_policy() patch_policy = self.get_patch_policy()
severity_list = list() severity_list = []
if patch_policy.critical == "approve": if patch_policy.critical == "approve":
severity_list.append("Critical") severity_list.append("Critical")
@@ -615,17 +618,14 @@ class Agent(BaseAuditModel):
if not agent_policy: if not agent_policy:
agent_policy = WinUpdatePolicy.objects.create(agent=self) agent_policy = WinUpdatePolicy.objects.create(agent=self)
# Get the list of policies applied to the agent and select the
# highest priority one.
policies = self.get_agent_policies() policies = self.get_agent_policies()
processed_policies: List[int] = list()
for _, policy in policies.items(): for _, policy in policies.items():
if ( if policy and policy.active and policy.winupdatepolicy.exists():
policy
and policy.active
and policy.pk not in processed_policies
and policy.winupdatepolicy.exists()
):
patch_policy = policy.winupdatepolicy.first() patch_policy = policy.winupdatepolicy.first()
break
# if policy still doesn't exist return the agent patch policy # if policy still doesn't exist return the agent patch policy
if not patch_policy: if not patch_policy:
@@ -677,7 +677,7 @@ class Agent(BaseAuditModel):
policies = self.get_agent_policies() policies = self.get_agent_policies()
# loop through all policies applied to agent and return an alert_template if found # loop through all policies applied to agent and return an alert_template if found
processed_policies: List[int] = list() processed_policies: List[int] = []
for key, policy in policies.items(): for key, policy in policies.items():
# default alert_template will override a default policy with alert template applied # default alert_template will override a default policy with alert template applied
if ( if (
@@ -742,10 +742,10 @@ class Agent(BaseAuditModel):
cache_key = f"agent_{self.agent_id}_checks" cache_key = f"agent_{self.agent_id}_checks"
elif self.policy: elif self.policy:
cache_key = f"site_{self.monitoring_type}_{self.site_id}_policy_{self.policy_id}_checks" cache_key = f"site_{self.monitoring_type}_{self.plat}_{self.site_id}_policy_{self.policy_id}_checks"
else: else:
cache_key = f"site_{self.monitoring_type}_{self.site_id}_checks" cache_key = f"site_{self.monitoring_type}_{self.plat}_{self.site_id}_checks"
cached_checks = cache.get(cache_key) cached_checks = cache.get(cache_key)
if isinstance(cached_checks, list): if isinstance(cached_checks, list):
@@ -767,10 +767,10 @@ class Agent(BaseAuditModel):
cache_key = f"agent_{self.agent_id}_tasks" cache_key = f"agent_{self.agent_id}_tasks"
elif self.policy: elif self.policy:
cache_key = f"site_{self.monitoring_type}_{self.site_id}_policy_{self.policy_id}_tasks" cache_key = f"site_{self.monitoring_type}_{self.plat}_{self.site_id}_policy_{self.policy_id}_tasks"
else: else:
cache_key = f"site_{self.monitoring_type}_{self.site_id}_tasks" cache_key = f"site_{self.monitoring_type}_{self.plat}_{self.site_id}_tasks"
cached_tasks = cache.get(cache_key) cached_tasks = cache.get(cache_key)
if isinstance(cached_tasks, list): if isinstance(cached_tasks, list):
@@ -778,7 +778,7 @@ class Agent(BaseAuditModel):
else: else:
# get agent tasks based on policies # get agent tasks based on policies
tasks = Policy.get_policy_tasks(self) tasks = Policy.get_policy_tasks(self)
cache.set(f"site_{self.site_id}_tasks", tasks, 600) cache.set(cache_key, tasks, 600)
return tasks return tasks
def _do_nats_debug(self, agent: "Agent", message: str) -> None: def _do_nats_debug(self, agent: "Agent", message: str) -> None:
@@ -829,9 +829,12 @@ class Agent(BaseAuditModel):
Return type: tuple(message: str, error: bool) Return type: tuple(message: str, error: bool)
""" """
if mode == "tacagent": if mode == "tacagent":
if self.is_posix: if self.plat == AgentPlat.LINUX:
cmd = "systemctl restart tacticalagent.service" cmd = "systemctl restart tacticalagent.service"
shell = 3 shell = 3
elif self.plat == AgentPlat.DARWIN:
cmd = "launchctl kickstart -k system/tacticalagent"
shell = 3
else: else:
cmd = "net stop tacticalrmm & taskkill /F /IM tacticalrmm.exe & net start tacticalrmm" cmd = "net stop tacticalrmm & taskkill /F /IM tacticalrmm.exe & net start tacticalrmm"
shell = 1 shell = 1
@@ -839,22 +842,22 @@ class Agent(BaseAuditModel):
asyncio.run( asyncio.run(
send_command_with_mesh(cmd, mesh_uri, self.mesh_node_id, shell, 0) send_command_with_mesh(cmd, mesh_uri, self.mesh_node_id, shell, 0)
) )
return ("ok", False) return "ok", False
elif mode == "mesh": elif mode == "mesh":
data = {"func": "recover", "payload": {"mode": mode}} data = {"func": "recover", "payload": {"mode": mode}}
if wait: if wait:
r = asyncio.run(self.nats_cmd(data, timeout=20)) r = asyncio.run(self.nats_cmd(data, timeout=20))
if r == "ok": if r == "ok":
return ("ok", False) return "ok", False
else: else:
return (str(r), True) return str(r), True
else: else:
asyncio.run(self.nats_cmd(data, timeout=20, wait=False)) asyncio.run(self.nats_cmd(data, timeout=20, wait=False))
return ("ok", False) return "ok", False
return ("invalid", True) return "invalid", True
@staticmethod @staticmethod
def serialize(agent: "Agent") -> Dict[str, Any]: def serialize(agent: "Agent") -> Dict[str, Any]:
@@ -864,7 +867,7 @@ class Agent(BaseAuditModel):
return AgentAuditSerializer(agent).data return AgentAuditSerializer(agent).data
def delete_superseded_updates(self) -> None: def delete_superseded_updates(self) -> None:
try: with suppress(Exception):
pks = [] # list of pks to delete pks = [] # list of pks to delete
kbs = list(self.winupdates.values_list("kb", flat=True)) kbs = list(self.winupdates.values_list("kb", flat=True))
d = Counter(kbs) d = Counter(kbs)
@@ -889,8 +892,6 @@ class Agent(BaseAuditModel):
pks = list(set(pks)) pks = list(set(pks))
self.winupdates.filter(pk__in=pks).delete() self.winupdates.filter(pk__in=pks).delete()
except:
pass
def should_create_alert( def should_create_alert(
self, alert_template: "Optional[AlertTemplate]" = None self, alert_template: "Optional[AlertTemplate]" = None
@@ -1009,16 +1010,16 @@ class AgentCustomField(models.Model):
return cast(List[str], self.multiple_value) return cast(List[str], self.multiple_value)
elif self.field.type == CustomFieldType.CHECKBOX: elif self.field.type == CustomFieldType.CHECKBOX:
return self.bool_value return self.bool_value
else:
return cast(str, self.string_value) return cast(str, self.string_value)
def save_to_field(self, value: Union[List[Any], bool, str]) -> None: def save_to_field(self, value: Union[List[Any], bool, str]) -> None:
if self.field.type in [ if self.field.type in (
CustomFieldType.TEXT, CustomFieldType.TEXT,
CustomFieldType.NUMBER, CustomFieldType.NUMBER,
CustomFieldType.SINGLE, CustomFieldType.SINGLE,
CustomFieldType.DATETIME, CustomFieldType.DATETIME,
]: ):
self.string_value = cast(str, value) self.string_value = cast(str, value)
self.save() self.save()
elif self.field.type == CustomFieldType.MULTIPLE: elif self.field.type == CustomFieldType.MULTIPLE:

View File

@@ -122,5 +122,5 @@ class AgentHistoryPerms(permissions.BasePermission):
return _has_perm(r, "can_list_agent_history") and _has_perm_on_agent( return _has_perm(r, "can_list_agent_history") and _has_perm_on_agent(
r.user, view.kwargs["agent_id"] r.user, view.kwargs["agent_id"]
) )
else:
return _has_perm(r, "can_list_agent_history") return _has_perm(r, "can_list_agent_history")

View File

@@ -90,12 +90,17 @@ class AgentTableSerializer(serializers.ModelSerializer):
last_seen = serializers.ReadOnlyField() last_seen = serializers.ReadOnlyField()
pending_actions_count = serializers.ReadOnlyField() pending_actions_count = serializers.ReadOnlyField()
has_patches_pending = serializers.ReadOnlyField() has_patches_pending = serializers.ReadOnlyField()
cpu_model = serializers.ReadOnlyField()
graphics = serializers.ReadOnlyField()
local_ips = serializers.ReadOnlyField()
make_model = serializers.ReadOnlyField()
physical_disks = serializers.ReadOnlyField()
def get_alert_template(self, obj): def get_alert_template(self, obj):
if not obj.alert_template: if not obj.alert_template:
return None return None
else:
return { return {
"name": obj.alert_template.name, "name": obj.alert_template.name,
"always_email": obj.alert_template.agent_always_email, "always_email": obj.alert_template.agent_always_email,
@@ -108,7 +113,7 @@ class AgentTableSerializer(serializers.ModelSerializer):
return obj.last_logged_in_user return obj.last_logged_in_user
elif obj.logged_in_username != "None": elif obj.logged_in_username != "None":
return obj.logged_in_username return obj.logged_in_username
else:
return "-" return "-"
def get_italic(self, obj) -> bool: def get_italic(self, obj) -> bool:
@@ -141,16 +146,18 @@ class AgentTableSerializer(serializers.ModelSerializer):
"plat", "plat",
"goarch", "goarch",
"has_patches_pending", "has_patches_pending",
"version",
"operating_system",
"public_ip",
"cpu_model",
"graphics",
"local_ips",
"make_model",
"physical_disks",
] ]
depth = 2 depth = 2
class WinAgentSerializer(serializers.ModelSerializer):
class Meta:
model = Agent
fields = "__all__"
class AgentHostnameSerializer(serializers.ModelSerializer): class AgentHostnameSerializer(serializers.ModelSerializer):
client = serializers.ReadOnlyField(source="client.name") client = serializers.ReadOnlyField(source="client.name")
site = serializers.ReadOnlyField(source="site.name") site = serializers.ReadOnlyField(source="site.name")

View File

@@ -153,6 +153,7 @@ def run_script_email_results_task(
emails: list[str], emails: list[str],
args: list[str] = [], args: list[str] = [],
history_pk: int = 0, history_pk: int = 0,
run_as_user: bool = False,
): ):
agent = Agent.objects.get(pk=agentpk) agent = Agent.objects.get(pk=agentpk)
script = Script.objects.get(pk=scriptpk) script = Script.objects.get(pk=scriptpk)
@@ -163,6 +164,7 @@ def run_script_email_results_task(
timeout=nats_timeout, timeout=nats_timeout,
wait=True, wait=True,
history_pk=history_pk, history_pk=history_pk,
run_as_user=run_as_user,
) )
if r == "timeout": if r == "timeout":
DebugLog.error( DebugLog.error(

View File

@@ -84,7 +84,7 @@ class TestAgentUpdate(TacticalTestCase):
site=self.site1, site=self.site1,
monitoring_type=AgentMonType.SERVER, monitoring_type=AgentMonType.SERVER,
plat=AgentPlat.WINDOWS, plat=AgentPlat.WINDOWS,
version="2.3.0", version="2.1.1",
) )
r = agent_noarch.do_update(token="", force=True) r = agent_noarch.do_update(token="", force=True)
self.assertEqual(r, "noarch") self.assertEqual(r, "noarch")
@@ -106,7 +106,7 @@ class TestAgentUpdate(TacticalTestCase):
site=self.site1, site=self.site1,
monitoring_type=AgentMonType.SERVER, monitoring_type=AgentMonType.SERVER,
plat=AgentPlat.WINDOWS, plat=AgentPlat.WINDOWS,
version="2.3.0", version="2.1.1",
goarch=GoArch.AMD64, goarch=GoArch.AMD64,
) )
@@ -115,7 +115,7 @@ class TestAgentUpdate(TacticalTestCase):
site=self.site3, site=self.site3,
monitoring_type=AgentMonType.WORKSTATION, monitoring_type=AgentMonType.WORKSTATION,
plat=AgentPlat.LINUX, plat=AgentPlat.LINUX,
version="2.3.0", version="2.1.1",
goarch=GoArch.ARM32, goarch=GoArch.ARM32,
) )
@@ -193,7 +193,7 @@ class TestAgentUpdate(TacticalTestCase):
site=self.site2, site=self.site2,
monitoring_type=AgentMonType.SERVER, monitoring_type=AgentMonType.SERVER,
plat=AgentPlat.WINDOWS, plat=AgentPlat.WINDOWS,
version="2.3.0", version="2.1.1",
goarch=GoArch.AMD64, goarch=GoArch.AMD64,
_quantity=6, _quantity=6,
) )
@@ -215,7 +215,7 @@ class TestAgentUpdate(TacticalTestCase):
site=self.site2, site=self.site2,
monitoring_type=AgentMonType.SERVER, monitoring_type=AgentMonType.SERVER,
plat=AgentPlat.WINDOWS, plat=AgentPlat.WINDOWS,
version="2.3.0", version="2.1.1",
goarch=GoArch.AMD64, goarch=GoArch.AMD64,
_quantity=7, _quantity=7,
) )

View File

@@ -50,7 +50,7 @@ class TestAgentUtils(TacticalTestCase):
self.assertIn(r"agentDL='asdasd3423'", ret) self.assertIn(r"agentDL='asdasd3423'", ret)
self.assertIn( self.assertIn(
r"meshDL='meshsite/meshagents?id=meshid&installflags=0&meshinstall=6'", ret r"meshDL='meshsite/meshagents?id=meshid&installflags=2&meshinstall=6'", ret
) )
self.assertIn(r"apiURL='api.example.com'", ret) self.assertIn(r"apiURL='api.example.com'", ret)
self.assertIn(r"agentDL='asdasd3423'", ret) self.assertIn(r"agentDL='asdasd3423'", ret)

View File

@@ -403,6 +403,7 @@ class TestAgentViews(TacticalTestCase):
"cmd": "ipconfig", "cmd": "ipconfig",
"shell": "cmd", "shell": "cmd",
"timeout": 30, "timeout": 30,
"run_as_user": False,
} }
mock_ret.return_value = "nt authority\\system" mock_ret.return_value = "nt authority\\system"
r = self.client.post(url, data, format="json") r = self.client.post(url, data, format="json")
@@ -417,16 +418,20 @@ class TestAgentViews(TacticalTestCase):
@patch("agents.models.Agent.nats_cmd") @patch("agents.models.Agent.nats_cmd")
def test_reboot_later(self, nats_cmd): def test_reboot_later(self, nats_cmd):
nats_cmd.return_value = "ok"
url = f"{base_url}/{self.agent.agent_id}/reboot/" url = f"{base_url}/{self.agent.agent_id}/reboot/"
data = { # ensure we don't allow dates in past
"datetime": "2025-08-29T18:41:02", data = {"datetime": "2022-07-11T01:51"}
} r = self.client.patch(url, data, format="json")
self.assertEqual(r.status_code, 400)
self.assertEqual(r.data, "Date cannot be set in the past")
nats_cmd.return_value = "ok" # test with date in future
data["datetime"] = "2027-08-29T18:41"
r = self.client.patch(url, data, format="json") r = self.client.patch(url, data, format="json")
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
self.assertEqual(r.data["time"], "August 29, 2025 at 06:41 PM") self.assertEqual(r.data["time"], "August 29, 2027 at 06:41 PM")
self.assertEqual(r.data["agent"], self.agent.hostname) self.assertEqual(r.data["agent"], self.agent.hostname)
nats_data = { nats_data = {
@@ -439,12 +444,12 @@ class TestAgentViews(TacticalTestCase):
"multiple_instances": 2, "multiple_instances": 2,
"trigger": "runonce", "trigger": "runonce",
"name": r.data["task_name"], "name": r.data["task_name"],
"start_year": 2025, "start_year": 2027,
"start_month": 8, "start_month": 8,
"start_day": 29, "start_day": 29,
"start_hour": 18, "start_hour": 18,
"start_min": 41, "start_min": 41,
"expire_year": 2025, "expire_year": 2027,
"expire_month": 8, "expire_month": 8,
"expire_day": 29, "expire_day": 29,
"expire_hour": 18, "expire_hour": 18,
@@ -534,6 +539,7 @@ class TestAgentViews(TacticalTestCase):
"output": "wait", "output": "wait",
"args": [], "args": [],
"timeout": 15, "timeout": 15,
"run_as_user": False,
} }
r = self.client.post(url, data, format="json") r = self.client.post(url, data, format="json")
@@ -543,7 +549,12 @@ class TestAgentViews(TacticalTestCase):
raise AgentHistory.DoesNotExist raise AgentHistory.DoesNotExist
run_script.assert_called_with( run_script.assert_called_with(
scriptpk=script.pk, args=[], timeout=18, wait=True, history_pk=hist.pk scriptpk=script.pk,
args=[],
timeout=18,
wait=True,
history_pk=hist.pk,
run_as_user=False,
) )
run_script.reset_mock() run_script.reset_mock()
@@ -555,6 +566,7 @@ class TestAgentViews(TacticalTestCase):
"timeout": 15, "timeout": 15,
"emailMode": "default", "emailMode": "default",
"emails": ["admin@example.com", "bob@example.com"], "emails": ["admin@example.com", "bob@example.com"],
"run_as_user": False,
} }
r = self.client.post(url, data, format="json") r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
@@ -564,6 +576,7 @@ class TestAgentViews(TacticalTestCase):
nats_timeout=18, nats_timeout=18,
emails=[], emails=[],
args=["abc", "123"], args=["abc", "123"],
run_as_user=False,
) )
email_task.reset_mock() email_task.reset_mock()
@@ -577,6 +590,7 @@ class TestAgentViews(TacticalTestCase):
nats_timeout=18, nats_timeout=18,
emails=["admin@example.com", "bob@example.com"], emails=["admin@example.com", "bob@example.com"],
args=["abc", "123"], args=["abc", "123"],
run_as_user=False,
) )
# test fire and forget # test fire and forget
@@ -585,6 +599,7 @@ class TestAgentViews(TacticalTestCase):
"output": "forget", "output": "forget",
"args": ["hello", "world"], "args": ["hello", "world"],
"timeout": 22, "timeout": 22,
"run_as_user": True,
} }
r = self.client.post(url, data, format="json") r = self.client.post(url, data, format="json")
@@ -594,7 +609,11 @@ class TestAgentViews(TacticalTestCase):
raise AgentHistory.DoesNotExist raise AgentHistory.DoesNotExist
run_script.assert_called_with( run_script.assert_called_with(
scriptpk=script.pk, args=["hello", "world"], timeout=25, history_pk=hist.pk scriptpk=script.pk,
args=["hello", "world"],
timeout=25,
history_pk=hist.pk,
run_as_user=True,
) )
run_script.reset_mock() run_script.reset_mock()
@@ -609,6 +628,7 @@ class TestAgentViews(TacticalTestCase):
"timeout": 22, "timeout": 22,
"custom_field": custom_field.pk, "custom_field": custom_field.pk,
"save_all_output": True, "save_all_output": True,
"run_as_user": False,
} }
r = self.client.post(url, data, format="json") r = self.client.post(url, data, format="json")
@@ -623,6 +643,7 @@ class TestAgentViews(TacticalTestCase):
timeout=25, timeout=25,
wait=True, wait=True,
history_pk=hist.pk, history_pk=hist.pk,
run_as_user=False,
) )
run_script.reset_mock() run_script.reset_mock()
@@ -640,6 +661,7 @@ class TestAgentViews(TacticalTestCase):
"timeout": 22, "timeout": 22,
"custom_field": custom_field.pk, "custom_field": custom_field.pk,
"save_all_output": False, "save_all_output": False,
"run_as_user": False,
} }
r = self.client.post(url, data, format="json") r = self.client.post(url, data, format="json")
@@ -654,6 +676,7 @@ class TestAgentViews(TacticalTestCase):
timeout=25, timeout=25,
wait=True, wait=True,
history_pk=hist.pk, history_pk=hist.pk,
run_as_user=False,
) )
run_script.reset_mock() run_script.reset_mock()
@@ -673,6 +696,7 @@ class TestAgentViews(TacticalTestCase):
"timeout": 22, "timeout": 22,
"custom_field": custom_field.pk, "custom_field": custom_field.pk,
"save_all_output": False, "save_all_output": False,
"run_as_user": False,
} }
r = self.client.post(url, data, format="json") r = self.client.post(url, data, format="json")
@@ -687,6 +711,7 @@ class TestAgentViews(TacticalTestCase):
timeout=25, timeout=25,
wait=True, wait=True,
history_pk=hist.pk, history_pk=hist.pk,
run_as_user=False,
) )
run_script.reset_mock() run_script.reset_mock()
@@ -703,6 +728,7 @@ class TestAgentViews(TacticalTestCase):
"output": "note", "output": "note",
"args": ["hello", "world"], "args": ["hello", "world"],
"timeout": 22, "timeout": 22,
"run_as_user": False,
} }
r = self.client.post(url, data, format="json") r = self.client.post(url, data, format="json")
@@ -717,6 +743,7 @@ class TestAgentViews(TacticalTestCase):
timeout=25, timeout=25,
wait=True, wait=True,
history_pk=hist.pk, history_pk=hist.pk,
run_as_user=False,
) )
run_script.reset_mock() run_script.reset_mock()

View File

@@ -1,6 +1,7 @@
import asyncio import asyncio
import tempfile import tempfile
import urllib.parse import urllib.parse
from pathlib import Path
from django.conf import settings from django.conf import settings
from django.http import FileResponse from django.http import FileResponse
@@ -51,12 +52,10 @@ def generate_linux_install(
uri = get_mesh_ws_url() uri = get_mesh_ws_url()
mesh_id = asyncio.run(get_mesh_device_id(uri, core.mesh_device_group)) mesh_id = asyncio.run(get_mesh_device_id(uri, core.mesh_device_group))
mesh_dl = ( mesh_dl = (
f"{core.mesh_site}/meshagents?id={mesh_id}&installflags=0&meshinstall={arch_id}" f"{core.mesh_site}/meshagents?id={mesh_id}&installflags=2&meshinstall={arch_id}"
) )
sh = settings.LINUX_AGENT_SCRIPT text = Path(settings.LINUX_AGENT_SCRIPT).read_text()
with open(sh, "r") as f:
text = f.read()
replace = { replace = {
"agentDLChange": download_url, "agentDLChange": download_url,

View File

@@ -4,6 +4,7 @@ import os
import random import random
import string import string
import time import time
from pathlib import Path
from django.conf import settings from django.conf import settings
from django.db.models import Count, Exists, OuterRef, Prefetch, Q from django.db.models import Count, Exists, OuterRef, Prefetch, Q
@@ -12,6 +13,7 @@ from django.shortcuts import get_object_or_404
from django.utils import timezone as djangotime from django.utils import timezone as djangotime
from meshctrl.utils import get_login_token from meshctrl.utils import get_login_token
from packaging import version as pyver from packaging import version as pyver
from rest_framework import serializers
from rest_framework.decorators import api_view, permission_classes from rest_framework.decorators import api_view, permission_classes
from rest_framework.exceptions import PermissionDenied from rest_framework.exceptions import PermissionDenied
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
@@ -31,15 +33,17 @@ from tacticalrmm.constants import (
AGENT_DEFER, AGENT_DEFER,
AGENT_STATUS_OFFLINE, AGENT_STATUS_OFFLINE,
AGENT_STATUS_ONLINE, AGENT_STATUS_ONLINE,
AGENT_TABLE_DEFER,
AgentHistoryType, AgentHistoryType,
AgentMonType, AgentMonType,
AgentPlat, AgentPlat,
CustomFieldModel, CustomFieldModel,
DebugLogType,
EvtLogNames, EvtLogNames,
PAAction, PAAction,
PAStatus, PAStatus,
) )
from tacticalrmm.helpers import notify_error from tacticalrmm.helpers import date_is_in_past, notify_error
from tacticalrmm.permissions import ( from tacticalrmm.permissions import (
_has_perm_on_agent, _has_perm_on_agent,
_has_perm_on_client, _has_perm_on_client,
@@ -113,7 +117,7 @@ class GetAgents(APIView):
Agent.objects.filter_by_role(request.user) # type: ignore Agent.objects.filter_by_role(request.user) # type: ignore
.filter(monitoring_type_filter) .filter(monitoring_type_filter)
.filter(client_site_filter) .filter(client_site_filter)
.defer(*AGENT_DEFER) .defer(*AGENT_TABLE_DEFER)
.select_related( .select_related(
"site__server_policy", "site__server_policy",
"site__workstation_policy", "site__workstation_policy",
@@ -165,6 +169,25 @@ class GetAgents(APIView):
class GetUpdateDeleteAgent(APIView): class GetUpdateDeleteAgent(APIView):
permission_classes = [IsAuthenticated, AgentPerms] permission_classes = [IsAuthenticated, AgentPerms]
class InputSerializer(serializers.ModelSerializer):
class Meta:
model = Agent
fields = [
"maintenance_mode", # TODO separate this
"policy", # TODO separate this
"block_policy_inheritance", # TODO separate this
"monitoring_type",
"description",
"overdue_email_alert",
"overdue_text_alert",
"overdue_dashboard_alert",
"offline_time",
"overdue_time",
"check_interval",
"time_zone",
"site",
]
# get agent details # get agent details
def get(self, request, agent_id): def get(self, request, agent_id):
agent = get_object_or_404(Agent, agent_id=agent_id) agent = get_object_or_404(Agent, agent_id=agent_id)
@@ -174,9 +197,9 @@ class GetUpdateDeleteAgent(APIView):
def put(self, request, agent_id): def put(self, request, agent_id):
agent = get_object_or_404(Agent, agent_id=agent_id) agent = get_object_or_404(Agent, agent_id=agent_id)
a_serializer = AgentSerializer(instance=agent, data=request.data, partial=True) s = self.InputSerializer(instance=agent, data=request.data, partial=True)
a_serializer.is_valid(raise_exception=True) s.is_valid(raise_exception=True)
a_serializer.save() s.save()
if "winupdatepolicy" in request.data.keys(): if "winupdatepolicy" in request.data.keys():
policy = agent.winupdatepolicy.get() # type: ignore policy = agent.winupdatepolicy.get() # type: ignore
@@ -215,18 +238,25 @@ class GetUpdateDeleteAgent(APIView):
def delete(self, request, agent_id): def delete(self, request, agent_id):
agent = get_object_or_404(Agent, agent_id=agent_id) agent = get_object_or_404(Agent, agent_id=agent_id)
code = "foo" code = "foo" # stub for windows
if agent.plat == AgentPlat.LINUX: if agent.plat == AgentPlat.LINUX:
with open(settings.LINUX_AGENT_SCRIPT, "r") as f: code = Path(settings.LINUX_AGENT_SCRIPT).read_text()
code = f.read() elif agent.plat == AgentPlat.DARWIN:
code = Path(settings.MAC_UNINSTALL).read_text()
asyncio.run(agent.nats_cmd({"func": "uninstall", "code": code}, wait=False)) asyncio.run(agent.nats_cmd({"func": "uninstall", "code": code}, wait=False))
name = agent.hostname name = agent.hostname
mesh_id = agent.mesh_node_id mesh_id = agent.mesh_node_id
agent.delete() agent.delete()
reload_nats() reload_nats()
try:
uri = get_mesh_ws_url() uri = get_mesh_ws_url()
asyncio.run(remove_mesh_agent(uri, mesh_id)) asyncio.run(remove_mesh_agent(uri, mesh_id))
except Exception as e:
DebugLog.error(
message=f"Unable to remove agent {name} from meshcentral database: {e}",
log_type=DebugLogType.AGENT_ISSUES,
)
return Response(f"{name} will now be uninstalled.") return Response(f"{name} will now be uninstalled.")
@@ -242,7 +272,7 @@ class AgentProcesses(APIView):
agent = get_object_or_404(Agent, agent_id=agent_id) agent = get_object_or_404(Agent, agent_id=agent_id)
r = asyncio.run(agent.nats_cmd(data={"func": "procs"}, timeout=5)) r = asyncio.run(agent.nats_cmd(data={"func": "procs"}, timeout=5))
if r == "timeout" or r == "natsdown": if r in ("timeout", "natsdown"):
return notify_error("Unable to contact the agent") return notify_error("Unable to contact the agent")
return Response(r) return Response(r)
@@ -253,7 +283,7 @@ class AgentProcesses(APIView):
agent.nats_cmd({"func": "killproc", "procpid": int(pid)}, timeout=15) agent.nats_cmd({"func": "killproc", "procpid": int(pid)}, timeout=15)
) )
if r == "timeout" or r == "natsdown": if r in ("timeout", "natsdown"):
return notify_error("Unable to contact the agent") return notify_error("Unable to contact the agent")
elif r != "ok": elif r != "ok":
return notify_error(r) return notify_error(r)
@@ -385,7 +415,7 @@ def get_event_log(request, agent_id, logtype, days):
}, },
} }
r = asyncio.run(agent.nats_cmd(data, timeout=timeout + 2)) r = asyncio.run(agent.nats_cmd(data, timeout=timeout + 2))
if r == "timeout" or r == "natsdown": if r in ("timeout", "natsdown"):
return notify_error("Unable to contact the agent") return notify_error("Unable to contact the agent")
return Response(r) return Response(r)
@@ -408,6 +438,7 @@ def send_raw_cmd(request, agent_id):
"command": request.data["cmd"], "command": request.data["cmd"],
"shell": shell, "shell": shell,
}, },
"run_as_user": request.data["run_as_user"],
} }
hist = AgentHistory.objects.create( hist = AgentHistory.objects.create(
@@ -452,10 +483,13 @@ class Reboot(APIView):
return notify_error(f"Not currently implemented for {agent.plat}") return notify_error(f"Not currently implemented for {agent.plat}")
try: try:
obj = dt.datetime.strptime(request.data["datetime"], "%Y-%m-%dT%H:%M:%S") obj = dt.datetime.strptime(request.data["datetime"], "%Y-%m-%dT%H:%M")
except Exception: except Exception:
return notify_error("Invalid date") return notify_error("Invalid date")
if date_is_in_past(datetime_obj=obj, agent_tz=agent.timezone):
return notify_error("Date cannot be set in the past")
task_name = "TacticalRMM_SchedReboot_" + "".join( task_name = "TacticalRMM_SchedReboot_" + "".join(
random.choice(string.ascii_letters) for _ in range(10) random.choice(string.ascii_letters) for _ in range(10)
) )
@@ -519,7 +553,15 @@ def install_agent(request):
codesign_token, is_valid = token_is_valid() codesign_token, is_valid = token_is_valid()
inno = f"tacticalagent-v{version}-{plat}-{goarch}.exe" if request.data["installMethod"] in {"bash", "mac"} and not is_valid:
return notify_error(
"Missing code signing token, or token is no longer valid. Please read the docs for more info."
)
inno = f"tacticalagent-v{version}-{plat}-{goarch}"
if plat == AgentPlat.WINDOWS:
inno += ".exe"
download_url = get_agent_url(goarch=goarch, plat=plat, token=codesign_token) download_url = get_agent_url(goarch=goarch, plat=plat, token=codesign_token)
installer_user = User.objects.filter(is_installer_user=True).first() installer_user = User.objects.filter(is_installer_user=True).first()
@@ -528,6 +570,21 @@ def install_agent(request):
user=installer_user, expiry=dt.timedelta(hours=request.data["expires"]) user=installer_user, expiry=dt.timedelta(hours=request.data["expires"])
) )
install_flags = [
"-m",
"install",
"--api",
request.data["api"],
"--client-id",
client_id,
"--site-id",
site_id,
"--agent-type",
request.data["agenttype"],
"--auth",
token,
]
if request.data["installMethod"] == "exe": if request.data["installMethod"] == "exe":
from tacticalrmm.utils import generate_winagent_exe from tacticalrmm.utils import generate_winagent_exe
@@ -545,14 +602,6 @@ def install_agent(request):
) )
elif request.data["installMethod"] == "bash": elif request.data["installMethod"] == "bash":
# TODO
# linux agents are in beta for now, only available for sponsors for testing
# remove this after it's out of beta
if not is_valid:
return notify_error(
"Missing code signing token, or token is no longer valid. Please read the docs for more info."
)
from agents.utils import generate_linux_install from agents.utils import generate_linux_install
@@ -566,7 +615,9 @@ def install_agent(request):
download_url=download_url, download_url=download_url,
) )
elif request.data["installMethod"] == "manual": elif request.data["installMethod"] in {"manual", "mac"}:
resp = {}
if request.data["installMethod"] == "manual":
cmd = [ cmd = [
inno, inno,
"/VERYSILENT", "/VERYSILENT",
@@ -578,19 +629,7 @@ def install_agent(request):
"5", "5",
"&&", "&&",
r'"C:\Program Files\TacticalAgent\tacticalrmm.exe"', r'"C:\Program Files\TacticalAgent\tacticalrmm.exe"',
"-m", ] + install_flags
"install",
"--api",
request.data["api"],
"--client-id",
client_id,
"--site-id",
site_id,
"--agent-type",
request.data["agenttype"],
"--auth",
token,
]
if int(request.data["rdp"]): if int(request.data["rdp"]):
cmd.append("--rdp") cmd.append("--rdp")
@@ -599,19 +638,22 @@ def install_agent(request):
if int(request.data["power"]): if int(request.data["power"]):
cmd.append("--power") cmd.append("--power")
resp = { resp["cmd"] = " ".join(str(i) for i in cmd)
"cmd": " ".join(str(i) for i in cmd), else:
"url": download_url, install_flags.insert(0, f"sudo ./{inno}")
} cmd = install_flags.copy()
dl = f"curl -L -o {inno} '{download_url}'"
resp["cmd"] = (
dl + f" && chmod +x {inno} && " + " ".join(str(i) for i in cmd)
)
resp["url"] = download_url
return Response(resp) return Response(resp)
elif request.data["installMethod"] == "powershell": elif request.data["installMethod"] == "powershell":
ps = os.path.join(settings.BASE_DIR, "core/installer.ps1") text = Path(settings.BASE_DIR / "core" / "installer.ps1").read_text()
with open(ps, "r") as f:
text = f.read()
replace_dict = { replace_dict = {
"innosetupchange": inno, "innosetupchange": inno,
@@ -638,8 +680,7 @@ def install_agent(request):
except Exception as e: except Exception as e:
DebugLog.error(message=str(e)) DebugLog.error(message=str(e))
with open(ps1, "w") as f: Path(ps1).write_text(text)
f.write(text)
if settings.DEBUG: if settings.DEBUG:
with open(ps1, "r") as f: with open(ps1, "r") as f:
@@ -681,6 +722,7 @@ def run_script(request, agent_id):
script = get_object_or_404(Script, pk=request.data["script"]) script = get_object_or_404(Script, pk=request.data["script"])
output = request.data["output"] output = request.data["output"]
args = request.data["args"] args = request.data["args"]
run_as_user: bool = request.data["run_as_user"]
req_timeout = int(request.data["timeout"]) + 3 req_timeout = int(request.data["timeout"]) + 3
AuditLog.audit_script_run( AuditLog.audit_script_run(
@@ -705,6 +747,7 @@ def run_script(request, agent_id):
timeout=req_timeout, timeout=req_timeout,
wait=True, wait=True,
history_pk=history_pk, history_pk=history_pk,
run_as_user=run_as_user,
) )
return Response(r) return Response(r)
@@ -718,6 +761,7 @@ def run_script(request, agent_id):
nats_timeout=req_timeout, nats_timeout=req_timeout,
emails=emails, emails=emails,
args=args, args=args,
run_as_user=run_as_user,
) )
elif output == "collector": elif output == "collector":
from core.models import CustomField from core.models import CustomField
@@ -728,6 +772,7 @@ def run_script(request, agent_id):
timeout=req_timeout, timeout=req_timeout,
wait=True, wait=True,
history_pk=history_pk, history_pk=history_pk,
run_as_user=run_as_user,
) )
custom_field = CustomField.objects.get(pk=request.data["custom_field"]) custom_field = CustomField.objects.get(pk=request.data["custom_field"])
@@ -756,13 +801,18 @@ def run_script(request, agent_id):
timeout=req_timeout, timeout=req_timeout,
wait=True, wait=True,
history_pk=history_pk, history_pk=history_pk,
run_as_user=run_as_user,
) )
Note.objects.create(agent=agent, user=request.user, note=r) Note.objects.create(agent=agent, user=request.user, note=r)
return Response(r) return Response(r)
else: else:
agent.run_script( agent.run_script(
scriptpk=script.pk, args=args, timeout=req_timeout, history_pk=history_pk scriptpk=script.pk,
args=args,
timeout=req_timeout,
history_pk=history_pk,
run_as_user=run_as_user,
) )
return Response(f"{script.name} will now be run on {agent.hostname}") return Response(f"{script.name} will now be run on {agent.hostname}")
@@ -872,6 +922,8 @@ def bulk(request):
q = q.filter(plat=AgentPlat.WINDOWS) q = q.filter(plat=AgentPlat.WINDOWS)
elif request.data["osType"] == AgentPlat.LINUX: elif request.data["osType"] == AgentPlat.LINUX:
q = q.filter(plat=AgentPlat.LINUX) q = q.filter(plat=AgentPlat.LINUX)
elif request.data["osType"] == AgentPlat.DARWIN:
q = q.filter(plat=AgentPlat.DARWIN)
agents: list[int] = [agent.pk for agent in q] agents: list[int] = [agent.pk for agent in q]
@@ -897,7 +949,7 @@ def bulk(request):
shell, shell,
request.data["timeout"], request.data["timeout"],
request.user.username[:50], request.user.username[:50],
run_on_offline=request.data["offlineAgents"], request.data["run_as_user"],
) )
return Response(f"Command will now be run on {len(agents)} agents") return Response(f"Command will now be run on {len(agents)} agents")
@@ -909,6 +961,7 @@ def bulk(request):
request.data["args"], request.data["args"],
request.data["timeout"], request.data["timeout"],
request.user.username[:50], request.user.username[:50],
request.data["run_as_user"],
) )
return Response(f"{script.name} will now be run on {len(agents)} agents") return Response(f"{script.name} will now be run on {len(agents)} agents")
@@ -956,7 +1009,7 @@ def agent_maintenance(request):
if count: if count:
action = "disabled" if not request.data["action"] else "enabled" action = "disabled" if not request.data["action"] else "enabled"
return Response(f"Maintenance mode has been {action} on {count} agents") return Response(f"Maintenance mode has been {action} on {count} agents")
else:
return Response( return Response(
f"No agents have been put in maintenance mode. You might not have permissions to the resources." f"No agents have been put in maintenance mode. You might not have permissions to the resources."
) )

View File

@@ -469,6 +469,7 @@ class Alert(models.Model):
wait=True, wait=True,
full=True, full=True,
run_on_any=True, run_on_any=True,
run_as_user=False,
) )
# command was successful # command was successful
@@ -591,6 +592,7 @@ class Alert(models.Model):
wait=True, wait=True,
full=True, full=True,
run_on_any=True, run_on_any=True,
run_as_user=False,
) )
# command was successful # command was successful
@@ -615,7 +617,7 @@ class Alert(models.Model):
if not args: if not args:
return [] return []
temp_args = list() temp_args = []
# pattern to match for injection # pattern to match for injection
pattern = re.compile(".*\\{\\{alert\\.(.*)\\}\\}.*") pattern = re.compile(".*\\{\\{alert\\.(.*)\\}\\}.*")

View File

@@ -32,7 +32,7 @@ def _has_perm_on_alert(user: "User", id: int) -> bool:
class AlertPerms(permissions.BasePermission): class AlertPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool: def has_permission(self, r, view) -> bool:
if r.method == "GET" or r.method == "PATCH": if r.method in ("GET", "PATCH"):
if "pk" in view.kwargs.keys(): if "pk" in view.kwargs.keys():
return _has_perm(r, "can_list_alerts") and _has_perm_on_alert( return _has_perm(r, "can_list_alerts") and _has_perm_on_alert(
r.user, view.kwargs["pk"] r.user, view.kwargs["pk"]
@@ -52,5 +52,5 @@ class AlertTemplatePerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool: def has_permission(self, r, view) -> bool:
if r.method == "GET": if r.method == "GET":
return _has_perm(r, "can_list_alerttemplates") return _has_perm(r, "can_list_alerttemplates")
else:
return _has_perm(r, "can_manage_alerttemplates") return _has_perm(r, "can_manage_alerttemplates")

View File

@@ -1424,6 +1424,7 @@ class TestAlertTasks(TacticalTestCase):
"timeout": 30, "timeout": 30,
"script_args": [], "script_args": [],
"payload": {"code": failure_action.code, "shell": failure_action.shell}, "payload": {"code": failure_action.code, "shell": failure_action.shell},
"run_as_user": False,
} }
nats_cmd.assert_called_with(data, timeout=30, wait=True) nats_cmd.assert_called_with(data, timeout=30, wait=True)
@@ -1452,6 +1453,7 @@ class TestAlertTasks(TacticalTestCase):
"timeout": 35, "timeout": 35,
"script_args": ["nice_arg"], "script_args": ["nice_arg"],
"payload": {"code": resolved_action.code, "shell": resolved_action.shell}, "payload": {"code": resolved_action.code, "shell": resolved_action.shell},
"run_as_user": False,
} }
nats_cmd.assert_called_with(data, timeout=35, wait=True) nats_cmd.assert_called_with(data, timeout=35, wait=True)

View File

@@ -41,13 +41,13 @@ class GetAddAlerts(APIView):
elif any( elif any(
key key
in [ in (
"timeFilter", "timeFilter",
"clientFilter", "clientFilter",
"severityFilter", "severityFilter",
"resolvedFilter", "resolvedFilter",
"snoozedFilter", "snoozedFilter",
] )
for key in request.data.keys() for key in request.data.keys()
): ):
clientFilter = Q() clientFilter = Q()

View File

@@ -24,6 +24,7 @@ from core.utils import (
get_core_settings, get_core_settings,
get_mesh_device_id, get_mesh_device_id,
get_mesh_ws_url, get_mesh_ws_url,
get_meshagent_url,
) )
from logs.models import DebugLog, PendingAction from logs.models import DebugLog, PendingAction
from software.models import InstalledSoftware from software.models import InstalledSoftware
@@ -398,25 +399,32 @@ class MeshExe(APIView):
def post(self, request): def post(self, request):
match request.data: match request.data:
case {"goarch": GoArch.AMD64, "plat": AgentPlat.WINDOWS}: case {"goarch": GoArch.AMD64, "plat": AgentPlat.WINDOWS}:
arch = MeshAgentIdent.WIN64 ident = MeshAgentIdent.WIN64
case {"goarch": GoArch.i386, "plat": AgentPlat.WINDOWS}: case {"goarch": GoArch.i386, "plat": AgentPlat.WINDOWS}:
arch = MeshAgentIdent.WIN32 ident = MeshAgentIdent.WIN32
case {"goarch": GoArch.AMD64, "plat": AgentPlat.DARWIN} | {
"goarch": GoArch.ARM64,
"plat": AgentPlat.DARWIN,
}:
ident = MeshAgentIdent.DARWIN_UNIVERSAL
case _: case _:
return notify_error("Arch not specified") return notify_error("Arch not supported")
core = get_core_settings() core = get_core_settings()
try: try:
uri = get_mesh_ws_url() uri = get_mesh_ws_url()
mesh_id = asyncio.run(get_mesh_device_id(uri, core.mesh_device_group)) mesh_device_id: str = asyncio.run(
get_mesh_device_id(uri, core.mesh_device_group)
)
except: except:
return notify_error("Unable to connect to mesh to get group id information") return notify_error("Unable to connect to mesh to get group id information")
if settings.DOCKER_BUILD: dl_url = get_meshagent_url(
dl_url = f"{settings.MESH_WS_URL.replace('ws://', 'http://')}/meshagents?id={arch}&meshid={mesh_id}&installflags=0" ident=ident,
else: plat=request.data["plat"],
dl_url = ( mesh_site=core.mesh_site,
f"{core.mesh_site}/meshagents?id={arch}&meshid={mesh_id}&installflags=0" mesh_device_id=mesh_device_id,
) )
try: try:

View File

@@ -218,12 +218,12 @@ class Policy(BaseAuditModel):
def get_policy_tasks(agent: "Agent") -> "List[AutomatedTask]": def get_policy_tasks(agent: "Agent") -> "List[AutomatedTask]":
# List of all tasks to be applied # List of all tasks to be applied
tasks = list() tasks = []
# Get policies applied to agent and agent site and client # Get policies applied to agent and agent site and client
policies = agent.get_agent_policies() policies = agent.get_agent_policies()
processed_policies = list() processed_policies = []
for _, policy in policies.items(): for _, policy in policies.items():
if policy and policy.active and policy.pk not in processed_policies: if policy and policy.active and policy.pk not in processed_policies:
@@ -244,10 +244,10 @@ class Policy(BaseAuditModel):
# Used to hold the policies that will be applied and the order in which they are applied # Used to hold the policies that will be applied and the order in which they are applied
# Enforced policies are applied first # Enforced policies are applied first
enforced_checks = list() enforced_checks = []
policy_checks = list() policy_checks = []
processed_policies = list() processed_policies = []
for _, policy in policies.items(): for _, policy in policies.items():
if policy and policy.active and policy.pk not in processed_policies: if policy and policy.active and policy.pk not in processed_policies:
@@ -263,24 +263,24 @@ class Policy(BaseAuditModel):
return [] return []
# Sorted Checks already added # Sorted Checks already added
added_diskspace_checks: List[str] = list() added_diskspace_checks: List[str] = []
added_ping_checks: List[str] = list() added_ping_checks: List[str] = []
added_winsvc_checks: List[str] = list() added_winsvc_checks: List[str] = []
added_script_checks: List[int] = list() added_script_checks: List[int] = []
added_eventlog_checks: List[List[str]] = list() added_eventlog_checks: List[List[str]] = []
added_cpuload_checks: List[int] = list() added_cpuload_checks: List[int] = []
added_memory_checks: List[int] = list() added_memory_checks: List[int] = []
# Lists all agent and policy checks that will be returned # Lists all agent and policy checks that will be returned
diskspace_checks: "List[Check]" = list() diskspace_checks: "List[Check]" = []
ping_checks: "List[Check]" = list() ping_checks: "List[Check]" = []
winsvc_checks: "List[Check]" = list() winsvc_checks: "List[Check]" = []
script_checks: "List[Check]" = list() script_checks: "List[Check]" = []
eventlog_checks: "List[Check]" = list() eventlog_checks: "List[Check]" = []
cpuload_checks: "List[Check]" = list() cpuload_checks: "List[Check]" = []
memory_checks: "List[Check]" = list() memory_checks: "List[Check]" = []
overridden_checks: List[int] = list() overridden_checks: List[int] = []
# Loop over checks in with enforced policies first, then non-enforced policies # Loop over checks in with enforced policies first, then non-enforced policies
for check in enforced_checks + agent_checks + policy_checks: for check in enforced_checks + agent_checks + policy_checks:

View File

@@ -7,5 +7,5 @@ class AutomationPolicyPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool: def has_permission(self, r, view) -> bool:
if r.method == "GET": if r.method == "GET":
return _has_perm(r, "can_list_automation_policies") return _has_perm(r, "can_list_automation_policies")
else:
return _has_perm(r, "can_manage_automation_policies") return _has_perm(r, "can_manage_automation_policies")

View File

@@ -7,8 +7,11 @@ from rest_framework.serializers import (
from agents.serializers import AgentHostnameSerializer from agents.serializers import AgentHostnameSerializer
from autotasks.models import TaskResult from autotasks.models import TaskResult
from checks.models import CheckResult from checks.models import CheckResult
from clients.models import Client from clients.models import Client, Site
from clients.serializers import ClientMinimumSerializer, SiteMinimumSerializer from clients.serializers import (
ClientMinimumSerializer,
SiteMinimumSerializer,
)
from winupdate.serializers import WinUpdatePolicySerializer from winupdate.serializers import WinUpdatePolicySerializer
from .models import Policy from .models import Policy
@@ -85,11 +88,29 @@ class PolicyRelatedSerializer(ModelSerializer):
) )
class PolicyOverviewSiteSerializer(ModelSerializer):
workstation_policy = PolicySerializer(read_only=True)
server_policy = PolicySerializer(read_only=True)
class Meta:
model = Site
fields = ("pk", "name", "workstation_policy", "server_policy")
class PolicyOverviewSerializer(ModelSerializer): class PolicyOverviewSerializer(ModelSerializer):
sites = SerializerMethodField()
workstation_policy = PolicySerializer(read_only=True)
server_policy = PolicySerializer(read_only=True)
def get_sites(self, obj):
return PolicyOverviewSiteSerializer(
obj.filtered_sites,
many=True,
).data
class Meta: class Meta:
model = Client model = Client
fields = ("pk", "name", "sites", "workstation_policy", "server_policy") fields = ("pk", "name", "sites", "workstation_policy", "server_policy")
depth = 2
class PolicyCheckStatusSerializer(ModelSerializer): class PolicyCheckStatusSerializer(ModelSerializer):

View File

@@ -2,8 +2,9 @@ from itertools import cycle
from unittest.mock import patch from unittest.mock import patch
from model_bakery import baker, seq from model_bakery import baker, seq
from django.db.models import Prefetch
from agents.models import Agent from agents.models import Agent
from clients.models import Site
from core.utils import get_core_settings from core.utils import get_core_settings
from tacticalrmm.constants import AgentMonType, TaskSyncStatus from tacticalrmm.constants import AgentMonType, TaskSyncStatus
from tacticalrmm.test import TacticalTestCase from tacticalrmm.test import TacticalTestCase
@@ -186,7 +187,17 @@ class TestPolicyViews(TacticalTestCase):
baker.make("clients.Site", client=cycle(clients), _quantity=3) baker.make("clients.Site", client=cycle(clients), _quantity=3)
resp = self.client.get(url, format="json") resp = self.client.get(url, format="json")
clients = Client.objects.all() clients = Client.objects.select_related(
"workstation_policy", "server_policy"
).prefetch_related(
Prefetch(
"sites",
queryset=Site.objects.select_related(
"workstation_policy", "server_policy"
),
to_attr="filtered_sites",
)
)
serializer = PolicyOverviewSerializer(clients, many=True) serializer = PolicyOverviewSerializer(clients, many=True)
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)

View File

@@ -7,10 +7,11 @@ from rest_framework.views import APIView
from agents.models import Agent from agents.models import Agent
from autotasks.models import TaskResult from autotasks.models import TaskResult
from checks.models import CheckResult from checks.models import CheckResult
from clients.models import Client from clients.models import Client, Site
from tacticalrmm.permissions import _has_perm_on_client, _has_perm_on_site from tacticalrmm.permissions import _has_perm_on_client, _has_perm_on_site
from winupdate.models import WinUpdatePolicy from winupdate.models import WinUpdatePolicy
from winupdate.serializers import WinUpdatePolicySerializer from winupdate.serializers import WinUpdatePolicySerializer
from django.db.models import Prefetch
from .models import Policy from .models import Policy
from .permissions import AutomationPolicyPerms from .permissions import AutomationPolicyPerms
@@ -108,8 +109,18 @@ class PolicyCheck(APIView):
class OverviewPolicy(APIView): class OverviewPolicy(APIView):
def get(self, request): def get(self, request):
clients = Client.objects.filter_by_role(request.user).select_related( clients = (
Client.objects.filter_by_role(request.user)
.select_related("workstation_policy", "server_policy")
.prefetch_related(
Prefetch(
"sites",
queryset=Site.objects.select_related(
"workstation_policy", "server_policy" "workstation_policy", "server_policy"
),
to_attr="filtered_sites",
)
)
) )
return Response(PolicyOverviewSerializer(clients, many=True).data) return Response(PolicyOverviewSerializer(clients, many=True).data)

View File

@@ -1,6 +1,7 @@
import asyncio import asyncio
import random import random
import string import string
from contextlib import suppress
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
import pytz import pytz
@@ -262,13 +263,13 @@ class AutomatedTask(BaseAuditModel):
else True, else True,
} }
if self.task_type in [ if self.task_type in (
TaskType.RUN_ONCE, TaskType.RUN_ONCE,
TaskType.DAILY, TaskType.DAILY,
TaskType.WEEKLY, TaskType.WEEKLY,
TaskType.MONTHLY, TaskType.MONTHLY,
TaskType.MONTHLY_DOW, TaskType.MONTHLY_DOW,
]: ):
# set runonce task in future if creating and run_asap_after_missed is set # set runonce task in future if creating and run_asap_after_missed is set
if ( if (
not editing not editing
@@ -432,10 +433,8 @@ class AutomatedTask(BaseAuditModel):
if r != "ok" and "The system cannot find the file specified" not in r: if r != "ok" and "The system cannot find the file specified" not in r:
task_result.sync_status = TaskSyncStatus.PENDING_DELETION task_result.sync_status = TaskSyncStatus.PENDING_DELETION
try: with suppress(DatabaseError):
task_result.save(update_fields=["sync_status"]) task_result.save(update_fields=["sync_status"])
except DatabaseError:
pass
DebugLog.warning( DebugLog.warning(
agent=agent, agent=agent,

View File

@@ -100,13 +100,13 @@ class TaskSerializer(serializers.ModelSerializer):
# run_time_date required # run_time_date required
if ( if (
data["task_type"] data["task_type"]
in [ in (
TaskType.RUN_ONCE, TaskType.RUN_ONCE,
TaskType.DAILY, TaskType.DAILY,
TaskType.WEEKLY, TaskType.WEEKLY,
TaskType.MONTHLY, TaskType.MONTHLY,
TaskType.MONTHLY_DOW, TaskType.MONTHLY_DOW,
] )
and not data["run_time_date"] and not data["run_time_date"]
): ):
raise serializers.ValidationError( raise serializers.ValidationError(
@@ -188,7 +188,6 @@ class TaskSerializer(serializers.ModelSerializer):
if not alert_template: if not alert_template:
return None return None
else:
return { return {
"name": alert_template.name, "name": alert_template.name,
"always_email": alert_template.task_always_email, "always_email": alert_template.task_always_email,
@@ -241,6 +240,7 @@ class TaskGOGetSerializer(serializers.ModelSerializer):
), ),
"shell": script.shell, "shell": script.shell,
"timeout": action["timeout"], "timeout": action["timeout"],
"run_as_user": script.run_as_user,
} }
) )
if actions_to_remove: if actions_to_remove:

View File

@@ -1,6 +1,7 @@
import asyncio import asyncio
import datetime as dt import datetime as dt
import random import random
from contextlib import suppress
from time import sleep from time import sleep
from typing import Optional, Union from typing import Optional, Union
@@ -16,60 +17,64 @@ from tacticalrmm.constants import DebugLogType
@app.task @app.task
def create_win_task_schedule(pk: int, agent_id: Optional[str] = None) -> str: def create_win_task_schedule(pk: int, agent_id: Optional[str] = None) -> str:
try: with suppress(
AutomatedTask.DoesNotExist,
Agent.DoesNotExist,
):
task = AutomatedTask.objects.get(pk=pk) task = AutomatedTask.objects.get(pk=pk)
if agent_id: if agent_id:
task.create_task_on_agent(Agent.objects.get(agent_id=agent_id)) task.create_task_on_agent(Agent.objects.get(agent_id=agent_id))
else: else:
task.create_task_on_agent() task.create_task_on_agent()
except (AutomatedTask.DoesNotExist, Agent.DoesNotExist):
pass
return "ok" return "ok"
@app.task @app.task
def modify_win_task(pk: int, agent_id: Optional[str] = None) -> str: def modify_win_task(pk: int, agent_id: Optional[str] = None) -> str:
try: with suppress(
AutomatedTask.DoesNotExist,
Agent.DoesNotExist,
):
task = AutomatedTask.objects.get(pk=pk) task = AutomatedTask.objects.get(pk=pk)
if agent_id: if agent_id:
task.modify_task_on_agent(Agent.objects.get(agent_id=agent_id)) task.modify_task_on_agent(Agent.objects.get(agent_id=agent_id))
else: else:
task.modify_task_on_agent() task.modify_task_on_agent()
except (AutomatedTask.DoesNotExist, Agent.DoesNotExist):
pass
return "ok" return "ok"
@app.task @app.task
def delete_win_task_schedule(pk: int, agent_id: Optional[str] = None) -> str: def delete_win_task_schedule(pk: int, agent_id: Optional[str] = None) -> str:
try: with suppress(
AutomatedTask.DoesNotExist,
Agent.DoesNotExist,
):
task = AutomatedTask.objects.get(pk=pk) task = AutomatedTask.objects.get(pk=pk)
if agent_id: if agent_id:
task.delete_task_on_agent(Agent.objects.get(agent_id=agent_id)) task.delete_task_on_agent(Agent.objects.get(agent_id=agent_id))
else: else:
task.delete_task_on_agent() task.delete_task_on_agent()
except (AutomatedTask.DoesNotExist, Agent.DoesNotExist):
pass
return "ok" return "ok"
@app.task @app.task
def run_win_task(pk: int, agent_id: Optional[str] = None) -> str: def run_win_task(pk: int, agent_id: Optional[str] = None) -> str:
try: with suppress(
AutomatedTask.DoesNotExist,
Agent.DoesNotExist,
):
task = AutomatedTask.objects.get(pk=pk) task = AutomatedTask.objects.get(pk=pk)
if agent_id: if agent_id:
task.run_win_task(Agent.objects.get(agent_id=agent_id)) task.run_win_task(Agent.objects.get(agent_id=agent_id))
else: else:
task.run_win_task() task.run_win_task()
except (AutomatedTask.DoesNotExist, Agent.DoesNotExist):
pass
return "ok" return "ok"

View File

@@ -149,7 +149,7 @@ class Check(BaseAuditModel):
def __str__(self): def __str__(self):
if self.agent: if self.agent:
return f"{self.agent.hostname} - {self.readable_desc}" return f"{self.agent.hostname} - {self.readable_desc}"
else:
return f"{self.policy.name} - {self.readable_desc}" return f"{self.policy.name} - {self.readable_desc}"
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
@@ -198,10 +198,7 @@ class Check(BaseAuditModel):
return f"{display}: Drive {self.disk} - {text}" return f"{display}: Drive {self.disk} - {text}"
elif self.check_type == CheckType.PING: elif self.check_type == CheckType.PING:
return f"{display}: {self.name}" return f"{display}: {self.name}"
elif ( elif self.check_type in (CheckType.CPU_LOAD, CheckType.MEMORY):
self.check_type == CheckType.CPU_LOAD or self.check_type == CheckType.MEMORY
):
text = "" text = ""
if self.warning_threshold: if self.warning_threshold:
text += f" Warning Threshold: {self.warning_threshold}%" text += f" Warning Threshold: {self.warning_threshold}%"
@@ -215,7 +212,7 @@ class Check(BaseAuditModel):
return f"{display}: {self.name}" return f"{display}: {self.name}"
elif self.check_type == CheckType.SCRIPT: elif self.check_type == CheckType.SCRIPT:
return f"{display}: {self.script.name}" return f"{display}: {self.script.name}"
else:
return "n/a" return "n/a"
@staticmethod @staticmethod
@@ -335,12 +332,12 @@ class CheckResult(models.Model):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
# if check is a policy check clear cache on everything # if check is a policy check clear cache on everything
if not self.alert_severity and self.assigned_check.check_type in [ if not self.alert_severity and self.assigned_check.check_type in (
CheckType.MEMORY, CheckType.MEMORY,
CheckType.CPU_LOAD, CheckType.CPU_LOAD,
CheckType.DISK_SPACE, CheckType.DISK_SPACE,
CheckType.SCRIPT, CheckType.SCRIPT,
]: ):
self.alert_severity = AlertSeverity.WARNING self.alert_severity = AlertSeverity.WARNING
super(CheckResult, self).save( super(CheckResult, self).save(

View File

@@ -5,7 +5,7 @@ from tacticalrmm.permissions import _has_perm, _has_perm_on_agent
class ChecksPerms(permissions.BasePermission): class ChecksPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool: def has_permission(self, r, view) -> bool:
if r.method == "GET" or r.method == "PATCH": if r.method in ("GET", "PATCH"):
if "agent_id" in view.kwargs.keys(): if "agent_id" in view.kwargs.keys():
return _has_perm(r, "can_list_checks") and _has_perm_on_agent( return _has_perm(r, "can_list_checks") and _has_perm_on_agent(
r.user, view.kwargs["agent_id"] r.user, view.kwargs["agent_id"]

View File

@@ -43,7 +43,7 @@ class CheckSerializer(serializers.ModelSerializer):
if not alert_template: if not alert_template:
return None return None
else:
return { return {
"name": alert_template.name, "name": alert_template.name,
"always_email": alert_template.check_always_email, "always_email": alert_template.check_always_email,

View File

@@ -170,5 +170,5 @@ def run_checks(request, agent_id):
return notify_error(f"Checks are already running on {agent.hostname}") return notify_error(f"Checks are already running on {agent.hostname}")
elif r == "ok": elif r == "ok":
return Response(f"Checks will now be re-run on {agent.hostname}") return Response(f"Checks will now be re-run on {agent.hostname}")
else:
return notify_error("Unable to contact the agent") return notify_error("Unable to contact the agent")

View File

@@ -70,11 +70,11 @@ class Client(BaseAuditModel):
sites = self.sites.all() sites = self.sites.all()
if old_client.workstation_policy != self.workstation_policy: if old_client.workstation_policy != self.workstation_policy:
for site in sites: for site in sites:
cache.delete_many_pattern(f"site_workstation_{site.pk}_*") cache.delete_many_pattern(f"site_workstation_*{site.pk}_*")
if old_client.server_policy != self.server_policy: if old_client.server_policy != self.server_policy:
for site in sites: for site in sites:
cache.delete_many_pattern(f"site_server_{site.pk}_*") cache.delete_many_pattern(f"site_server_*{site.pk}_*")
class Meta: class Meta:
ordering = ("name",) ordering = ("name",)
@@ -145,10 +145,10 @@ class Site(BaseAuditModel):
cache_agents_alert_template.delay() cache_agents_alert_template.delay()
if old_site.workstation_policy != self.workstation_policy: if old_site.workstation_policy != self.workstation_policy:
cache.delete_many_pattern(f"site_workstation_{self.pk}_*") cache.delete_many_pattern(f"site_workstation_*{self.pk}_*")
if old_site.server_policy != self.server_policy: if old_site.server_policy != self.server_policy:
cache.delete_many_pattern(f"site_server_{self.pk}_*") cache.delete_many_pattern(f"site_server_*{self.pk}_*")
class Meta: class Meta:
ordering = ("name",) ordering = ("name",)
@@ -229,16 +229,16 @@ class ClientCustomField(models.Model):
return self.multiple_value return self.multiple_value
elif self.field.type == CustomFieldType.CHECKBOX: elif self.field.type == CustomFieldType.CHECKBOX:
return self.bool_value return self.bool_value
else:
return self.string_value return self.string_value
def save_to_field(self, value): def save_to_field(self, value):
if self.field.type in [ if self.field.type in (
CustomFieldType.TEXT, CustomFieldType.TEXT,
CustomFieldType.NUMBER, CustomFieldType.NUMBER,
CustomFieldType.SINGLE, CustomFieldType.SINGLE,
CustomFieldType.DATETIME, CustomFieldType.DATETIME,
]: ):
self.string_value = value self.string_value = value
self.save() self.save()
elif self.field.type == CustomFieldType.MULTIPLE: elif self.field.type == CustomFieldType.MULTIPLE:
@@ -280,16 +280,16 @@ class SiteCustomField(models.Model):
return self.multiple_value return self.multiple_value
elif self.field.type == CustomFieldType.CHECKBOX: elif self.field.type == CustomFieldType.CHECKBOX:
return self.bool_value return self.bool_value
else:
return self.string_value return self.string_value
def save_to_field(self, value): def save_to_field(self, value):
if self.field.type in [ if self.field.type in (
CustomFieldType.TEXT, CustomFieldType.TEXT,
CustomFieldType.NUMBER, CustomFieldType.NUMBER,
CustomFieldType.SINGLE, CustomFieldType.SINGLE,
CustomFieldType.DATETIME, CustomFieldType.DATETIME,
]: ):
self.string_value = value self.string_value = value
self.save() self.save()
elif self.field.type == CustomFieldType.MULTIPLE: elif self.field.type == CustomFieldType.MULTIPLE:

View File

@@ -12,7 +12,7 @@ class ClientsPerms(permissions.BasePermission):
) )
else: else:
return _has_perm(r, "can_list_clients") return _has_perm(r, "can_list_clients")
elif r.method == "PUT" or r.method == "DELETE": elif r.method in ("PUT", "DELETE"):
return _has_perm(r, "can_manage_clients") and _has_perm_on_client( return _has_perm(r, "can_manage_clients") and _has_perm_on_client(
r.user, view.kwargs["pk"] r.user, view.kwargs["pk"]
) )
@@ -29,7 +29,7 @@ class SitesPerms(permissions.BasePermission):
) )
else: else:
return _has_perm(r, "can_list_sites") return _has_perm(r, "can_list_sites")
elif r.method == "PUT" or r.method == "DELETE": elif r.method in ("PUT", "DELETE"):
return _has_perm(r, "can_manage_sites") and _has_perm_on_site( return _has_perm(r, "can_manage_sites") and _has_perm_on_site(
r.user, view.kwargs["pk"] r.user, view.kwargs["pk"]
) )
@@ -41,5 +41,5 @@ class DeploymentPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool: def has_permission(self, r, view) -> bool:
if r.method == "GET": if r.method == "GET":
return _has_perm(r, "can_list_deployments") return _has_perm(r, "can_list_deployments")
else:
return _has_perm(r, "can_manage_deployments") return _has_perm(r, "can_manage_deployments")

View File

@@ -1,6 +1,7 @@
import datetime as dt import datetime as dt
import re import re
import uuid import uuid
from contextlib import suppress
from django.db.models import Count, Exists, OuterRef, Prefetch, prefetch_related_objects from django.db.models import Count, Exists, OuterRef, Prefetch, prefetch_related_objects
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
@@ -338,10 +339,8 @@ class AgentDeployment(APIView):
if not _has_perm_on_site(request.user, d.site.pk): if not _has_perm_on_site(request.user, d.site.pk):
raise PermissionDenied() raise PermissionDenied()
try: with suppress(Exception):
d.auth_token.delete() d.auth_token.delete()
except:
pass
d.delete() d.delete()
return Response("The deployment was deleted") return Response("The deployment was deleted")

View File

@@ -67,8 +67,11 @@ RemoveOldAgent() {
InstallMesh() { InstallMesh() {
if [ -f /etc/os-release ]; then if [ -f /etc/os-release ]; then
distroID=$(. /etc/os-release; echo $ID) distroID=$(. /etc/os-release; echo $ID)
distroIDLIKE=$(. /etc/os-release; echo $ID_LIKE)
if [[ " ${deb[*]} " =~ " ${distroID} " ]]; then if [[ " ${deb[*]} " =~ " ${distroID} " ]]; then
set_locale_deb set_locale_deb
elif [[ " ${deb[*]} " =~ " ${distroIDLIKE} " ]]; then
set_locale_deb
elif [[ " ${rhe[*]} " =~ " ${distroID} " ]]; then elif [[ " ${rhe[*]} " =~ " ${distroID} " ]]; then
set_locale_rhel set_locale_rhel
else else
@@ -78,21 +81,21 @@ InstallMesh() {
meshTmpDir=$(mktemp -d -t "mesh-XXXXXXXXX") meshTmpDir=$(mktemp -d -t "mesh-XXXXXXXXX")
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
meshTmpDir='meshtemp' meshTmpDir='/root/meshtemp'
mkdir -p ${meshTmpDir} mkdir -p ${meshTmpDir}
fi fi
meshTmpBin="${meshTmpDir}/meshagent" meshTmpBin="${meshTmpDir}/meshagent"
wget --no-check-certificate -q -O ${meshTmpBin} ${meshDL} wget --no-check-certificate -q -O ${meshTmpBin} ${meshDL}
chmod +x ${meshTmpBin} chmod +x ${meshTmpBin}
mkdir -p ${meshDir} mkdir -p ${meshDir}
env LC_ALL=en_US.UTF-8 LANGUAGE=en_US ${meshTmpBin} -install --installPath=${meshDir} env LC_ALL=en_US.UTF-8 LANGUAGE=en_US XAUTHORITY=foo DISPLAY=bar ${meshTmpBin} -install --installPath=${meshDir}
sleep 1 sleep 1
rm -rf ${meshTmpDir} rm -rf ${meshTmpDir}
} }
RemoveMesh() { RemoveMesh() {
if [ -f "${meshSystemBin}" ]; then if [ -f "${meshSystemBin}" ]; then
${meshSystemBin} -uninstall env XAUTHORITY=foo DISPLAY=bar ${meshSystemBin} -uninstall
sleep 1 sleep 1
fi fi
@@ -119,6 +122,10 @@ RemoveOldAgent
echo "Downloading tactical agent..." echo "Downloading tactical agent..."
wget -q -O ${agentBin} "${agentDL}" wget -q -O ${agentBin} "${agentDL}"
if [ $? -ne 0 ]; then
echo "ERROR: Unable to download tactical agent"
exit 1
fi
chmod +x ${agentBin} chmod +x ${agentBin}
MESH_NODE_ID="" MESH_NODE_ID=""
@@ -133,7 +140,7 @@ else
InstallMesh InstallMesh
sleep 2 sleep 2
echo "Getting mesh node id..." echo "Getting mesh node id..."
MESH_NODE_ID=$(${agentBin} -m nixmeshnodeid) MESH_NODE_ID=$(env XAUTHORITY=foo DISPLAY=bar ${agentBin} -m nixmeshnodeid)
fi fi
if [ ! -d "${agentBinPath}" ]; then if [ ! -d "${agentBinPath}" ]; then

View File

@@ -1,4 +1,5 @@
import asyncio import asyncio
from contextlib import suppress
from channels.db import database_sync_to_async from channels.db import database_sync_to_async
from channels.generic.websocket import AsyncJsonWebsocketConsumer from channels.generic.websocket import AsyncJsonWebsocketConsumer
@@ -24,10 +25,8 @@ class DashInfo(AsyncJsonWebsocketConsumer):
async def disconnect(self, close_code): async def disconnect(self, close_code):
try: with suppress(Exception):
self.dash_info.cancel() self.dash_info.cancel()
except:
pass
self.connected = False self.connected = False
await self.close() await self.close()

View File

@@ -39,9 +39,8 @@ If (Get-Service $serviceName -ErrorAction SilentlyContinue) {
$DefenderStatus = Get-MpComputerStatus | select AntivirusEnabled $DefenderStatus = Get-MpComputerStatus | select AntivirusEnabled
if ($DefenderStatus -match "True") { if ($DefenderStatus -match "True") {
Add-MpPreference -ExclusionPath 'C:\Program Files\TacticalAgent\*' Add-MpPreference -ExclusionPath 'C:\Program Files\TacticalAgent\*'
Add-MpPreference -ExclusionPath 'C:\Windows\Temp\winagent-v*.exe'
Add-MpPreference -ExclusionPath 'C:\Program Files\Mesh Agent\*' Add-MpPreference -ExclusionPath 'C:\Program Files\Mesh Agent\*'
Add-MpPreference -ExclusionPath 'C:\Windows\Temp\trmm*\*' Add-MpPreference -ExclusionPath 'C:\ProgramData\TacticalRMM\*'
} }
} }
Catch { Catch {

View File

@@ -0,0 +1,8 @@
#!/bin/bash
/usr/local/mesh_services/meshagent/meshagent -fulluninstall
launchctl bootout system /Library/LaunchDaemons/tacticalagent.plist
rm -rf /usr/local/mesh_services
rm -f /etc/tacticalagent
rm -rf /opt/tacticalagent
rm -f /Library/LaunchDaemons/tacticalagent.plist

View File

@@ -0,0 +1,60 @@
import configparser
import os
from pathlib import Path
from django.conf import settings
from django.core.management.base import BaseCommand
class Command(BaseCommand):
help = "Generate conf for uwsgi"
def handle(self, *args, **kwargs):
self.stdout.write("Creating uwsgi conf...")
config = configparser.ConfigParser()
if getattr(settings, "DOCKER_BUILD", False):
home = str(Path(os.getenv("VIRTUAL_ENV"))) # type: ignore
socket = "0.0.0.0:8080"
else:
home = str(settings.BASE_DIR.parents[0] / "env")
socket = str(settings.BASE_DIR / "tacticalrmm.sock")
config["uwsgi"] = {
"chdir": str(settings.BASE_DIR),
"module": "tacticalrmm.wsgi",
"home": home,
"master": str(getattr(settings, "UWSGI_MASTER", True)).lower(),
"enable-threads": str(
getattr(settings, "UWSGI_ENABLE_THREADS", True)
).lower(),
"socket": socket,
"harakiri": str(getattr(settings, "UWSGI_HARAKIRI", 300)),
"chmod-socket": str(getattr(settings, "UWSGI_CHMOD_SOCKET", 660)),
"buffer-size": str(getattr(settings, "UWSGI_BUFFER_SIZE", 65535)),
"vacuum": str(getattr(settings, "UWSGI_VACUUM", True)).lower(),
"die-on-term": str(getattr(settings, "UWSGI_DIE_ON_TERM", True)).lower(),
"max-requests": str(getattr(settings, "UWSGI_MAX_REQUESTS", 500)),
"disable-logging": str(
getattr(settings, "UWSGI_DISABLE_LOGGING", True)
).lower(),
"cheaper-algo": "busyness",
"cheaper": str(getattr(settings, "UWSGI_CHEAPER", 4)),
"cheaper-initial": str(getattr(settings, "UWSGI_CHEAPER_INITIAL", 4)),
"workers": str(getattr(settings, "UWSGI_MAX_WORKERS", 40)),
"cheaper-step": str(getattr(settings, "UWSGI_CHEAPER_STEP", 2)),
"cheaper-overload": str(getattr(settings, "UWSGI_CHEAPER_OVERLOAD", 3)),
"cheaper-busyness-min": str(getattr(settings, "UWSGI_BUSYNESS_MIN", 5)),
"cheaper-busyness-max": str(getattr(settings, "UWSGI_BUSYNESS_MAX", 10)),
}
if getattr(settings, "UWSGI_DEBUG", False):
config["uwsgi"]["stats"] = "/tmp/stats.socket"
config["uwsgi"]["cheaper-busyness-verbose"] = str(True).lower()
with open(settings.BASE_DIR / "app.ini", "w") as fp:
config.write(fp)
self.stdout.write("Created uwsgi conf")

View File

@@ -1,3 +1,5 @@
from contextlib import suppress
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
@@ -8,9 +10,7 @@ class Command(BaseCommand):
help = "Populates the global site settings on first install" help = "Populates the global site settings on first install"
def handle(self, *args, **kwargs): def handle(self, *args, **kwargs):
try: # can only be 1 instance of this. Prevents error when rebuilding docker container
with suppress(ValidationError):
CoreSettings().save() CoreSettings().save()
self.stdout.write("Core db populated") self.stdout.write("Core db populated")
except ValidationError:
# can only be 1 instance of this. Prevents error when rebuilding docker container
pass

View File

@@ -7,6 +7,6 @@ class Command(BaseCommand):
help = "Collection of tasks to run after updating the rmm, before migrations" help = "Collection of tasks to run after updating the rmm, before migrations"
def handle(self, *args, **kwargs): def handle(self, *args, **kwargs):
self.stdout.write(self.style.WARNING("Clearning the cache")) self.stdout.write(self.style.WARNING("Cleaning the cache"))
clear_entire_cache() clear_entire_cache()
self.stdout.write(self.style.SUCCESS("Cache was cleared!")) self.stdout.write(self.style.SUCCESS("Cache was cleared!"))

View File

@@ -0,0 +1,630 @@
# Generated by Django 4.1 on 2022-08-24 07:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0034_alter_customfield_name"),
]
operations = [
migrations.AlterField(
model_name="coresettings",
name="default_time_zone",
field=models.CharField(
choices=[
("Africa/Abidjan", "Africa/Abidjan"),
("Africa/Accra", "Africa/Accra"),
("Africa/Addis_Ababa", "Africa/Addis_Ababa"),
("Africa/Algiers", "Africa/Algiers"),
("Africa/Asmara", "Africa/Asmara"),
("Africa/Asmera", "Africa/Asmera"),
("Africa/Bamako", "Africa/Bamako"),
("Africa/Bangui", "Africa/Bangui"),
("Africa/Banjul", "Africa/Banjul"),
("Africa/Bissau", "Africa/Bissau"),
("Africa/Blantyre", "Africa/Blantyre"),
("Africa/Brazzaville", "Africa/Brazzaville"),
("Africa/Bujumbura", "Africa/Bujumbura"),
("Africa/Cairo", "Africa/Cairo"),
("Africa/Casablanca", "Africa/Casablanca"),
("Africa/Ceuta", "Africa/Ceuta"),
("Africa/Conakry", "Africa/Conakry"),
("Africa/Dakar", "Africa/Dakar"),
("Africa/Dar_es_Salaam", "Africa/Dar_es_Salaam"),
("Africa/Djibouti", "Africa/Djibouti"),
("Africa/Douala", "Africa/Douala"),
("Africa/El_Aaiun", "Africa/El_Aaiun"),
("Africa/Freetown", "Africa/Freetown"),
("Africa/Gaborone", "Africa/Gaborone"),
("Africa/Harare", "Africa/Harare"),
("Africa/Johannesburg", "Africa/Johannesburg"),
("Africa/Juba", "Africa/Juba"),
("Africa/Kampala", "Africa/Kampala"),
("Africa/Khartoum", "Africa/Khartoum"),
("Africa/Kigali", "Africa/Kigali"),
("Africa/Kinshasa", "Africa/Kinshasa"),
("Africa/Lagos", "Africa/Lagos"),
("Africa/Libreville", "Africa/Libreville"),
("Africa/Lome", "Africa/Lome"),
("Africa/Luanda", "Africa/Luanda"),
("Africa/Lubumbashi", "Africa/Lubumbashi"),
("Africa/Lusaka", "Africa/Lusaka"),
("Africa/Malabo", "Africa/Malabo"),
("Africa/Maputo", "Africa/Maputo"),
("Africa/Maseru", "Africa/Maseru"),
("Africa/Mbabane", "Africa/Mbabane"),
("Africa/Mogadishu", "Africa/Mogadishu"),
("Africa/Monrovia", "Africa/Monrovia"),
("Africa/Nairobi", "Africa/Nairobi"),
("Africa/Ndjamena", "Africa/Ndjamena"),
("Africa/Niamey", "Africa/Niamey"),
("Africa/Nouakchott", "Africa/Nouakchott"),
("Africa/Ouagadougou", "Africa/Ouagadougou"),
("Africa/Porto-Novo", "Africa/Porto-Novo"),
("Africa/Sao_Tome", "Africa/Sao_Tome"),
("Africa/Timbuktu", "Africa/Timbuktu"),
("Africa/Tripoli", "Africa/Tripoli"),
("Africa/Tunis", "Africa/Tunis"),
("Africa/Windhoek", "Africa/Windhoek"),
("America/Adak", "America/Adak"),
("America/Anchorage", "America/Anchorage"),
("America/Anguilla", "America/Anguilla"),
("America/Antigua", "America/Antigua"),
("America/Araguaina", "America/Araguaina"),
(
"America/Argentina/Buenos_Aires",
"America/Argentina/Buenos_Aires",
),
("America/Argentina/Catamarca", "America/Argentina/Catamarca"),
(
"America/Argentina/ComodRivadavia",
"America/Argentina/ComodRivadavia",
),
("America/Argentina/Cordoba", "America/Argentina/Cordoba"),
("America/Argentina/Jujuy", "America/Argentina/Jujuy"),
("America/Argentina/La_Rioja", "America/Argentina/La_Rioja"),
("America/Argentina/Mendoza", "America/Argentina/Mendoza"),
(
"America/Argentina/Rio_Gallegos",
"America/Argentina/Rio_Gallegos",
),
("America/Argentina/Salta", "America/Argentina/Salta"),
("America/Argentina/San_Juan", "America/Argentina/San_Juan"),
("America/Argentina/San_Luis", "America/Argentina/San_Luis"),
("America/Argentina/Tucuman", "America/Argentina/Tucuman"),
("America/Argentina/Ushuaia", "America/Argentina/Ushuaia"),
("America/Aruba", "America/Aruba"),
("America/Asuncion", "America/Asuncion"),
("America/Atikokan", "America/Atikokan"),
("America/Atka", "America/Atka"),
("America/Bahia", "America/Bahia"),
("America/Bahia_Banderas", "America/Bahia_Banderas"),
("America/Barbados", "America/Barbados"),
("America/Belem", "America/Belem"),
("America/Belize", "America/Belize"),
("America/Blanc-Sablon", "America/Blanc-Sablon"),
("America/Boa_Vista", "America/Boa_Vista"),
("America/Bogota", "America/Bogota"),
("America/Boise", "America/Boise"),
("America/Buenos_Aires", "America/Buenos_Aires"),
("America/Cambridge_Bay", "America/Cambridge_Bay"),
("America/Campo_Grande", "America/Campo_Grande"),
("America/Cancun", "America/Cancun"),
("America/Caracas", "America/Caracas"),
("America/Catamarca", "America/Catamarca"),
("America/Cayenne", "America/Cayenne"),
("America/Cayman", "America/Cayman"),
("America/Chicago", "America/Chicago"),
("America/Chihuahua", "America/Chihuahua"),
("America/Coral_Harbour", "America/Coral_Harbour"),
("America/Cordoba", "America/Cordoba"),
("America/Costa_Rica", "America/Costa_Rica"),
("America/Creston", "America/Creston"),
("America/Cuiaba", "America/Cuiaba"),
("America/Curacao", "America/Curacao"),
("America/Danmarkshavn", "America/Danmarkshavn"),
("America/Dawson", "America/Dawson"),
("America/Dawson_Creek", "America/Dawson_Creek"),
("America/Denver", "America/Denver"),
("America/Detroit", "America/Detroit"),
("America/Dominica", "America/Dominica"),
("America/Edmonton", "America/Edmonton"),
("America/Eirunepe", "America/Eirunepe"),
("America/El_Salvador", "America/El_Salvador"),
("America/Ensenada", "America/Ensenada"),
("America/Fort_Nelson", "America/Fort_Nelson"),
("America/Fort_Wayne", "America/Fort_Wayne"),
("America/Fortaleza", "America/Fortaleza"),
("America/Glace_Bay", "America/Glace_Bay"),
("America/Godthab", "America/Godthab"),
("America/Goose_Bay", "America/Goose_Bay"),
("America/Grand_Turk", "America/Grand_Turk"),
("America/Grenada", "America/Grenada"),
("America/Guadeloupe", "America/Guadeloupe"),
("America/Guatemala", "America/Guatemala"),
("America/Guayaquil", "America/Guayaquil"),
("America/Guyana", "America/Guyana"),
("America/Halifax", "America/Halifax"),
("America/Havana", "America/Havana"),
("America/Hermosillo", "America/Hermosillo"),
("America/Indiana/Indianapolis", "America/Indiana/Indianapolis"),
("America/Indiana/Knox", "America/Indiana/Knox"),
("America/Indiana/Marengo", "America/Indiana/Marengo"),
("America/Indiana/Petersburg", "America/Indiana/Petersburg"),
("America/Indiana/Tell_City", "America/Indiana/Tell_City"),
("America/Indiana/Vevay", "America/Indiana/Vevay"),
("America/Indiana/Vincennes", "America/Indiana/Vincennes"),
("America/Indiana/Winamac", "America/Indiana/Winamac"),
("America/Indianapolis", "America/Indianapolis"),
("America/Inuvik", "America/Inuvik"),
("America/Iqaluit", "America/Iqaluit"),
("America/Jamaica", "America/Jamaica"),
("America/Jujuy", "America/Jujuy"),
("America/Juneau", "America/Juneau"),
("America/Kentucky/Louisville", "America/Kentucky/Louisville"),
("America/Kentucky/Monticello", "America/Kentucky/Monticello"),
("America/Knox_IN", "America/Knox_IN"),
("America/Kralendijk", "America/Kralendijk"),
("America/La_Paz", "America/La_Paz"),
("America/Lima", "America/Lima"),
("America/Los_Angeles", "America/Los_Angeles"),
("America/Louisville", "America/Louisville"),
("America/Lower_Princes", "America/Lower_Princes"),
("America/Maceio", "America/Maceio"),
("America/Managua", "America/Managua"),
("America/Manaus", "America/Manaus"),
("America/Marigot", "America/Marigot"),
("America/Martinique", "America/Martinique"),
("America/Matamoros", "America/Matamoros"),
("America/Mazatlan", "America/Mazatlan"),
("America/Mendoza", "America/Mendoza"),
("America/Menominee", "America/Menominee"),
("America/Merida", "America/Merida"),
("America/Metlakatla", "America/Metlakatla"),
("America/Mexico_City", "America/Mexico_City"),
("America/Miquelon", "America/Miquelon"),
("America/Moncton", "America/Moncton"),
("America/Monterrey", "America/Monterrey"),
("America/Montevideo", "America/Montevideo"),
("America/Montreal", "America/Montreal"),
("America/Montserrat", "America/Montserrat"),
("America/Nassau", "America/Nassau"),
("America/New_York", "America/New_York"),
("America/Nipigon", "America/Nipigon"),
("America/Nome", "America/Nome"),
("America/Noronha", "America/Noronha"),
("America/North_Dakota/Beulah", "America/North_Dakota/Beulah"),
("America/North_Dakota/Center", "America/North_Dakota/Center"),
(
"America/North_Dakota/New_Salem",
"America/North_Dakota/New_Salem",
),
("America/Nuuk", "America/Nuuk"),
("America/Ojinaga", "America/Ojinaga"),
("America/Panama", "America/Panama"),
("America/Pangnirtung", "America/Pangnirtung"),
("America/Paramaribo", "America/Paramaribo"),
("America/Phoenix", "America/Phoenix"),
("America/Port-au-Prince", "America/Port-au-Prince"),
("America/Port_of_Spain", "America/Port_of_Spain"),
("America/Porto_Acre", "America/Porto_Acre"),
("America/Porto_Velho", "America/Porto_Velho"),
("America/Puerto_Rico", "America/Puerto_Rico"),
("America/Punta_Arenas", "America/Punta_Arenas"),
("America/Rainy_River", "America/Rainy_River"),
("America/Rankin_Inlet", "America/Rankin_Inlet"),
("America/Recife", "America/Recife"),
("America/Regina", "America/Regina"),
("America/Resolute", "America/Resolute"),
("America/Rio_Branco", "America/Rio_Branco"),
("America/Rosario", "America/Rosario"),
("America/Santa_Isabel", "America/Santa_Isabel"),
("America/Santarem", "America/Santarem"),
("America/Santiago", "America/Santiago"),
("America/Santo_Domingo", "America/Santo_Domingo"),
("America/Sao_Paulo", "America/Sao_Paulo"),
("America/Scoresbysund", "America/Scoresbysund"),
("America/Shiprock", "America/Shiprock"),
("America/Sitka", "America/Sitka"),
("America/St_Barthelemy", "America/St_Barthelemy"),
("America/St_Johns", "America/St_Johns"),
("America/St_Kitts", "America/St_Kitts"),
("America/St_Lucia", "America/St_Lucia"),
("America/St_Thomas", "America/St_Thomas"),
("America/St_Vincent", "America/St_Vincent"),
("America/Swift_Current", "America/Swift_Current"),
("America/Tegucigalpa", "America/Tegucigalpa"),
("America/Thule", "America/Thule"),
("America/Thunder_Bay", "America/Thunder_Bay"),
("America/Tijuana", "America/Tijuana"),
("America/Toronto", "America/Toronto"),
("America/Tortola", "America/Tortola"),
("America/Vancouver", "America/Vancouver"),
("America/Virgin", "America/Virgin"),
("America/Whitehorse", "America/Whitehorse"),
("America/Winnipeg", "America/Winnipeg"),
("America/Yakutat", "America/Yakutat"),
("America/Yellowknife", "America/Yellowknife"),
("Antarctica/Casey", "Antarctica/Casey"),
("Antarctica/Davis", "Antarctica/Davis"),
("Antarctica/DumontDUrville", "Antarctica/DumontDUrville"),
("Antarctica/Macquarie", "Antarctica/Macquarie"),
("Antarctica/Mawson", "Antarctica/Mawson"),
("Antarctica/McMurdo", "Antarctica/McMurdo"),
("Antarctica/Palmer", "Antarctica/Palmer"),
("Antarctica/Rothera", "Antarctica/Rothera"),
("Antarctica/South_Pole", "Antarctica/South_Pole"),
("Antarctica/Syowa", "Antarctica/Syowa"),
("Antarctica/Troll", "Antarctica/Troll"),
("Antarctica/Vostok", "Antarctica/Vostok"),
("Arctic/Longyearbyen", "Arctic/Longyearbyen"),
("Asia/Aden", "Asia/Aden"),
("Asia/Almaty", "Asia/Almaty"),
("Asia/Amman", "Asia/Amman"),
("Asia/Anadyr", "Asia/Anadyr"),
("Asia/Aqtau", "Asia/Aqtau"),
("Asia/Aqtobe", "Asia/Aqtobe"),
("Asia/Ashgabat", "Asia/Ashgabat"),
("Asia/Ashkhabad", "Asia/Ashkhabad"),
("Asia/Atyrau", "Asia/Atyrau"),
("Asia/Baghdad", "Asia/Baghdad"),
("Asia/Bahrain", "Asia/Bahrain"),
("Asia/Baku", "Asia/Baku"),
("Asia/Bangkok", "Asia/Bangkok"),
("Asia/Barnaul", "Asia/Barnaul"),
("Asia/Beirut", "Asia/Beirut"),
("Asia/Bishkek", "Asia/Bishkek"),
("Asia/Brunei", "Asia/Brunei"),
("Asia/Calcutta", "Asia/Calcutta"),
("Asia/Chita", "Asia/Chita"),
("Asia/Choibalsan", "Asia/Choibalsan"),
("Asia/Chongqing", "Asia/Chongqing"),
("Asia/Chungking", "Asia/Chungking"),
("Asia/Colombo", "Asia/Colombo"),
("Asia/Dacca", "Asia/Dacca"),
("Asia/Damascus", "Asia/Damascus"),
("Asia/Dhaka", "Asia/Dhaka"),
("Asia/Dili", "Asia/Dili"),
("Asia/Dubai", "Asia/Dubai"),
("Asia/Dushanbe", "Asia/Dushanbe"),
("Asia/Famagusta", "Asia/Famagusta"),
("Asia/Gaza", "Asia/Gaza"),
("Asia/Harbin", "Asia/Harbin"),
("Asia/Hebron", "Asia/Hebron"),
("Asia/Ho_Chi_Minh", "Asia/Ho_Chi_Minh"),
("Asia/Hong_Kong", "Asia/Hong_Kong"),
("Asia/Hovd", "Asia/Hovd"),
("Asia/Irkutsk", "Asia/Irkutsk"),
("Asia/Istanbul", "Asia/Istanbul"),
("Asia/Jakarta", "Asia/Jakarta"),
("Asia/Jayapura", "Asia/Jayapura"),
("Asia/Jerusalem", "Asia/Jerusalem"),
("Asia/Kabul", "Asia/Kabul"),
("Asia/Kamchatka", "Asia/Kamchatka"),
("Asia/Karachi", "Asia/Karachi"),
("Asia/Kashgar", "Asia/Kashgar"),
("Asia/Kathmandu", "Asia/Kathmandu"),
("Asia/Katmandu", "Asia/Katmandu"),
("Asia/Khandyga", "Asia/Khandyga"),
("Asia/Kolkata", "Asia/Kolkata"),
("Asia/Krasnoyarsk", "Asia/Krasnoyarsk"),
("Asia/Kuala_Lumpur", "Asia/Kuala_Lumpur"),
("Asia/Kuching", "Asia/Kuching"),
("Asia/Kuwait", "Asia/Kuwait"),
("Asia/Macao", "Asia/Macao"),
("Asia/Macau", "Asia/Macau"),
("Asia/Magadan", "Asia/Magadan"),
("Asia/Makassar", "Asia/Makassar"),
("Asia/Manila", "Asia/Manila"),
("Asia/Muscat", "Asia/Muscat"),
("Asia/Nicosia", "Asia/Nicosia"),
("Asia/Novokuznetsk", "Asia/Novokuznetsk"),
("Asia/Novosibirsk", "Asia/Novosibirsk"),
("Asia/Omsk", "Asia/Omsk"),
("Asia/Oral", "Asia/Oral"),
("Asia/Phnom_Penh", "Asia/Phnom_Penh"),
("Asia/Pontianak", "Asia/Pontianak"),
("Asia/Pyongyang", "Asia/Pyongyang"),
("Asia/Qatar", "Asia/Qatar"),
("Asia/Qostanay", "Asia/Qostanay"),
("Asia/Qyzylorda", "Asia/Qyzylorda"),
("Asia/Rangoon", "Asia/Rangoon"),
("Asia/Riyadh", "Asia/Riyadh"),
("Asia/Saigon", "Asia/Saigon"),
("Asia/Sakhalin", "Asia/Sakhalin"),
("Asia/Samarkand", "Asia/Samarkand"),
("Asia/Seoul", "Asia/Seoul"),
("Asia/Shanghai", "Asia/Shanghai"),
("Asia/Singapore", "Asia/Singapore"),
("Asia/Srednekolymsk", "Asia/Srednekolymsk"),
("Asia/Taipei", "Asia/Taipei"),
("Asia/Tashkent", "Asia/Tashkent"),
("Asia/Tbilisi", "Asia/Tbilisi"),
("Asia/Tehran", "Asia/Tehran"),
("Asia/Tel_Aviv", "Asia/Tel_Aviv"),
("Asia/Thimbu", "Asia/Thimbu"),
("Asia/Thimphu", "Asia/Thimphu"),
("Asia/Tokyo", "Asia/Tokyo"),
("Asia/Tomsk", "Asia/Tomsk"),
("Asia/Ujung_Pandang", "Asia/Ujung_Pandang"),
("Asia/Ulaanbaatar", "Asia/Ulaanbaatar"),
("Asia/Ulan_Bator", "Asia/Ulan_Bator"),
("Asia/Urumqi", "Asia/Urumqi"),
("Asia/Ust-Nera", "Asia/Ust-Nera"),
("Asia/Vientiane", "Asia/Vientiane"),
("Asia/Vladivostok", "Asia/Vladivostok"),
("Asia/Yakutsk", "Asia/Yakutsk"),
("Asia/Yangon", "Asia/Yangon"),
("Asia/Yekaterinburg", "Asia/Yekaterinburg"),
("Asia/Yerevan", "Asia/Yerevan"),
("Atlantic/Azores", "Atlantic/Azores"),
("Atlantic/Bermuda", "Atlantic/Bermuda"),
("Atlantic/Canary", "Atlantic/Canary"),
("Atlantic/Cape_Verde", "Atlantic/Cape_Verde"),
("Atlantic/Faeroe", "Atlantic/Faeroe"),
("Atlantic/Faroe", "Atlantic/Faroe"),
("Atlantic/Jan_Mayen", "Atlantic/Jan_Mayen"),
("Atlantic/Madeira", "Atlantic/Madeira"),
("Atlantic/Reykjavik", "Atlantic/Reykjavik"),
("Atlantic/South_Georgia", "Atlantic/South_Georgia"),
("Atlantic/St_Helena", "Atlantic/St_Helena"),
("Atlantic/Stanley", "Atlantic/Stanley"),
("Australia/ACT", "Australia/ACT"),
("Australia/Adelaide", "Australia/Adelaide"),
("Australia/Brisbane", "Australia/Brisbane"),
("Australia/Broken_Hill", "Australia/Broken_Hill"),
("Australia/Canberra", "Australia/Canberra"),
("Australia/Currie", "Australia/Currie"),
("Australia/Darwin", "Australia/Darwin"),
("Australia/Eucla", "Australia/Eucla"),
("Australia/Hobart", "Australia/Hobart"),
("Australia/LHI", "Australia/LHI"),
("Australia/Lindeman", "Australia/Lindeman"),
("Australia/Lord_Howe", "Australia/Lord_Howe"),
("Australia/Melbourne", "Australia/Melbourne"),
("Australia/NSW", "Australia/NSW"),
("Australia/North", "Australia/North"),
("Australia/Perth", "Australia/Perth"),
("Australia/Queensland", "Australia/Queensland"),
("Australia/South", "Australia/South"),
("Australia/Sydney", "Australia/Sydney"),
("Australia/Tasmania", "Australia/Tasmania"),
("Australia/Victoria", "Australia/Victoria"),
("Australia/West", "Australia/West"),
("Australia/Yancowinna", "Australia/Yancowinna"),
("Brazil/Acre", "Brazil/Acre"),
("Brazil/DeNoronha", "Brazil/DeNoronha"),
("Brazil/East", "Brazil/East"),
("Brazil/West", "Brazil/West"),
("CET", "CET"),
("CST6CDT", "CST6CDT"),
("Canada/Atlantic", "Canada/Atlantic"),
("Canada/Central", "Canada/Central"),
("Canada/Eastern", "Canada/Eastern"),
("Canada/Mountain", "Canada/Mountain"),
("Canada/Newfoundland", "Canada/Newfoundland"),
("Canada/Pacific", "Canada/Pacific"),
("Canada/Saskatchewan", "Canada/Saskatchewan"),
("Canada/Yukon", "Canada/Yukon"),
("Chile/Continental", "Chile/Continental"),
("Chile/EasterIsland", "Chile/EasterIsland"),
("Cuba", "Cuba"),
("EET", "EET"),
("EST", "EST"),
("EST5EDT", "EST5EDT"),
("Egypt", "Egypt"),
("Eire", "Eire"),
("Etc/GMT", "Etc/GMT"),
("Etc/GMT+0", "Etc/GMT+0"),
("Etc/GMT+1", "Etc/GMT+1"),
("Etc/GMT+10", "Etc/GMT+10"),
("Etc/GMT+11", "Etc/GMT+11"),
("Etc/GMT+12", "Etc/GMT+12"),
("Etc/GMT+2", "Etc/GMT+2"),
("Etc/GMT+3", "Etc/GMT+3"),
("Etc/GMT+4", "Etc/GMT+4"),
("Etc/GMT+5", "Etc/GMT+5"),
("Etc/GMT+6", "Etc/GMT+6"),
("Etc/GMT+7", "Etc/GMT+7"),
("Etc/GMT+8", "Etc/GMT+8"),
("Etc/GMT+9", "Etc/GMT+9"),
("Etc/GMT-0", "Etc/GMT-0"),
("Etc/GMT-1", "Etc/GMT-1"),
("Etc/GMT-10", "Etc/GMT-10"),
("Etc/GMT-11", "Etc/GMT-11"),
("Etc/GMT-12", "Etc/GMT-12"),
("Etc/GMT-13", "Etc/GMT-13"),
("Etc/GMT-14", "Etc/GMT-14"),
("Etc/GMT-2", "Etc/GMT-2"),
("Etc/GMT-3", "Etc/GMT-3"),
("Etc/GMT-4", "Etc/GMT-4"),
("Etc/GMT-5", "Etc/GMT-5"),
("Etc/GMT-6", "Etc/GMT-6"),
("Etc/GMT-7", "Etc/GMT-7"),
("Etc/GMT-8", "Etc/GMT-8"),
("Etc/GMT-9", "Etc/GMT-9"),
("Etc/GMT0", "Etc/GMT0"),
("Etc/Greenwich", "Etc/Greenwich"),
("Etc/UCT", "Etc/UCT"),
("Etc/UTC", "Etc/UTC"),
("Etc/Universal", "Etc/Universal"),
("Etc/Zulu", "Etc/Zulu"),
("Europe/Amsterdam", "Europe/Amsterdam"),
("Europe/Andorra", "Europe/Andorra"),
("Europe/Astrakhan", "Europe/Astrakhan"),
("Europe/Athens", "Europe/Athens"),
("Europe/Belfast", "Europe/Belfast"),
("Europe/Belgrade", "Europe/Belgrade"),
("Europe/Berlin", "Europe/Berlin"),
("Europe/Bratislava", "Europe/Bratislava"),
("Europe/Brussels", "Europe/Brussels"),
("Europe/Bucharest", "Europe/Bucharest"),
("Europe/Budapest", "Europe/Budapest"),
("Europe/Busingen", "Europe/Busingen"),
("Europe/Chisinau", "Europe/Chisinau"),
("Europe/Copenhagen", "Europe/Copenhagen"),
("Europe/Dublin", "Europe/Dublin"),
("Europe/Gibraltar", "Europe/Gibraltar"),
("Europe/Guernsey", "Europe/Guernsey"),
("Europe/Helsinki", "Europe/Helsinki"),
("Europe/Isle_of_Man", "Europe/Isle_of_Man"),
("Europe/Istanbul", "Europe/Istanbul"),
("Europe/Jersey", "Europe/Jersey"),
("Europe/Kaliningrad", "Europe/Kaliningrad"),
("Europe/Kiev", "Europe/Kiev"),
("Europe/Kirov", "Europe/Kirov"),
("Europe/Kyiv", "Europe/Kyiv"),
("Europe/Lisbon", "Europe/Lisbon"),
("Europe/Ljubljana", "Europe/Ljubljana"),
("Europe/London", "Europe/London"),
("Europe/Luxembourg", "Europe/Luxembourg"),
("Europe/Madrid", "Europe/Madrid"),
("Europe/Malta", "Europe/Malta"),
("Europe/Mariehamn", "Europe/Mariehamn"),
("Europe/Minsk", "Europe/Minsk"),
("Europe/Monaco", "Europe/Monaco"),
("Europe/Moscow", "Europe/Moscow"),
("Europe/Nicosia", "Europe/Nicosia"),
("Europe/Oslo", "Europe/Oslo"),
("Europe/Paris", "Europe/Paris"),
("Europe/Podgorica", "Europe/Podgorica"),
("Europe/Prague", "Europe/Prague"),
("Europe/Riga", "Europe/Riga"),
("Europe/Rome", "Europe/Rome"),
("Europe/Samara", "Europe/Samara"),
("Europe/San_Marino", "Europe/San_Marino"),
("Europe/Sarajevo", "Europe/Sarajevo"),
("Europe/Saratov", "Europe/Saratov"),
("Europe/Simferopol", "Europe/Simferopol"),
("Europe/Skopje", "Europe/Skopje"),
("Europe/Sofia", "Europe/Sofia"),
("Europe/Stockholm", "Europe/Stockholm"),
("Europe/Tallinn", "Europe/Tallinn"),
("Europe/Tirane", "Europe/Tirane"),
("Europe/Tiraspol", "Europe/Tiraspol"),
("Europe/Ulyanovsk", "Europe/Ulyanovsk"),
("Europe/Uzhgorod", "Europe/Uzhgorod"),
("Europe/Vaduz", "Europe/Vaduz"),
("Europe/Vatican", "Europe/Vatican"),
("Europe/Vienna", "Europe/Vienna"),
("Europe/Vilnius", "Europe/Vilnius"),
("Europe/Volgograd", "Europe/Volgograd"),
("Europe/Warsaw", "Europe/Warsaw"),
("Europe/Zagreb", "Europe/Zagreb"),
("Europe/Zaporozhye", "Europe/Zaporozhye"),
("Europe/Zurich", "Europe/Zurich"),
("GB", "GB"),
("GB-Eire", "GB-Eire"),
("GMT", "GMT"),
("GMT+0", "GMT+0"),
("GMT-0", "GMT-0"),
("GMT0", "GMT0"),
("Greenwich", "Greenwich"),
("HST", "HST"),
("Hongkong", "Hongkong"),
("Iceland", "Iceland"),
("Indian/Antananarivo", "Indian/Antananarivo"),
("Indian/Chagos", "Indian/Chagos"),
("Indian/Christmas", "Indian/Christmas"),
("Indian/Cocos", "Indian/Cocos"),
("Indian/Comoro", "Indian/Comoro"),
("Indian/Kerguelen", "Indian/Kerguelen"),
("Indian/Mahe", "Indian/Mahe"),
("Indian/Maldives", "Indian/Maldives"),
("Indian/Mauritius", "Indian/Mauritius"),
("Indian/Mayotte", "Indian/Mayotte"),
("Indian/Reunion", "Indian/Reunion"),
("Iran", "Iran"),
("Israel", "Israel"),
("Jamaica", "Jamaica"),
("Japan", "Japan"),
("Kwajalein", "Kwajalein"),
("Libya", "Libya"),
("MET", "MET"),
("MST", "MST"),
("MST7MDT", "MST7MDT"),
("Mexico/BajaNorte", "Mexico/BajaNorte"),
("Mexico/BajaSur", "Mexico/BajaSur"),
("Mexico/General", "Mexico/General"),
("NZ", "NZ"),
("NZ-CHAT", "NZ-CHAT"),
("Navajo", "Navajo"),
("PRC", "PRC"),
("PST8PDT", "PST8PDT"),
("Pacific/Apia", "Pacific/Apia"),
("Pacific/Auckland", "Pacific/Auckland"),
("Pacific/Bougainville", "Pacific/Bougainville"),
("Pacific/Chatham", "Pacific/Chatham"),
("Pacific/Chuuk", "Pacific/Chuuk"),
("Pacific/Easter", "Pacific/Easter"),
("Pacific/Efate", "Pacific/Efate"),
("Pacific/Enderbury", "Pacific/Enderbury"),
("Pacific/Fakaofo", "Pacific/Fakaofo"),
("Pacific/Fiji", "Pacific/Fiji"),
("Pacific/Funafuti", "Pacific/Funafuti"),
("Pacific/Galapagos", "Pacific/Galapagos"),
("Pacific/Gambier", "Pacific/Gambier"),
("Pacific/Guadalcanal", "Pacific/Guadalcanal"),
("Pacific/Guam", "Pacific/Guam"),
("Pacific/Honolulu", "Pacific/Honolulu"),
("Pacific/Johnston", "Pacific/Johnston"),
("Pacific/Kanton", "Pacific/Kanton"),
("Pacific/Kiritimati", "Pacific/Kiritimati"),
("Pacific/Kosrae", "Pacific/Kosrae"),
("Pacific/Kwajalein", "Pacific/Kwajalein"),
("Pacific/Majuro", "Pacific/Majuro"),
("Pacific/Marquesas", "Pacific/Marquesas"),
("Pacific/Midway", "Pacific/Midway"),
("Pacific/Nauru", "Pacific/Nauru"),
("Pacific/Niue", "Pacific/Niue"),
("Pacific/Norfolk", "Pacific/Norfolk"),
("Pacific/Noumea", "Pacific/Noumea"),
("Pacific/Pago_Pago", "Pacific/Pago_Pago"),
("Pacific/Palau", "Pacific/Palau"),
("Pacific/Pitcairn", "Pacific/Pitcairn"),
("Pacific/Pohnpei", "Pacific/Pohnpei"),
("Pacific/Ponape", "Pacific/Ponape"),
("Pacific/Port_Moresby", "Pacific/Port_Moresby"),
("Pacific/Rarotonga", "Pacific/Rarotonga"),
("Pacific/Saipan", "Pacific/Saipan"),
("Pacific/Samoa", "Pacific/Samoa"),
("Pacific/Tahiti", "Pacific/Tahiti"),
("Pacific/Tarawa", "Pacific/Tarawa"),
("Pacific/Tongatapu", "Pacific/Tongatapu"),
("Pacific/Truk", "Pacific/Truk"),
("Pacific/Wake", "Pacific/Wake"),
("Pacific/Wallis", "Pacific/Wallis"),
("Pacific/Yap", "Pacific/Yap"),
("Poland", "Poland"),
("Portugal", "Portugal"),
("ROC", "ROC"),
("ROK", "ROK"),
("Singapore", "Singapore"),
("Turkey", "Turkey"),
("UCT", "UCT"),
("US/Alaska", "US/Alaska"),
("US/Aleutian", "US/Aleutian"),
("US/Arizona", "US/Arizona"),
("US/Central", "US/Central"),
("US/East-Indiana", "US/East-Indiana"),
("US/Eastern", "US/Eastern"),
("US/Hawaii", "US/Hawaii"),
("US/Indiana-Starke", "US/Indiana-Starke"),
("US/Michigan", "US/Michigan"),
("US/Mountain", "US/Mountain"),
("US/Pacific", "US/Pacific"),
("US/Samoa", "US/Samoa"),
("UTC", "UTC"),
("Universal", "Universal"),
("W-SU", "W-SU"),
("WET", "WET"),
("Zulu", "Zulu"),
],
default="America/Los_Angeles",
max_length=255,
),
),
]

View File

@@ -1,4 +1,5 @@
import smtplib import smtplib
from contextlib import suppress
from email.message import EmailMessage from email.message import EmailMessage
from typing import TYPE_CHECKING, List, Optional, cast from typing import TYPE_CHECKING, List, Optional, cast
@@ -108,12 +109,10 @@ class CoreSettings(BaseAuditModel):
# for install script # for install script
if not self.pk: if not self.pk:
try: with suppress(Exception):
self.mesh_site = settings.MESH_SITE self.mesh_site = settings.MESH_SITE
self.mesh_username = settings.MESH_USERNAME.lower() self.mesh_username = settings.MESH_USERNAME.lower()
self.mesh_token = settings.MESH_TOKEN_KEY self.mesh_token = settings.MESH_TOKEN_KEY
except:
pass
old_settings = type(self).objects.get(pk=self.pk) if self.pk else None old_settings = type(self).objects.get(pk=self.pk) if self.pk else None
super(BaseAuditModel, self).save(*args, **kwargs) super(BaseAuditModel, self).save(*args, **kwargs)
@@ -182,10 +181,10 @@ class CoreSettings(BaseAuditModel):
test: bool = False, test: bool = False,
) -> tuple[str, bool]: ) -> tuple[str, bool]:
if test and not self.email_is_configured: if test and not self.email_is_configured:
return ("There needs to be at least one email recipient configured", False) return "There needs to be at least one email recipient configured", False
# return since email must be configured to continue # return since email must be configured to continue
elif not self.email_is_configured: elif not self.email_is_configured:
return ("SMTP messaging not configured.", False) return "SMTP messaging not configured.", False
# override email from if alert_template is passed and is set # override email from if alert_template is passed and is set
if alert_template and alert_template.email_from: if alert_template and alert_template.email_from:
@@ -199,7 +198,7 @@ class CoreSettings(BaseAuditModel):
elif self.email_alert_recipients: elif self.email_alert_recipients:
email_recipients = ", ".join(cast(List[str], self.email_alert_recipients)) email_recipients = ", ".join(cast(List[str], self.email_alert_recipients))
else: else:
return ("There needs to be at least one email recipient configured", False) return "There needs to be at least one email recipient configured", False
try: try:
msg = EmailMessage() msg = EmailMessage()
@@ -226,12 +225,12 @@ class CoreSettings(BaseAuditModel):
except Exception as e: except Exception as e:
DebugLog.error(message=f"Sending email failed with error: {e}") DebugLog.error(message=f"Sending email failed with error: {e}")
if test: if test:
return (str(e), False) return str(e), False
if test: if test:
return ("Email test ok!", True) return "Email test ok!", True
return ("ok", True) return "ok", True
def send_sms( def send_sms(
self, self,
@@ -240,7 +239,7 @@ class CoreSettings(BaseAuditModel):
test: bool = False, test: bool = False,
) -> tuple[str, bool]: ) -> tuple[str, bool]:
if not self.sms_is_configured: if not self.sms_is_configured:
return ("Sms alerting is not setup correctly.", False) return "Sms alerting is not setup correctly.", False
# override email recipients if alert_template is passed and is set # override email recipients if alert_template is passed and is set
if alert_template and alert_template.text_recipients: if alert_template and alert_template.text_recipients:
@@ -248,7 +247,7 @@ class CoreSettings(BaseAuditModel):
elif self.sms_alert_recipients: elif self.sms_alert_recipients:
text_recipients = cast(List[str], self.sms_alert_recipients) text_recipients = cast(List[str], self.sms_alert_recipients)
else: else:
return ("No sms recipients found", False) return "No sms recipients found", False
tw_client = TwClient(self.twilio_account_sid, self.twilio_auth_token) tw_client = TwClient(self.twilio_account_sid, self.twilio_auth_token)
for num in text_recipients: for num in text_recipients:
@@ -257,12 +256,12 @@ class CoreSettings(BaseAuditModel):
except TwilioRestException as e: except TwilioRestException as e:
DebugLog.error(message=f"SMS failed to send: {e}") DebugLog.error(message=f"SMS failed to send: {e}")
if test: if test:
return (str(e), False) return str(e), False
if test: if test:
return ("SMS Test sent successfully!", True) return "SMS Test sent successfully!", True
return ("ok", True) return "ok", True
@staticmethod @staticmethod
def serialize(core): def serialize(core):
@@ -315,7 +314,7 @@ class CustomField(BaseAuditModel):
return self.default_values_multiple return self.default_values_multiple
elif self.type == CustomFieldType.CHECKBOX: elif self.type == CustomFieldType.CHECKBOX:
return self.default_value_bool return self.default_value_bool
else:
return self.default_value_string return self.default_value_string
def get_or_create_field_value(self, instance): def get_or_create_field_value(self, instance):
@@ -365,6 +364,23 @@ class CodeSignToken(models.Model):
return r.status_code == 200 return r.status_code == 200
@property
def is_expired(self) -> bool:
if not self.token:
return False
try:
r = requests.post(
settings.CHECK_TOKEN_URL,
json={"token": self.token, "api": settings.ALLOWED_HOSTS[0]},
headers={"Content-type": "application/json"},
timeout=15,
)
except:
return False
return r.status_code == 401
def __str__(self): def __str__(self):
return "Code signing token" return "Code signing token"

View File

@@ -7,7 +7,7 @@ class CoreSettingsPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool: def has_permission(self, r, view) -> bool:
if r.method == "GET": if r.method == "GET":
return _has_perm(r, "can_view_core_settings") return _has_perm(r, "can_view_core_settings")
else:
return _has_perm(r, "can_edit_core_settings") return _has_perm(r, "can_edit_core_settings")
@@ -30,5 +30,5 @@ class CustomFieldPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool: def has_permission(self, r, view) -> bool:
if r.method == "GET": if r.method == "GET":
return _has_perm(r, "can_view_customfields") return _has_perm(r, "can_view_customfields")
else:
return _has_perm(r, "can_manage_customfields") return _has_perm(r, "can_manage_customfields")

View File

@@ -174,7 +174,7 @@ def _get_failing_data(agents: "QuerySet[Any]") -> Dict[str, bool]:
and task.task_result.status == TaskStatus.FAILING and task.task_result.status == TaskStatus.FAILING
and task.alert_severity == AlertSeverity.WARNING and task.alert_severity == AlertSeverity.WARNING
): ):
data["warning"] data["warning"] = True
return data return data

View File

@@ -5,13 +5,20 @@ from channels.db import database_sync_to_async
from channels.testing import WebsocketCommunicator from channels.testing import WebsocketCommunicator
from django.conf import settings from django.conf import settings
from django.core.management import call_command from django.core.management import call_command
from django.test import override_settings
from model_bakery import baker from model_bakery import baker
from rest_framework.authtoken.models import Token from rest_framework.authtoken.models import Token
from agents.models import Agent from agents.models import Agent
from core.utils import get_core_settings from core.utils import get_core_settings, get_meshagent_url
from logs.models import PendingAction from logs.models import PendingAction
from tacticalrmm.constants import CONFIG_MGMT_CMDS, CustomFieldModel, PAAction, PAStatus from tacticalrmm.constants import (
CONFIG_MGMT_CMDS,
CustomFieldModel,
MeshAgentIdent,
PAAction,
PAStatus,
)
from tacticalrmm.test import TacticalTestCase from tacticalrmm.test import TacticalTestCase
from .consumers import DashInfo from .consumers import DashInfo
@@ -444,3 +451,58 @@ class TestCorePermissions(TacticalTestCase):
def setUp(self): def setUp(self):
self.setup_client() self.setup_client()
self.setup_coresettings() self.setup_coresettings()
class TestCoreUtils(TacticalTestCase):
def setUp(self):
self.setup_coresettings()
def test_get_meshagent_url_standard(self):
r = get_meshagent_url(
ident=MeshAgentIdent.DARWIN_UNIVERSAL,
plat="darwin",
mesh_site="https://mesh.example.com",
mesh_device_id="abc123",
)
self.assertEqual(
r,
"https://mesh.example.com/meshagents?id=abc123&installflags=2&meshinstall=10005",
)
r = get_meshagent_url(
ident=MeshAgentIdent.WIN64,
plat="windows",
mesh_site="https://mesh.example.com",
mesh_device_id="abc123",
)
self.assertEqual(
r,
"https://mesh.example.com/meshagents?id=4&meshid=abc123&installflags=0",
)
@override_settings(DOCKER_BUILD=True)
@override_settings(MESH_WS_URL="ws://tactical-meshcentral:4443")
def test_get_meshagent_url_docker(self):
r = get_meshagent_url(
ident=MeshAgentIdent.DARWIN_UNIVERSAL,
plat="darwin",
mesh_site="https://mesh.example.com",
mesh_device_id="abc123",
)
self.assertEqual(
r,
"http://tactical-meshcentral:4443/meshagents?id=abc123&installflags=2&meshinstall=10005",
)
r = get_meshagent_url(
ident=MeshAgentIdent.WIN64,
plat="windows",
mesh_site="https://mesh.example.com",
mesh_device_id="abc123",
)
self.assertEqual(
r,
"http://tactical-meshcentral:4443/meshagents?id=4&meshid=abc123&installflags=0",
)

View File

@@ -1,6 +1,7 @@
import json import json
import subprocess import subprocess
import tempfile import tempfile
import urllib.parse
from base64 import b64encode from base64 import b64encode
from typing import TYPE_CHECKING, Optional, cast from typing import TYPE_CHECKING, Optional, cast
@@ -11,7 +12,12 @@ from django.core.cache import cache
from django.http import FileResponse from django.http import FileResponse
from meshctrl.utils import get_auth_token from meshctrl.utils import get_auth_token
from tacticalrmm.constants import CORESETTINGS_CACHE_KEY, ROLE_CACHE_PREFIX from tacticalrmm.constants import (
CORESETTINGS_CACHE_KEY,
ROLE_CACHE_PREFIX,
AgentPlat,
MeshAgentIdent,
)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.models import CodeSignToken, CoreSettings from core.models import CodeSignToken, CoreSettings
@@ -47,6 +53,16 @@ def token_is_valid() -> tuple[str, bool]:
return "", False return "", False
def token_is_expired() -> bool:
from core.models import CodeSignToken
t: "CodeSignToken" = CodeSignToken.objects.first()
if not t or not t.token:
return False
return t.is_expired
def get_core_settings() -> "CoreSettings": def get_core_settings() -> "CoreSettings":
from core.models import CORESETTINGS_CACHE_KEY, CoreSettings from core.models import CORESETTINGS_CACHE_KEY, CoreSettings
@@ -142,3 +158,28 @@ def sysd_svc_is_running(svc: str) -> bool:
cmd = ["systemctl", "is-active", "--quiet", svc] cmd = ["systemctl", "is-active", "--quiet", svc]
r = subprocess.run(cmd, capture_output=True) r = subprocess.run(cmd, capture_output=True)
return not r.returncode return not r.returncode
def get_meshagent_url(
*, ident: "MeshAgentIdent", plat: str, mesh_site: str, mesh_device_id: str
) -> str:
if settings.DOCKER_BUILD:
base = settings.MESH_WS_URL.replace("ws://", "http://")
else:
base = mesh_site
if plat == AgentPlat.WINDOWS:
params = {
"id": ident,
"meshid": mesh_device_id,
"installflags": 0,
}
else:
params = {
"id": mesh_device_id,
"installflags": 2,
"meshinstall": ident,
}
return base + "/meshagents?" + urllib.parse.urlencode(params)

View File

@@ -1,4 +1,5 @@
import re import re
from pathlib import Path
import psutil import psutil
import pytz import pytz
@@ -6,8 +7,8 @@ from cryptography import x509
from django.conf import settings from django.conf import settings
from django.http import JsonResponse from django.http import JsonResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.views.decorators.csrf import csrf_exempt
from django.utils import timezone as djangotime from django.utils import timezone as djangotime
from django.views.decorators.csrf import csrf_exempt
from rest_framework.decorators import api_view, permission_classes from rest_framework.decorators import api_view, permission_classes
from rest_framework.exceptions import PermissionDenied from rest_framework.exceptions import PermissionDenied
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
@@ -73,6 +74,7 @@ def clear_cache(request):
@api_view() @api_view()
def dashboard_info(request): def dashboard_info(request):
from core.utils import token_is_expired
from tacticalrmm.utils import get_latest_trmm_ver from tacticalrmm.utils import get_latest_trmm_ver
return Response( return Response(
@@ -93,6 +95,7 @@ def dashboard_info(request):
"hosted": getattr(settings, "HOSTED", False), "hosted": getattr(settings, "HOSTED", False),
"date_format": request.user.date_format, "date_format": request.user.date_format,
"default_date_format": get_core_settings().date_format, "default_date_format": get_core_settings().date_format,
"token_is_expired": token_is_expired(),
} }
) )
@@ -175,7 +178,7 @@ class GetAddCustomFields(APIView):
if "model" in request.data.keys(): if "model" in request.data.keys():
fields = CustomField.objects.filter(model=request.data["model"]) fields = CustomField.objects.filter(model=request.data["model"])
return Response(CustomFieldSerializer(fields, many=True).data) return Response(CustomFieldSerializer(fields, many=True).data)
else:
return notify_error("The request was invalid") return notify_error("The request was invalid")
def post(self, request): def post(self, request):
@@ -231,7 +234,7 @@ class CodeSign(APIView):
except Exception as e: except Exception as e:
return notify_error(str(e)) return notify_error(str(e))
if r.status_code == 400 or r.status_code == 401: if r.status_code in (400, 401):
return notify_error(r.json()["ret"]) return notify_error(r.json()["ret"])
elif r.status_code == 200: elif r.status_code == 200:
t = CodeSignToken.objects.first() t = CodeSignToken.objects.first()
@@ -414,8 +417,7 @@ def status(request):
mem_usage: int = round(psutil.virtual_memory().percent) mem_usage: int = round(psutil.virtual_memory().percent)
cert_file, _ = get_certs() cert_file, _ = get_certs()
with open(cert_file, "rb") as f: cert_bytes = Path(cert_file).read_bytes()
cert_bytes = f.read()
cert = x509.load_pem_x509_certificate(cert_bytes) cert = x509.load_pem_x509_certificate(cert_bytes)
expires = pytz.utc.localize(cert.not_valid_after) expires = pytz.utc.localize(cert.not_valid_after)
@@ -424,6 +426,7 @@ def status(request):
ret = { ret = {
"version": settings.TRMM_VERSION, "version": settings.TRMM_VERSION,
"latest_agent_version": settings.LATEST_AGENT_VER,
"agent_count": Agent.objects.count(), "agent_count": Agent.objects.count(),
"client_count": Client.objects.count(), "client_count": Client.objects.count(),
"site_count": Site.objects.count(), "site_count": Site.objects.count(),

View File

@@ -282,7 +282,7 @@ class DebugLog(models.Model):
agent: "Optional[Agent]" = None, agent: "Optional[Agent]" = None,
log_type: str = DebugLogType.SYSTEM_ISSUES, log_type: str = DebugLogType.SYSTEM_ISSUES,
) -> None: ) -> None:
if get_debug_level() in [DebugLogLevel.INFO]: if get_debug_level() == DebugLogLevel.INFO:
cls.objects.create( cls.objects.create(
log_level=DebugLogLevel.INFO, log_level=DebugLogLevel.INFO,
agent=agent, agent=agent,
@@ -297,7 +297,7 @@ class DebugLog(models.Model):
agent: "Optional[Agent]" = None, agent: "Optional[Agent]" = None,
log_type: str = DebugLogType.SYSTEM_ISSUES, log_type: str = DebugLogType.SYSTEM_ISSUES,
) -> None: ) -> None:
if get_debug_level() in [DebugLogLevel.INFO, DebugLogLevel.WARN]: if get_debug_level() in (DebugLogLevel.INFO, DebugLogLevel.WARN):
cls.objects.create( cls.objects.create(
log_level=DebugLogLevel.INFO, log_level=DebugLogLevel.INFO,
agent=agent, agent=agent,
@@ -312,11 +312,11 @@ class DebugLog(models.Model):
agent: "Optional[Agent]" = None, agent: "Optional[Agent]" = None,
log_type: str = DebugLogType.SYSTEM_ISSUES, log_type: str = DebugLogType.SYSTEM_ISSUES,
) -> None: ) -> None:
if get_debug_level() in [ if get_debug_level() in (
DebugLogLevel.INFO, DebugLogLevel.INFO,
DebugLogLevel.WARN, DebugLogLevel.WARN,
DebugLogLevel.ERROR, DebugLogLevel.ERROR,
]: ):
cls.objects.create( cls.objects.create(
log_level=DebugLogLevel.ERROR, log_level=DebugLogLevel.ERROR,
agent=agent, agent=agent,
@@ -331,12 +331,12 @@ class DebugLog(models.Model):
agent: "Optional[Agent]" = None, agent: "Optional[Agent]" = None,
log_type: str = DebugLogType.SYSTEM_ISSUES, log_type: str = DebugLogType.SYSTEM_ISSUES,
) -> None: ) -> None:
if get_debug_level() in [ if get_debug_level() in (
DebugLogLevel.INFO, DebugLogLevel.INFO,
DebugLogLevel.WARN, DebugLogLevel.WARN,
DebugLogLevel.ERROR, DebugLogLevel.ERROR,
DebugLogLevel.CRITICAL, DebugLogLevel.CRITICAL,
]: ):
cls.objects.create( cls.objects.create(
log_level=DebugLogLevel.CRITICAL, log_level=DebugLogLevel.CRITICAL,
agent=agent, agent=agent,
@@ -376,7 +376,7 @@ class PendingAction(models.Model):
return "Next update cycle" return "Next update cycle"
elif self.action_type == PAAction.CHOCO_INSTALL: elif self.action_type == PAAction.CHOCO_INSTALL:
return "ASAP" return "ASAP"
else:
return "On next checkin" return "On next checkin"
@property @property
@@ -390,14 +390,14 @@ class PendingAction(models.Model):
elif self.action_type == PAAction.CHOCO_INSTALL: elif self.action_type == PAAction.CHOCO_INSTALL:
return f"{self.details['name']} software install" return f"{self.details['name']} software install"
elif self.action_type in [ elif self.action_type in (
PAAction.RUN_CMD, PAAction.RUN_CMD,
PAAction.RUN_SCRIPT, PAAction.RUN_SCRIPT,
PAAction.RUN_PATCH_SCAN, PAAction.RUN_PATCH_SCAN,
PAAction.RUN_PATCH_INSTALL, PAAction.RUN_PATCH_INSTALL,
]: ):
return f"{self.action_type}" return f"{self.action_type}"
else:
return None return None

View File

@@ -16,7 +16,7 @@ class AuditLogSerializer(serializers.ModelSerializer):
return SiteMinimumSerializer( return SiteMinimumSerializer(
Agent.objects.get(agent_id=obj.agent_id).site Agent.objects.get(agent_id=obj.agent_id).site
).data ).data
else:
return None return None
class Meta: class Meta:

View File

@@ -1,11 +1,10 @@
import datetime as dt import datetime as dt
import pytz
from django.db.models.signals import post_init from django.db.models.signals import post_init
from django.dispatch import receiver from django.dispatch import receiver
from django.utils import timezone as djangotime
from tacticalrmm.constants import PAAction, PAStatus from tacticalrmm.constants import PAAction, PAStatus
from tacticalrmm.helpers import date_is_in_past
from .models import PendingAction from .models import PendingAction
@@ -22,14 +21,8 @@ def handle_status(sender, instance: PendingAction, **kwargs):
reboot_time = dt.datetime.strptime( reboot_time = dt.datetime.strptime(
instance.details["time"], "%Y-%m-%d %H:%M:%S" instance.details["time"], "%Y-%m-%d %H:%M:%S"
) )
if date_is_in_past(
# need to convert agent tz to UTC in order to compare datetime_obj=reboot_time, agent_tz=instance.agent.timezone
agent_tz = pytz.timezone(instance.agent.timezone) ):
localized = agent_tz.localize(reboot_time)
now = djangotime.now()
reboot_time_utc = localized.astimezone(pytz.utc)
if now > reboot_time_utc:
instance.status = PAStatus.COMPLETED instance.status = PAStatus.COMPLETED
instance.save(update_fields=["status"]) instance.save(update_fields=["status"])

View File

@@ -6,3 +6,4 @@ pytest-django
pytest-xdist pytest-xdist
pytest-cov pytest-cov
codecov codecov
refurb

View File

@@ -1,39 +1,39 @@
asgiref==3.5.2 asgiref==3.5.2
celery==5.2.7 celery==5.2.7
certifi==2022.6.15 certifi==2022.9.24
cffi==1.15.1 cffi==1.15.1
channels==3.0.5 channels==4.0.0
channels_redis==3.4.0 channels_redis==4.0.0
chardet==4.0.0 chardet==4.0.0
cryptography==37.0.4 cryptography==38.0.1
daphne==3.0.2 daphne==4.0.0
Django==4.0.6 Django==4.1.2
django-cors-headers==3.13.0 django-cors-headers==3.13.0
django-ipware==4.0.2 django-ipware==4.0.2
django-rest-knox==4.2.0 django-rest-knox==4.2.0
djangorestframework==3.13.1 djangorestframework==3.14.0
drf-spectacular==0.24.2
future==0.18.2 future==0.18.2
msgpack==1.0.4 msgpack==1.0.4
nats-py==2.1.3 nats-py==2.2.0
psutil==5.9.1 psutil==5.9.3
psycopg2-binary==2.9.3 psycopg2-binary==2.9.4
pycparser==2.21 pycparser==2.21
pycryptodome==3.15.0 pycryptodome==3.15.0
pyotp==2.6.0 pyotp==2.7.0
pyparsing==3.0.9 pyparsing==3.0.9
pytz==2022.1 pytz==2022.5
qrcode==7.3.1 qrcode==7.3.1
redis==4.3.4 redis==4.3.4
hiredis==2.0.0 hiredis==2.0.0
requests==2.28.1 requests==2.28.1
six==1.16.0 six==1.16.0
sqlparse==0.4.2 sqlparse==0.4.3
twilio==7.10.0 twilio==7.14.2
urllib3==1.26.9 urllib3==1.26.12
uWSGI==2.0.20 uWSGI==2.0.20
validators==0.20.0 validators==0.20.0
vine==5.0.0 vine==5.0.0
websockets==10.3 websockets==10.3
zipp==3.8.0 zipp==3.9.0
drf_spectacular==0.22.1
meshctrl==0.1.15 meshctrl==0.1.15

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.0.6 on 2022-07-30 21:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('scripts', '0017_auto_20220311_0100'),
]
operations = [
migrations.AddField(
model_name='script',
name='run_as_user',
field=models.BooleanField(default=False),
),
]

View File

@@ -40,13 +40,14 @@ class Script(BaseAuditModel):
supported_platforms = ArrayField( supported_platforms = ArrayField(
models.CharField(max_length=20), null=True, blank=True, default=list models.CharField(max_length=20), null=True, blank=True, default=list
) )
run_as_user = models.BooleanField(default=False)
def __str__(self): def __str__(self):
return self.name return self.name
@property @property
def code_no_snippets(self): def code_no_snippets(self):
return self.script_body if self.script_body else "" return self.script_body or ""
@property @property
def code(self): def code(self):
@@ -65,10 +66,11 @@ class Script(BaseAuditModel):
else: else:
value = "" value = ""
replaced_code = re.sub(snippet.group(), value, replaced_code) replaced_code = re.sub(
snippet.group(), value.replace("\\", "\\\\"), replaced_code
)
return replaced_code return replaced_code
else:
return code return code
def hash_script_body(self): def hash_script_body(self):
@@ -111,14 +113,14 @@ class Script(BaseAuditModel):
else 90 else 90
) )
args = script["args"] if "args" in script.keys() else list() args = script["args"] if "args" in script.keys() else []
syntax = script["syntax"] if "syntax" in script.keys() else "" syntax = script["syntax"] if "syntax" in script.keys() else ""
supported_platforms = ( supported_platforms = (
script["supported_platforms"] script["supported_platforms"]
if "supported_platforms" in script.keys() if "supported_platforms" in script.keys()
else list() else []
) )
# if community script exists update it # if community script exists update it
@@ -186,12 +188,12 @@ class Script(BaseAuditModel):
return ScriptSerializer(script).data return ScriptSerializer(script).data
@classmethod @classmethod
def parse_script_args(cls, agent, shell: str, args: List[str] = list()) -> list: def parse_script_args(cls, agent, shell: str, args: List[str] = []) -> list:
if not args: if not args:
return [] return []
temp_args = list() temp_args = []
# pattern to match for injection # pattern to match for injection
pattern = re.compile(".*\\{\\{(.*)\\}\\}.*") pattern = re.compile(".*\\{\\{(.*)\\}\\}.*")

View File

@@ -7,5 +7,5 @@ class ScriptsPerms(permissions.BasePermission):
def has_permission(self, r, view) -> bool: def has_permission(self, r, view) -> bool:
if r.method == "GET": if r.method == "GET":
return _has_perm(r, "can_list_scripts") return _has_perm(r, "can_list_scripts")
else:
return _has_perm(r, "can_manage_scripts") return _has_perm(r, "can_manage_scripts")

View File

@@ -20,6 +20,7 @@ class ScriptTableSerializer(ModelSerializer):
"filename", "filename",
"hidden", "hidden",
"supported_platforms", "supported_platforms",
"run_as_user",
] ]
@@ -43,16 +44,17 @@ class ScriptSerializer(ModelSerializer):
"filename", "filename",
"hidden", "hidden",
"supported_platforms", "supported_platforms",
"run_as_user",
] ]
class ScriptCheckSerializer(ModelSerializer): class ScriptCheckSerializer(ModelSerializer):
code = ReadOnlyField() code = ReadOnlyField()
script_hash = ReadOnlyField script_hash = ReadOnlyField()
class Meta: class Meta:
model = Script model = Script
fields = ["code", "shell", "script_hash"] fields = ["code", "shell", "run_as_user", "script_hash"]
class ScriptSnippetSerializer(ModelSerializer): class ScriptSnippetSerializer(ModelSerializer):

View File

@@ -9,7 +9,12 @@ from tacticalrmm.constants import AgentHistoryType
@app.task @app.task
def handle_bulk_command_task( def handle_bulk_command_task(
agentpks, cmd, shell, timeout, username, run_on_offline=False agentpks: list[int],
cmd: str,
shell: str,
timeout,
username,
run_as_user: bool = False,
) -> None: ) -> None:
nats_data = { nats_data = {
"func": "rawcmd", "func": "rawcmd",
@@ -18,7 +23,9 @@ def handle_bulk_command_task(
"command": cmd, "command": cmd,
"shell": shell, "shell": shell,
}, },
"run_as_user": run_as_user,
} }
agent: "Agent"
for agent in Agent.objects.filter(pk__in=agentpks): for agent in Agent.objects.filter(pk__in=agentpks):
hist = AgentHistory.objects.create( hist = AgentHistory.objects.create(
agent=agent, agent=agent,
@@ -33,9 +40,15 @@ def handle_bulk_command_task(
@app.task @app.task
def handle_bulk_script_task( def handle_bulk_script_task(
scriptpk: int, agentpks: List[int], args: List[str], timeout: int, username: str scriptpk: int,
agentpks: List[int],
args: List[str],
timeout: int,
username: str,
run_as_user: bool = False,
) -> None: ) -> None:
script = Script.objects.get(pk=scriptpk) script = Script.objects.get(pk=scriptpk)
agent: "Agent"
for agent in Agent.objects.filter(pk__in=agentpks): for agent in Agent.objects.filter(pk__in=agentpks):
hist = AgentHistory.objects.create( hist = AgentHistory.objects.create(
agent=agent, agent=agent,
@@ -44,5 +57,9 @@ def handle_bulk_script_task(
username=username, username=username,
) )
agent.run_script( agent.run_script(
scriptpk=script.pk, args=args, timeout=timeout, history_pk=hist.pk scriptpk=script.pk,
args=args,
timeout=timeout,
history_pk=hist.pk,
run_as_user=run_as_user,
) )

View File

@@ -145,6 +145,7 @@ class TestScriptViews(TacticalTestCase):
"timeout": 90, "timeout": 90,
"args": [], "args": [],
"shell": ScriptShell.POWERSHELL, "shell": ScriptShell.POWERSHELL,
"run_as_user": False,
} }
resp = self.client.post(url, data, format="json") resp = self.client.post(url, data, format="json")

View File

@@ -17,6 +17,7 @@ from .serializers import (
ScriptSnippetSerializer, ScriptSnippetSerializer,
ScriptTableSerializer, ScriptTableSerializer,
) )
from core.utils import clear_entire_cache
class GetAddScripts(APIView): class GetAddScripts(APIView):
@@ -58,7 +59,7 @@ class GetUpdateDeleteScript(APIView):
return Response(ScriptSerializer(script).data) return Response(ScriptSerializer(script).data)
def put(self, request, pk): def put(self, request, pk):
script = get_object_or_404(Script, pk=pk) script = get_object_or_404(Script.objects.prefetch_related("script"), pk=pk)
data = request.data data = request.data
@@ -76,7 +77,12 @@ class GetUpdateDeleteScript(APIView):
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
obj = serializer.save() obj = serializer.save()
# obj.hash_script_body() # TODO rename the related field from 'script' to 'scriptchecks' so it's not so confusing
if script.script.exists():
for script_check in script.script.all():
if script_check.policy:
clear_entire_cache()
break
return Response(f"{obj.name} was edited!") return Response(f"{obj.name} was edited!")
@@ -154,6 +160,7 @@ class TestScript(APIView):
"code": Script.replace_with_snippets(request.data["code"]), "code": Script.replace_with_snippets(request.data["code"]),
"shell": request.data["shell"], "shell": request.data["shell"],
}, },
"run_as_user": request.data["run_as_user"],
} }
r = asyncio.run( r = asyncio.run(

View File

@@ -9,5 +9,5 @@ class WinSvcsPerms(permissions.BasePermission):
return _has_perm(r, "can_manage_winsvcs") and _has_perm_on_agent( return _has_perm(r, "can_manage_winsvcs") and _has_perm_on_agent(
r.user, view.kwargs["agent_id"] r.user, view.kwargs["agent_id"]
) )
else:
return _has_perm(r, "can_manage_winsvcs") return _has_perm(r, "can_manage_winsvcs")

View File

@@ -26,7 +26,7 @@ def process_nats_response(data: Union[str, Dict]) -> Tuple[bool, bool, str]:
else "timeout" else "timeout"
) )
return (success, natserror, errormsg) return success, natserror, errormsg
class GetServices(APIView): class GetServices(APIView):
@@ -41,7 +41,7 @@ class GetServices(APIView):
agent = get_object_or_404(Agent, agent_id=agent_id) agent = get_object_or_404(Agent, agent_id=agent_id)
r = asyncio.run(agent.nats_cmd(data={"func": "winservices"}, timeout=10)) r = asyncio.run(agent.nats_cmd(data={"func": "winservices"}, timeout=10))
if r == "timeout" or r == "natsdown": if r in ("timeout", "natsdown"):
return notify_error("Unable to contact the agent") return notify_error("Unable to contact the agent")
agent.services = r agent.services = r

View File

@@ -1,6 +1,6 @@
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from agents.models import Agent from software.models import InstalledSoftware
class Command(BaseCommand): class Command(BaseCommand):
@@ -12,22 +12,15 @@ class Command(BaseCommand):
def handle(self, *args, **kwargs): def handle(self, *args, **kwargs):
search = kwargs["name"].lower() search = kwargs["name"].lower()
agents = Agent.objects.all() all_sw = InstalledSoftware.objects.select_related(
for agent in agents: "agent", "agent__site", "agent__site__client"
try:
sw = agent.installedsoftware_set.first().software
except:
self.stdout.write(
self.style.ERROR(
f"Agent {agent.hostname} missing software list. Try manually refreshing it from the web UI from the software tab."
) )
) for instance in all_sw.iterator(chunk_size=20):
continue for sw in instance.software:
for i in sw: if search in sw["name"].lower():
if search in i["name"].lower():
self.stdout.write( self.stdout.write(
self.style.SUCCESS( self.style.SUCCESS(
f"Found {i['name']} installed on {agent.hostname}" f"Found {sw['name']} installed on: {instance.agent.client.name}\\{instance.agent.site.name}\\{instance.agent.hostname}"
) )
) )
break break

View File

@@ -13,7 +13,6 @@ class SoftwarePerms(permissions.BasePermission):
return _has_perm(r, "can_list_software") return _has_perm(r, "can_list_software")
else:
return _has_perm(r, "can_manage_software") and _has_perm_on_agent( return _has_perm(r, "can_manage_software") and _has_perm_on_agent(
r.user, view.kwargs["agent_id"] r.user, view.kwargs["agent_id"]
) )

View File

@@ -22,7 +22,7 @@ def chocos(request):
chocos = ChocoSoftware.objects.last() chocos = ChocoSoftware.objects.last()
if not chocos: if not chocos:
return Response({}) return Response({})
else:
return Response(chocos.chocos) return Response(chocos.chocos)
@@ -79,7 +79,7 @@ class GetSoftware(APIView):
return notify_error(f"Not available for {agent.plat}") return notify_error(f"Not available for {agent.plat}")
r: Any = asyncio.run(agent.nats_cmd({"func": "softwarelist"}, timeout=15)) r: Any = asyncio.run(agent.nats_cmd({"func": "softwarelist"}, timeout=15))
if r == "timeout" or r == "natsdown": if r in ("timeout", "natsdown"):
return notify_error("Unable to contact the agent") return notify_error("Unable to contact the agent")
if not InstalledSoftware.objects.filter(agent=agent).exists(): if not InstalledSoftware.objects.filter(agent=agent).exists():

View File

@@ -61,4 +61,4 @@ class APIAuthentication(BaseAuthentication):
if apikey.expiration and apikey.expiration < djangotime.now(): if apikey.expiration and apikey.expiration < djangotime.now():
raise exceptions.AuthenticationFailed(_("The token as expired.")) raise exceptions.AuthenticationFailed(_("The token as expired."))
return (apikey.user, apikey.key) return apikey.user, apikey.key

View File

@@ -10,6 +10,7 @@ class MeshAgentIdent(Enum):
LINUX64 = 6 LINUX64 = 6
LINUX_ARM_64 = 26 LINUX_ARM_64 = 26
LINUX_ARM_HF = 25 LINUX_ARM_HF = 25
DARWIN_UNIVERSAL = 10005
def __str__(self): def __str__(self):
return str(self.value) return str(self.value)
@@ -240,6 +241,14 @@ AGENT_DEFER = (
"modified_time", "modified_time",
) )
AGENT_TABLE_DEFER = (
"services",
"created_by",
"created_time",
"modified_by",
"modified_time",
)
ONLINE_AGENTS = ( ONLINE_AGENTS = (
"pk", "pk",
"agent_id", "agent_id",

View File

@@ -1,7 +1,14 @@
from typing import TYPE_CHECKING
import pytz
from django.conf import settings from django.conf import settings
from django.utils import timezone as djangotime
from rest_framework import status from rest_framework import status
from rest_framework.response import Response from rest_framework.response import Response
if TYPE_CHECKING:
from datetime import datetime
def get_certs() -> tuple[str, str]: def get_certs() -> tuple[str, str]:
domain = settings.ALLOWED_HOSTS[0].split(".", 1)[1] domain = settings.ALLOWED_HOSTS[0].split(".", 1)[1]
@@ -12,7 +19,7 @@ def get_certs() -> tuple[str, str]:
cert_file = settings.CERT_FILE cert_file = settings.CERT_FILE
key_file = settings.KEY_FILE key_file = settings.KEY_FILE
return (cert_file, key_file) return cert_file, key_file
def notify_error(msg: str) -> Response: def notify_error(msg: str) -> Response:
@@ -26,4 +33,16 @@ def get_nats_ports() -> tuple[int, int]:
nats_standard_port = getattr(settings, "NATS_STANDARD_PORT", 4222) nats_standard_port = getattr(settings, "NATS_STANDARD_PORT", 4222)
nats_websocket_port = getattr(settings, "NATS_WEBSOCKET_PORT", 9235) nats_websocket_port = getattr(settings, "NATS_WEBSOCKET_PORT", 9235)
return (nats_standard_port, nats_websocket_port) return nats_standard_port, nats_websocket_port
def date_is_in_past(*, datetime_obj: "datetime", agent_tz: str) -> bool:
"""
datetime_obj must be a naive datetime
"""
now = djangotime.now()
# convert agent tz to UTC to compare
agent_pytz = pytz.timezone(agent_tz)
localized = agent_pytz.localize(datetime_obj)
utc_time = localized.astimezone(pytz.utc)
return now > utc_time

View File

@@ -1,4 +1,5 @@
import threading import threading
from contextlib import suppress
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
from django.conf import settings from django.conf import settings
@@ -62,7 +63,7 @@ class AuditMiddleware:
request = APIView().initialize_request(request) request = APIView().initialize_request(request)
# check if user is authenticated # check if user is authenticated
try: with suppress(AuthenticationFailed):
if hasattr(request, "user") and request.user.is_authenticated: if hasattr(request, "user") and request.user.is_authenticated:
try: try:
@@ -83,8 +84,6 @@ class AuditMiddleware:
# get authenticated user after request # get authenticated user after request
request_local.username = request.user.username request_local.username = request.user.username
except AuthenticationFailed:
pass
def process_exception(self, request, exception): def process_exception(self, request, exception):
request_local.debug_info = None request_local.debug_info = None

View File

@@ -26,7 +26,7 @@ class PermissionQuerySet(models.QuerySet):
model_name = self.model._meta.label.split(".")[1] model_name = self.model._meta.label.split(".")[1]
# checks which sites and clients the user has access to and filters agents # checks which sites and clients the user has access to and filters agents
if model_name in ["Agent", "Deployment"]: if model_name in ("Agent", "Deployment"):
if can_view_clients: if can_view_clients:
clients_queryset = models.Q(site__client__in=can_view_clients) clients_queryset = models.Q(site__client__in=can_view_clients)
@@ -81,7 +81,7 @@ class PermissionQuerySet(models.QuerySet):
return self return self
# if model that is being filtered is a Check or Automated task we need to allow checks/tasks that are associated with policies # if model that is being filtered is a Check or Automated task we need to allow checks/tasks that are associated with policies
if model_name in ["Check", "AutomatedTask", "DebugLog"] and ( if model_name in ("Check", "AutomatedTask", "DebugLog") and (
can_view_clients or can_view_sites can_view_clients or can_view_sites
): ):
agent_queryset = models.Q(agent=None) # dont filter if agent is None agent_queryset = models.Q(agent=None) # dont filter if agent is None

View File

@@ -1,4 +1,5 @@
import os import os
from contextlib import suppress
from datetime import timedelta from datetime import timedelta
from pathlib import Path from pathlib import Path
@@ -14,29 +15,31 @@ EXE_DIR = os.path.join(BASE_DIR, "tacticalrmm/private/exe")
LINUX_AGENT_SCRIPT = BASE_DIR / "core" / "agent_linux.sh" LINUX_AGENT_SCRIPT = BASE_DIR / "core" / "agent_linux.sh"
MAC_UNINSTALL = BASE_DIR / "core" / "mac_uninstall.sh"
AUTH_USER_MODEL = "accounts.User" AUTH_USER_MODEL = "accounts.User"
# latest release # latest release
TRMM_VERSION = "0.14.0" TRMM_VERSION = "0.15.1"
# https://github.com/amidaware/tacticalrmm-web # https://github.com/amidaware/tacticalrmm-web
WEB_VERSION = "0.100.4" WEB_VERSION = "0.101.3"
# bump this version everytime vue code is changed # bump this version everytime vue code is changed
# to alert user they need to manually refresh their browser # to alert user they need to manually refresh their browser
APP_VER = "0.0.165" APP_VER = "0.0.172"
# https://github.com/amidaware/rmmagent # https://github.com/amidaware/rmmagent
LATEST_AGENT_VER = "2.1.0" LATEST_AGENT_VER = "2.4.0"
MESH_VER = "1.0.43" MESH_VER = "1.0.85"
NATS_SERVER_VER = "2.8.4" NATS_SERVER_VER = "2.9.3"
# for the update script, bump when need to recreate venv # for the update script, bump when need to recreate venv
PIP_VER = "31" PIP_VER = "33"
SETUPTOOLS_VER = "62.6.0" SETUPTOOLS_VER = "65.5.0"
WHEEL_VER = "0.37.1" WHEEL_VER = "0.37.1"
AGENT_BASE_URL = "https://agents.tacticalrmm.com" AGENT_BASE_URL = "https://agents.tacticalrmm.com"
@@ -71,10 +74,8 @@ HOSTED = False
SWAGGER_ENABLED = False SWAGGER_ENABLED = False
REDIS_HOST = "127.0.0.1" REDIS_HOST = "127.0.0.1"
try: with suppress(ImportError):
from .local_settings import * from .local_settings import *
except ImportError:
pass
if "GHACTIONS" in os.environ: if "GHACTIONS" in os.environ:
DEBUG = False DEBUG = False
@@ -104,6 +105,7 @@ if not DEBUG:
) )
INSTALLED_APPS = [ INSTALLED_APPS = [
"daphne",
"django.contrib.auth", "django.contrib.auth",
"django.contrib.contenttypes", "django.contrib.contenttypes",
"django.contrib.sessions", "django.contrib.sessions",

View File

@@ -28,7 +28,7 @@ from tacticalrmm.constants import (
DebugLogType, DebugLogType,
ScriptShell, ScriptShell,
) )
from tacticalrmm.helpers import get_certs, notify_error, get_nats_ports from tacticalrmm.helpers import get_certs, get_nats_ports, notify_error
def generate_winagent_exe( def generate_winagent_exe(
@@ -112,7 +112,7 @@ def bitdays_to_string(day: int) -> str:
return "Every day" return "Every day"
for key, value in WEEK_DAYS.items(): for key, value in WEEK_DAYS.items():
if day & int(value): if day & value:
ret.append(key) ret.append(key)
return ", ".join(ret) return ", ".join(ret)
@@ -123,7 +123,7 @@ def bitmonths_to_string(month: int) -> str:
return "Every month" return "Every month"
for key, value in MONTHS.items(): for key, value in MONTHS.items():
if month & int(value): if month & value:
ret.append(key) ret.append(key)
return ", ".join(ret) return ", ".join(ret)
@@ -134,7 +134,7 @@ def bitweeks_to_string(week: int) -> str:
return "Every week" return "Every week"
for key, value in WEEKS.items(): for key, value in WEEKS.items():
if week & int(value): if week & value:
ret.append(key) ret.append(key)
return ", ".join(ret) return ", ".join(ret)
@@ -144,11 +144,11 @@ def bitmonthdays_to_string(day: int) -> str:
if day == MONTH_DAYS["Last Day"]: if day == MONTH_DAYS["Last Day"]:
return "Last day" return "Last day"
elif day == 2147483647 or day == 4294967295: elif day in (2147483647, 4294967295):
return "Every day" return "Every day"
for key, value in MONTH_DAYS.items(): for key, value in MONTH_DAYS.items():
if day & int(value): if day & value:
ret.append(key) ret.append(key)
return ", ".join(ret) return ", ".join(ret)
@@ -157,7 +157,7 @@ def convert_to_iso_duration(string: str) -> str:
tmp = string.upper() tmp = string.upper()
if "D" in tmp: if "D" in tmp:
return f"P{tmp.replace('D', 'DT')}" return f"P{tmp.replace('D', 'DT')}"
else:
return f"PT{tmp}" return f"PT{tmp}"
@@ -209,6 +209,11 @@ def reload_nats() -> None:
}, },
} }
if "NATS_HTTP_PORT" in os.environ:
config["http_port"] = int(os.getenv("NATS_HTTP_PORT")) # type: ignore
elif hasattr(settings, "NATS_HTTP_PORT"):
config["http_port"] = settings.NATS_HTTP_PORT # type: ignore
conf = os.path.join(settings.BASE_DIR, "nats-rmm.conf") conf = os.path.join(settings.BASE_DIR, "nats-rmm.conf")
with open(conf, "w") as f: with open(conf, "w") as f:
json.dump(config, f) json.dump(config, f)
@@ -391,5 +396,5 @@ def format_shell_array(value: list[str]) -> str:
def format_shell_bool(value: bool, shell: Optional[str]) -> str: def format_shell_bool(value: bool, shell: Optional[str]) -> str:
if shell == ScriptShell.POWERSHELL: if shell == ScriptShell.POWERSHELL:
return "$True" if value else "$False" return "$True" if value else "$False"
else:
return "1" if value else "0" return "1" if value else "0"

Some files were not shown because too many files have changed in this diff Show More