Compare commits

...

80 Commits

Author SHA1 Message Date
wh1te909
09c535f159 Release 0.2.0 2020-11-26 03:43:40 +00:00
wh1te909
7fb11da5df update scripts and bump version 2020-11-26 03:42:27 +00:00
wh1te909
9c9a46499a allow changing of refresh interval for task manager 2020-11-26 01:54:20 +00:00
wh1te909
6fca60261e fix recovery 2020-11-26 01:04:42 +00:00
wh1te909
00537b32ef hide output 2020-11-26 00:38:13 +00:00
wh1te909
8636758a90 fix tests 2020-11-26 00:02:11 +00:00
wh1te909
e39dfbd624 Merge branch 'develop' of https://github.com/wh1te909/tacticalrmm into develop 2020-11-25 23:48:43 +00:00
wh1te909
6e048b2a12 agent recovery improvements 2020-11-25 23:48:14 +00:00
wh1te909
f9657599c2 update agents UI fixes 2020-11-25 23:45:02 +00:00
wh1te909
42ae3bba9b increase timeout for software list 2020-11-25 23:41:49 +00:00
Josh
2fd56a4bfe lock in mongodb and redis service cotnainers to a specific versions 2020-11-25 23:09:17 +00:00
wh1te909
824bcc5603 black 2020-11-25 22:19:27 +00:00
wh1te909
4fbb613aaa change bg tasks schedule 2020-11-25 21:18:03 +00:00
sadnub
9eb45270f2 Update docker readme 2020-11-25 14:53:38 -05:00
Tragic Bronson
75c61c53e8 Merge pull request #190 from sadnub/develop
Docker Setup
2020-11-24 20:46:05 -08:00
sadnub
2688a47436 fix settings 2020-11-24 23:32:26 -05:00
sadnub
fe3bf4b189 get nats container working 2020-11-24 23:25:34 -05:00
Josh Krawczyk
456cb5ebb2 mesh data fix 2020-11-24 23:25:34 -05:00
Josh Krawczyk
3d91d574b4 docker changes 2020-11-24 23:25:34 -05:00
sadnub
54876c5499 fixes to domain in cert generation 2020-11-24 23:25:34 -05:00
sadnub
d256585284 docker changes 2020-11-24 23:25:34 -05:00
sadnub
bd8f100b43 move tactical docker image to alpine 2020-11-24 23:25:34 -05:00
sadnub
44f05f2dcc nats docker setup 2020-11-24 23:25:34 -05:00
sadnub
43f7f82bdc docker fixes 2020-11-24 23:25:34 -05:00
wh1te909
e902f63211 fix response 2020-11-25 03:40:39 +00:00
wh1te909
129f68e194 remove task that's no longer applicable due to recent db changes 2020-11-25 02:47:44 +00:00
wh1te909
4b37fe12d7 remove task that's no longer applicable due to recent db changes 2020-11-25 02:46:50 +00:00
wh1te909
6de79922c5 fix git failing to switch branches during update 2020-11-25 01:18:26 +00:00
wh1te909
e1a9791f44 move run task to nats 2020-11-25 00:17:12 +00:00
wh1te909
81795f51c6 more cleanup 2020-11-24 21:13:54 +00:00
wh1te909
68dfb11155 style fix 2020-11-24 21:09:15 +00:00
wh1te909
39fc1beb89 one more nats 2020-11-24 10:08:20 +00:00
wh1te909
fe0ddec0f9 move runchecks to nats 2020-11-24 05:49:42 +00:00
wh1te909
9b52b4efd9 move wmi to nats 2020-11-24 05:14:45 +00:00
wh1te909
e90e527603 move bulk cmd/script to nats 2020-11-24 04:09:52 +00:00
wh1te909
a510854741 fix core settings for install script 2020-11-23 07:58:22 +00:00
wh1te909
8935ce4ccf move installed software to nats wh1te909/rmmagent@b5b5297350 2020-11-23 06:59:26 +00:00
wh1te909
f9edc9059a format 2020-11-23 06:15:26 +00:00
wh1te909
db8917a769 move reboot to nats 2020-11-23 05:09:06 +00:00
wh1te909
c2d70cc1c2 more nats, fix tests 2020-11-23 03:58:37 +00:00
wh1te909
3b13c7f9ce move agent uninstall to nats wh1te909/rmmagent@502cc0d3de 2020-11-23 02:19:54 +00:00
wh1te909
b7150d8026 don't update last seen in checkrunner 2020-11-23 01:25:33 +00:00
wh1te909
041830a7f8 bump quasar 2020-11-23 01:15:53 +00:00
wh1te909
a18daf0195 fix tests 2020-11-23 00:47:16 +00:00
wh1te909
5d3dfceb22 remove extra logger 2020-11-23 00:46:48 +00:00
wh1te909
c82855e732 remove travis 2020-11-22 23:37:01 +00:00
wh1te909
956f156018 notify if agent not supported 2020-11-22 23:31:59 +00:00
wh1te909
9b13c35e7f nats winsvc check 2020-11-22 23:06:52 +00:00
wh1te909
bc8e637bba add port 4222 to instructions 2020-11-22 22:50:59 +00:00
wh1te909
f03c28c906 mesh nats 2020-11-22 11:29:47 +00:00
wh1te909
e4b1f39fdc move run script to nats 2020-11-22 10:32:21 +00:00
wh1te909
4780af910c add nats recovery 2020-11-22 04:09:23 +00:00
wh1te909
d61ce5c524 move edit winsvc to nats wh1te909/rmmagent@88085847a5 2020-11-22 00:39:56 +00:00
wh1te909
20ab151f4d start moving win svcs to nats wh1te909/rmmagent@d2c9ec7f6d 2020-11-21 23:14:24 +00:00
wh1te909
8a7be7543a Merge branch 'nats' into develop 2020-11-21 04:00:21 +00:00
wh1te909
3f806aec9c fix scripts 2020-11-21 03:37:39 +00:00
wh1te909
6c273b32bb switch axios url 2020-11-21 03:21:16 +00:00
wh1te909
b986f9d6ee add missing escape 2020-11-21 03:07:59 +00:00
wh1te909
c98cca6b7b Merge branch 'develop' of https://github.com/wh1te909/tacticalrmm into develop 2020-11-21 02:47:41 +00:00
wh1te909
fbec78ede5 cut down on nginx logging 2020-11-21 02:47:23 +00:00
sadnub
c1d9a2d1f1 certificate fixes and mesh setup improvements 2020-11-20 16:42:12 -05:00
sadnub
8a10036f32 fix tests 2020-11-20 10:30:07 -05:00
sadnub
924a3aec0e Update readme.md 2020-11-20 10:21:45 -05:00
sadnub
3b3ac31541 fix certificates 2020-11-20 09:46:05 -05:00
wh1te909
e0cb2f9d0f add new agent update method wh1te909/rmmagent@9ede622837 2020-11-20 09:23:38 +00:00
wh1te909
549b4edb59 self update the update script 2020-11-20 08:09:58 +00:00
Tragic Bronson
67c912aca2 Merge pull request #186 from sadnub/develop
New Docker Setup
2020-11-19 22:59:03 -08:00
sadnub
a74dde5d9e attempt tests fix 2020-11-20 00:04:28 -05:00
sadnub
f7bcd24726 working docker setup with Dockerhub 2020-11-19 23:34:50 -05:00
sadnub
337c900770 fix merge conflicts 2020-11-19 19:18:33 -05:00
Josh Krawczyk
e83e73ead4 finished up docker 2020-11-19 19:03:44 -05:00
Josh Krawczyk
9ec2f6b64d more docker changes 2020-11-18 22:42:45 -05:00
wh1te909
f970592efe eventlog 2020-11-18 07:35:57 +00:00
sadnub
7592c11e99 more docker changes 2020-11-17 23:39:03 -05:00
sadnub
759b05e137 more docker changes 2020-11-17 22:56:16 -05:00
wh1te909
42ebd9ffce procs, cmd and eventlog nats 2020-11-17 08:25:56 +00:00
Josh Krawczyk
bc0fc33966 more docker additions 2020-11-16 21:22:28 -05:00
Josh Krawczyk
f4aab16e39 more docker changes 2020-11-16 14:28:10 -05:00
wh1te909
e91425287c start nats 2020-11-15 07:40:26 +00:00
sadnub
f05908f570 docker rework start 2020-11-14 16:55:05 -05:00
96 changed files with 3066 additions and 3185 deletions

2
.gitignore vendored
View File

@@ -42,4 +42,6 @@ api/tacticalrmm/accounts/management/commands/random_data.py
versioninfo.go
resource.syso
htmlcov/
docker-compose.dev.yml
docs/.vuepress/dist
nats-rmm.conf

View File

@@ -1,43 +0,0 @@
dist: focal
matrix:
include:
- language: node_js
node_js: "12"
before_install:
- cd web
install:
- npm install
script:
- npm run test:unit
- language: python
python: "3.8"
services:
- redis
addons:
postgresql: "13"
apt:
packages:
- postgresql-13
before_script:
- psql -c 'CREATE DATABASE travisci;' -U postgres
- psql -c "CREATE USER travisci WITH PASSWORD 'travisSuperSekret6645';" -U postgres
- psql -c 'GRANT ALL PRIVILEGES ON DATABASE travisci TO travisci;' -U postgres
- psql -c 'ALTER USER travisci CREATEDB;' -U postgres
before_install:
- cd api/tacticalrmm
install:
- pip install --no-cache-dir --upgrade pip
- pip install --no-cache-dir setuptools==49.6.0 wheel==0.35.1
- pip install --no-cache-dir -r requirements.txt -r requirements-test.txt
script:
- coverage run manage.py test -v 2
after_success:
- coveralls

View File

@@ -1,6 +1,5 @@
# Tactical RMM
[![Build Status](https://travis-ci.com/wh1te909/tacticalrmm.svg?branch=develop)](https://travis-ci.com/wh1te909/tacticalrmm)
[![Build Status](https://dev.azure.com/dcparsi/Tactical%20RMM/_apis/build/status/wh1te909.tacticalrmm?branchName=develop)](https://dev.azure.com/dcparsi/Tactical%20RMM/_build/latest?definitionId=4&branchName=develop)
[![Coverage Status](https://coveralls.io/repos/github/wh1te909/tacticalrmm/badge.png?branch=develop&kill_cache=1)](https://coveralls.io/github/wh1te909/tacticalrmm?branch=develop)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
@@ -64,6 +63,7 @@ sudo ufw allow ssh
sudo ufw allow http
sudo ufw allow https
sudo ufw allow proto tcp from any to any port 4505,4506
sudo ufw allow proto tcp from any to any port 4222
sudo ufw enable && sudo ufw reload
```
@@ -78,7 +78,7 @@ Create A record ```mesh.tacticalrmm.com``` for meshcentral
Download the install script and run it
```
wget https://raw.githubusercontent.com/wh1te909/tacticalrmm/develop/install.sh
wget https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/install.sh
chmod +x install.sh
./install.sh
```
@@ -92,17 +92,17 @@ chmod +x install.sh
From the app's dashboard, choose Agents > Install Agent to generate an installer.
## Updating
Download and run [update.sh](./update.sh) ([Raw](https://raw.githubusercontent.com/wh1te909/tacticalrmm/develop/update.sh))
Download and run [update.sh](https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/update.sh)
```
wget https://raw.githubusercontent.com/wh1te909/tacticalrmm/develop/update.sh
wget https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/update.sh
chmod +x update.sh
./update.sh
```
## Backup
Download [backup.sh](./backup.sh) ([Raw](https://raw.githubusercontent.com/wh1te909/tacticalrmm/develop/backup.sh))
Download [backup.sh](https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/backup.sh)
```
wget https://raw.githubusercontent.com/wh1te909/tacticalrmm/develop/backup.sh
wget https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/backup.sh
```
Change the postgres username and password at the top of the file (you can find them in `/rmm/api/tacticalrmm/tacticalrmm/local_settings.py` under the DATABASES section)
@@ -121,7 +121,7 @@ Copy backup file to new server
Download the restore script, and edit the postgres username/password at the top of the file. Same instructions as above in the backup steps.
```
wget https://raw.githubusercontent.com/wh1te909/tacticalrmm/develop/restore.sh
wget https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/restore.sh
```
Run the restore script, passing it the backup tar file as the first argument
@@ -129,14 +129,3 @@ Run the restore script, passing it the backup tar file as the first argument
chmod +x restore.sh
./restore.sh rmm-backup-xxxxxxx.tar
```
## Using another ssl certificate
During the install you can opt out of using the Let's Encrypt certificate. If you do this the script will create a self-signed certificate, so that https continues to work. You can replace the certificates in /certs/example.com/(privkey.pem | pubkey.pem) with your own.
If you are migrating from Let's Encrypt to another certificate provider, you can create the /certs directory and copy your certificates there. It is recommended to do this because this directory will be backed up with the backup script provided. Then modify the nginx configurations to use your new certificates
The cert that is generated is a wildcard certificate and is used in the nginx configurations: rmm.conf, api.conf, and mesh.conf. If you can't generate wildcard certificates you can create a cert for each subdomain and configure each nginx configuration file to use its own certificate. Then restart nginx:
```
sudo systemctl restart nginx
```

View File

@@ -26,6 +26,7 @@ def get_wmi_data():
agent = Recipe(
Agent,
hostname="DESKTOP-TEST123",
version="1.1.0",
monitoring_type=cycle(["workstation", "server"]),
salt_id=generate_agent_id("DESKTOP-TEST123"),
agent_id="71AHC-AA813-HH1BC-AAHH5-00013|DESKTOP-TEST123",

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.3 on 2020-11-22 04:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agents', '0024_auto_20201101_2319'),
]
operations = [
migrations.AlterField(
model_name='recoveryaction',
name='mode',
field=models.CharField(choices=[('salt', 'Salt'), ('mesh', 'Mesh'), ('command', 'Command'), ('rpc', 'Nats RPC')], default='mesh', max_length=50),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.3 on 2020-11-25 23:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agents', '0025_auto_20201122_0407'),
]
operations = [
migrations.AlterField(
model_name='recoveryaction',
name='mode',
field=models.CharField(choices=[('salt', 'Salt'), ('mesh', 'Mesh'), ('command', 'Command'), ('rpc', 'Nats RPC'), ('checkrunner', 'Checkrunner')], default='mesh', max_length=50),
),
]

View File

@@ -7,6 +7,7 @@ from Crypto.Random import get_random_bytes
from Crypto.Hash import SHA3_384
from Crypto.Util.Padding import pad
import validators
import msgpack
import random
import re
import string
@@ -14,6 +15,8 @@ from collections import Counter
from loguru import logger
from packaging import version as pyver
from distutils.version import LooseVersion
from nats.aio.client import Client as NATS
from nats.aio.errors import ErrTimeout
from django.db import models
from django.conf import settings
@@ -82,6 +85,10 @@ class Agent(BaseAuditModel):
def client(self):
return self.site.client
@property
def has_nats(self):
return pyver.parse(self.version) >= pyver.parse("1.1.0")
@property
def timezone(self):
# return the default timezone unless the timezone is explicity set per agent
@@ -142,11 +149,7 @@ class Agent(BaseAuditModel):
@property
def has_patches_pending(self):
if self.winupdates.filter(action="approve").filter(installed=False).exists():
return True
else:
return False
return self.winupdates.filter(action="approve").filter(installed=False).exists()
@property
def checks(self):
@@ -433,6 +436,37 @@ class Agent(BaseAuditModel):
except Exception:
return "err"
async def nats_cmd(self, data, timeout=30, wait=True):
nc = NATS()
options = {
"servers": f"tls://{settings.ALLOWED_HOSTS[0]}:4222",
"user": "tacticalrmm",
"password": settings.SECRET_KEY,
"connect_timeout": 3,
"max_reconnect_attempts": 2,
}
try:
await nc.connect(**options)
except:
return "natsdown"
if wait:
try:
msg = await nc.request(
self.agent_id, msgpack.dumps(data), timeout=timeout
)
except ErrTimeout:
ret = "timeout"
else:
ret = msgpack.loads(msg.data)
await nc.close()
return ret
else:
await nc.publish(self.agent_id, msgpack.dumps(data))
await nc.flush()
await nc.close()
def salt_api_cmd(self, **kwargs):
# salt should always timeout first before the requests' timeout
@@ -592,10 +626,7 @@ class Agent(BaseAuditModel):
return "failed"
def not_supported(self, version_added):
if pyver.parse(self.version) < pyver.parse(version_added):
return True
return False
return pyver.parse(self.version) < pyver.parse(version_added)
def delete_superseded_updates(self):
try:
@@ -721,6 +752,8 @@ RECOVERY_CHOICES = [
("salt", "Salt"),
("mesh", "Mesh"),
("command", "Command"),
("rpc", "Nats RPC"),
("checkrunner", "Checkrunner"),
]

View File

@@ -1,3 +1,4 @@
import asyncio
from loguru import logger
from time import sleep
import random
@@ -11,6 +12,7 @@ from django.conf import settings
from tacticalrmm.celery import app
from agents.models import Agent, AgentOutage
from core.models import CoreSettings
from logs.models import PendingAction
logger.configure(**settings.LOG_CONFIG)
@@ -53,6 +55,25 @@ def send_agent_update_task(pks, version):
logger.info(
f"Updating {agent.salt_id} current version {agent.version} using {inno}"
)
if agent.has_nats:
if agent.pendingactions.filter(
action_type="agentupdate", status="pending"
).exists():
continue
PendingAction.objects.create(
agent=agent,
action_type="agentupdate",
details={
"url": agent.winagent_dl,
"version": settings.LATEST_AGENT_VER,
"inno": agent.win_inno_exe,
},
)
# TODO
# Salt is deprecated, remove this once salt is gone
else:
r = agent.salt_api_async(
func="win_agent.do_agent_update_v2",
kwargs={
@@ -60,7 +81,6 @@ def send_agent_update_task(pks, version):
"url": url,
},
)
logger.info(f"{agent.salt_id}: {r}")
sleep(10)
@@ -107,6 +127,25 @@ def auto_self_agent_update_task():
logger.info(
f"Updating {agent.salt_id} current version {agent.version} using {inno}"
)
if agent.has_nats:
if agent.pendingactions.filter(
action_type="agentupdate", status="pending"
).exists():
continue
PendingAction.objects.create(
agent=agent,
action_type="agentupdate",
details={
"url": agent.winagent_dl,
"version": settings.LATEST_AGENT_VER,
"inno": agent.win_inno_exe,
},
)
# TODO
# Salt is deprecated, remove this once salt is gone
else:
r = agent.salt_api_async(
func="win_agent.do_agent_update_v2",
kwargs={
@@ -114,7 +153,6 @@ def auto_self_agent_update_task():
"url": url,
},
)
logger.info(f"{agent.salt_id}: {r}")
sleep(10)
@@ -140,7 +178,11 @@ def update_salt_minion_task():
@app.task
def get_wmi_detail_task(pk):
agent = Agent.objects.get(pk=pk)
r = agent.salt_api_async(timeout=30, func="win_agent.local_sys_info")
if agent.has_nats:
asyncio.run(agent.nats_cmd({"func": "sysinfo"}, wait=False))
else:
agent.salt_api_async(timeout=30, func="win_agent.local_sys_info")
return "ok"
@@ -160,7 +202,7 @@ def sync_salt_modules_task(pk):
def batch_sync_modules_task():
# sync modules, split into chunks of 50 agents to not overload salt
agents = Agent.objects.all()
online = [i.salt_id for i in agents if i.status == "online"]
online = [i.salt_id for i in agents]
chunks = (online[i : i + 50] for i in range(0, len(online), 50))
for chunk in chunks:
Agent.salt_batch_async(minions=chunk, func="saltutil.sync_modules")
@@ -171,15 +213,19 @@ def batch_sync_modules_task():
def batch_sysinfo_task():
# update system info using WMI
agents = Agent.objects.all()
online = [
i.salt_id
for i in agents
if not i.not_supported("0.11.0") and i.status == "online"
agents_nats = [agent for agent in agents if agent.has_nats]
minions = [
agent.salt_id
for agent in agents
if not agent.has_nats and pyver.parse(agent.version) >= pyver.parse("0.11.0")
]
chunks = (online[i : i + 30] for i in range(0, len(online), 30))
for chunk in chunks:
Agent.salt_batch_async(minions=chunk, func="win_agent.local_sys_info")
sleep(10)
if minions:
Agent.salt_batch_async(minions=minions, func="win_agent.local_sys_info")
for agent in agents_nats:
asyncio.run(agent.nats_cmd({"func": "sysinfo"}, wait=False))
@app.task

View File

@@ -7,9 +7,7 @@ from itertools import cycle
from django.conf import settings
from django.utils import timezone as djangotime
from rest_framework.authtoken.models import Token
from accounts.models import User
from tacticalrmm.test import TacticalTestCase
from .serializers import AgentSerializer
from winupdate.serializers import WinUpdatePolicySerializer
@@ -34,7 +32,9 @@ class TestAgentViews(TacticalTestCase):
client = baker.make("clients.Client", name="Google")
site = baker.make("clients.Site", client=client, name="LA Office")
self.agent = baker.make_recipe("agents.online_agent", site=site)
self.agent = baker.make_recipe(
"agents.online_agent", site=site, version="1.1.0"
)
baker.make_recipe("winupdate.winupdate_policy", agent=self.agent)
def test_get_patch_policy(self):
@@ -81,29 +81,29 @@ class TestAgentViews(TacticalTestCase):
self.check_not_authenticated("post", url)
@patch("agents.models.Agent.salt_api_cmd")
def test_ping(self, mock_ret):
@patch("agents.models.Agent.nats_cmd")
def test_ping(self, nats_cmd):
url = f"/agents/{self.agent.pk}/ping/"
mock_ret.return_value = "timeout"
nats_cmd.return_value = "timeout"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
ret = {"name": self.agent.hostname, "status": "offline"}
self.assertEqual(r.json(), ret)
mock_ret.return_value = "error"
nats_cmd.return_value = "natsdown"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
ret = {"name": self.agent.hostname, "status": "offline"}
self.assertEqual(r.json(), ret)
mock_ret.return_value = True
nats_cmd.return_value = "pong"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
ret = {"name": self.agent.hostname, "status": "online"}
self.assertEqual(r.json(), ret)
mock_ret.return_value = False
nats_cmd.return_value = "asdasjdaksdasd"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
ret = {"name": self.agent.hostname, "status": "offline"}
@@ -111,39 +111,23 @@ class TestAgentViews(TacticalTestCase):
self.check_not_authenticated("get", url)
@patch("agents.models.Agent.nats_cmd")
@patch("agents.tasks.uninstall_agent_task.delay")
def test_uninstall(self, mock_task):
@patch("agents.views.reload_nats")
def test_uninstall(self, reload_nats, mock_task, nats_cmd):
url = "/agents/uninstall/"
data = {"pk": self.agent.pk}
r = self.client.delete(url, data, format="json")
self.assertEqual(r.status_code, 200)
nats_cmd.assert_called_with({"func": "uninstall"}, wait=False)
reload_nats.assert_called_once()
mock_task.assert_called_with(self.agent.salt_id)
self.check_not_authenticated("delete", url)
@patch("agents.tasks.uninstall_agent_task.delay")
def test_uninstall_catch_no_user(self, mock_task):
# setup data
agent_user = User.objects.create_user(
username=self.agent.agent_id, password=User.objects.make_random_password(60)
)
agent_token = Token.objects.create(user=agent_user)
url = "/agents/uninstall/"
data = {"pk": self.agent.pk}
agent_user.delete()
r = self.client.delete(url, data, format="json")
self.assertEqual(r.status_code, 200)
mock_task.assert_called_with(self.agent.salt_id)
self.check_not_authenticated("delete", url)
@patch("agents.models.Agent.salt_api_cmd")
@patch("agents.models.Agent.nats_cmd")
def test_get_processes(self, mock_ret):
url = f"/agents/{self.agent.pk}/getprocs/"
@@ -163,82 +147,61 @@ class TestAgentViews(TacticalTestCase):
r = self.client.get(url)
self.assertEqual(r.status_code, 400)
mock_ret.return_value = "error"
r = self.client.get(url)
self.assertEqual(r.status_code, 400)
self.check_not_authenticated("get", url)
@patch("agents.models.Agent.salt_api_cmd")
def test_kill_proc(self, mock_ret):
@patch("agents.models.Agent.nats_cmd")
def test_kill_proc(self, nats_cmd):
url = f"/agents/{self.agent.pk}/8234/killproc/"
mock_ret.return_value = True
nats_cmd.return_value = "ok"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
mock_ret.return_value = False
nats_cmd.return_value = "timeout"
r = self.client.get(url)
self.assertEqual(r.status_code, 400)
mock_ret.return_value = "timeout"
r = self.client.get(url)
self.assertEqual(r.status_code, 400)
mock_ret.return_value = "error"
nats_cmd.return_value = "process doesn't exist"
r = self.client.get(url)
self.assertEqual(r.status_code, 400)
self.check_not_authenticated("get", url)
@patch("agents.models.Agent.salt_api_cmd")
@patch("agents.models.Agent.nats_cmd")
def test_get_event_log(self, mock_ret):
url = f"/agents/{self.agent.pk}/geteventlog/Application/30/"
with open(
os.path.join(settings.BASE_DIR, "tacticalrmm/test_data/eventlograw.json")
os.path.join(settings.BASE_DIR, "tacticalrmm/test_data/appeventlog.json")
) as f:
mock_ret.return_value = json.load(f)
with open(
os.path.join(settings.BASE_DIR, "tacticalrmm/test_data/appeventlog.json")
) as f:
decoded = json.load(f)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(decoded, r.json())
mock_ret.return_value = "timeout"
r = self.client.get(url)
self.assertEqual(r.status_code, 400)
mock_ret.return_value = "error"
r = self.client.get(url)
self.assertEqual(r.status_code, 400)
self.check_not_authenticated("get", url)
@patch("agents.models.Agent.salt_api_cmd")
def test_power_action(self, mock_ret):
@patch("agents.models.Agent.nats_cmd")
def test_power_action(self, nats_cmd):
url = f"/agents/poweraction/"
data = {"pk": self.agent.pk, "action": "rebootnow"}
mock_ret.return_value = True
nats_cmd.return_value = "ok"
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
nats_cmd.assert_called_with({"func": "rebootnow"}, timeout=10)
mock_ret.return_value = "error"
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 400)
mock_ret.return_value = False
nats_cmd.return_value = "timeout"
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 400)
self.check_not_authenticated("post", url)
@patch("agents.models.Agent.salt_api_cmd")
@patch("agents.models.Agent.nats_cmd")
def test_send_raw_cmd(self, mock_ret):
url = f"/agents/sendrawcmd/"
@@ -257,10 +220,6 @@ class TestAgentViews(TacticalTestCase):
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 400)
mock_ret.return_value = False
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 400)
self.check_not_authenticated("post", url)
@patch("agents.models.Agent.salt_api_cmd")
@@ -569,12 +528,14 @@ class TestAgentViews(TacticalTestCase):
self.check_not_authenticated("get", url)
@patch("winupdate.tasks.bulk_check_for_updates_task.delay")
@patch("scripts.tasks.handle_bulk_script_task.delay")
@patch("scripts.tasks.handle_bulk_command_task.delay")
@patch("agents.models.Agent.salt_batch_async")
def test_bulk_cmd_script(self, mock_ret, mock_update):
def test_bulk_cmd_script(
self, salt_batch_async, bulk_command, bulk_script, mock_update
):
url = "/agents/bulk/"
mock_ret.return_value = "ok"
payload = {
"mode": "command",
"target": "agents",
@@ -589,6 +550,7 @@ class TestAgentViews(TacticalTestCase):
}
r = self.client.post(url, payload, format="json")
bulk_command.assert_called_with([self.agent.pk], "gpupdate /force", "cmd", 300)
self.assertEqual(r.status_code, 200)
payload = {
@@ -620,6 +582,7 @@ class TestAgentViews(TacticalTestCase):
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 200)
bulk_command.assert_called_with([self.agent.pk], "gpupdate /force", "cmd", 300)
payload = {
"mode": "command",
@@ -636,12 +599,7 @@ class TestAgentViews(TacticalTestCase):
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 200)
mock_ret.return_value = "timeout"
payload["client"] = self.agent.client.id
payload["site"] = self.agent.site.id
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 400)
bulk_command.assert_called_with([self.agent.pk], "gpupdate /force", "cmd", 300)
payload = {
"mode": "scan",
@@ -652,9 +610,8 @@ class TestAgentViews(TacticalTestCase):
self.agent.pk,
],
}
mock_ret.return_value = "ok"
r = self.client.post(url, payload, format="json")
mock_update.assert_called_once()
mock_update.assert_called_with(minions=[self.agent.salt_id])
self.assertEqual(r.status_code, 200)
payload = {
@@ -666,6 +623,7 @@ class TestAgentViews(TacticalTestCase):
self.agent.pk,
],
}
salt_batch_async.return_value = "ok"
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 200)
@@ -681,41 +639,18 @@ class TestAgentViews(TacticalTestCase):
self.check_not_authenticated("post", url)
@patch("agents.models.Agent.salt_api_cmd")
def test_restart_mesh(self, mock_ret):
url = f"/agents/{self.agent.pk}/restartmesh/"
mock_ret.return_value = "timeout"
r = self.client.get(url)
self.assertEqual(r.status_code, 400)
mock_ret.return_value = "error"
r = self.client.get(url)
self.assertEqual(r.status_code, 400)
mock_ret.return_value = False
r = self.client.get(url)
self.assertEqual(r.status_code, 400)
mock_ret.return_value = True
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.check_not_authenticated("get", url)
@patch("agents.models.Agent.salt_api_cmd")
def test_recover_mesh(self, mock_ret):
@patch("agents.models.Agent.nats_cmd")
def test_recover_mesh(self, nats_cmd):
url = f"/agents/{self.agent.pk}/recovermesh/"
mock_ret.return_value = True
nats_cmd.return_value = "ok"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertIn(self.agent.hostname, r.data)
nats_cmd.assert_called_with(
{"func": "recover", "payload": {"mode": "mesh"}}, timeout=45
)
mock_ret.return_value = "timeout"
r = self.client.get(url)
self.assertEqual(r.status_code, 400)
mock_ret.return_value = "error"
nats_cmd.return_value = "timeout"
r = self.client.get(url)
self.assertEqual(r.status_code, 400)
@@ -804,13 +739,19 @@ class TestAgentTasks(TacticalTestCase):
self.authenticate()
self.setup_coresettings()
@patch("agents.models.Agent.nats_cmd")
@patch("agents.models.Agent.salt_api_async", return_value=None)
def test_get_wmi_detail_task(self, salt_api_async):
self.agent = baker.make_recipe("agents.agent")
ret = get_wmi_detail_task.s(self.agent.pk).apply()
def test_get_wmi_detail_task(self, salt_api_async, nats_cmd):
self.agent_salt = baker.make_recipe("agents.agent", version="1.0.2")
ret = get_wmi_detail_task.s(self.agent_salt.pk).apply()
salt_api_async.assert_called_with(timeout=30, func="win_agent.local_sys_info")
self.assertEqual(ret.status, "SUCCESS")
self.agent_nats = baker.make_recipe("agents.agent", version="1.1.0")
ret = get_wmi_detail_task.s(self.agent_nats.pk).apply()
nats_cmd.assert_called_with({"func": "sysinfo"}, wait=False)
self.assertEqual(ret.status, "SUCCESS")
@patch("agents.models.Agent.salt_api_cmd")
def test_sync_salt_modules_task(self, salt_api_cmd):
self.agent = baker.make_recipe("agents.agent")
@@ -833,7 +774,7 @@ class TestAgentTasks(TacticalTestCase):
@patch("agents.models.Agent.salt_batch_async", return_value=None)
@patch("agents.tasks.sleep", return_value=None)
def test_batch_sync_modules_task(self, mock_sleep, salt_batch_async):
# chunks of 50, 60 online should run only 2 times
# chunks of 50, should run 4 times
baker.make_recipe(
"agents.online_agent", last_seen=djangotime.now(), _quantity=60
)
@@ -843,32 +784,41 @@ class TestAgentTasks(TacticalTestCase):
_quantity=115,
)
ret = batch_sync_modules_task.s().apply()
self.assertEqual(salt_batch_async.call_count, 2)
self.assertEqual(salt_batch_async.call_count, 4)
self.assertEqual(ret.status, "SUCCESS")
@patch("agents.models.Agent.nats_cmd")
@patch("agents.models.Agent.salt_batch_async", return_value=None)
@patch("agents.tasks.sleep", return_value=None)
def test_batch_sysinfo_task(self, mock_sleep, salt_batch_async):
# chunks of 30, 70 online should run only 3 times
self.online = baker.make_recipe(
"agents.online_agent", version=settings.LATEST_AGENT_VER, _quantity=70
)
self.overdue = baker.make_recipe(
"agents.overdue_agent", version=settings.LATEST_AGENT_VER, _quantity=115
def test_batch_sysinfo_task(self, mock_sleep, salt_batch_async, nats_cmd):
self.agents_nats = baker.make_recipe(
"agents.agent", version="1.1.0", _quantity=20
)
# test nats
ret = batch_sysinfo_task.s().apply()
self.assertEqual(salt_batch_async.call_count, 3)
self.assertEqual(nats_cmd.call_count, 20)
nats_cmd.assert_called_with({"func": "sysinfo"}, wait=False)
self.assertEqual(ret.status, "SUCCESS")
self.agents_salt = baker.make_recipe(
"agents.agent", version="1.0.2", _quantity=70
)
minions = [i.salt_id for i in self.agents_salt]
ret = batch_sysinfo_task.s().apply()
self.assertEqual(salt_batch_async.call_count, 1)
salt_batch_async.assert_called_with(
minions=minions, func="win_agent.local_sys_info"
)
self.assertEqual(ret.status, "SUCCESS")
salt_batch_async.reset_mock()
[i.delete() for i in self.online]
[i.delete() for i in self.overdue]
[i.delete() for i in self.agents_salt]
# test old agents, should not run
self.online_old = baker.make_recipe(
"agents.online_agent", version="0.10.2", _quantity=70
)
self.overdue_old = baker.make_recipe(
"agents.overdue_agent", version="0.10.2", _quantity=115
self.agents_old = baker.make_recipe(
"agents.agent", version="0.10.2", _quantity=70
)
ret = batch_sysinfo_task.s().apply()
salt_batch_async.assert_not_called()

View File

@@ -25,7 +25,6 @@ urlpatterns = [
path("<int:pk>/ping/", views.ping),
path("recover/", views.recover),
path("runscript/", views.run_script),
path("<int:pk>/restartmesh/", views.restart_mesh),
path("<int:pk>/recovermesh/", views.recover_mesh),
path("<int:pk>/notes/", views.GetAddNotes.as_view()),
path("<int:pk>/note/", views.GetEditDeleteNote.as_view()),

View File

@@ -1,9 +1,7 @@
import asyncio
from loguru import logger
import os
import subprocess
import zlib
import json
import base64
import pytz
import datetime as dt
from packaging import version as pyver
@@ -18,9 +16,6 @@ from rest_framework.response import Response
from rest_framework import status, generics
from .models import Agent, AgentOutage, RecoveryAction, Note
from winupdate.models import WinUpdatePolicy
from clients.models import Client, Site
from accounts.models import User
from core.models import CoreSettings
from scripts.models import Script
from logs.models import AuditLog
@@ -37,9 +32,9 @@ from winupdate.serializers import WinUpdatePolicySerializer
from .tasks import uninstall_agent_task, send_agent_update_task
from winupdate.tasks import bulk_check_for_updates_task
from scripts.tasks import run_script_bg_task, run_bulk_script_task
from scripts.tasks import handle_bulk_command_task, handle_bulk_script_task
from tacticalrmm.utils import notify_error
from tacticalrmm.utils import notify_error, reload_nats
logger.configure(**settings.LOG_CONFIG)
@@ -66,24 +61,30 @@ def update_agents(request):
@api_view()
def ping(request, pk):
agent = get_object_or_404(Agent, pk=pk)
r = agent.salt_api_cmd(timeout=5, func="test.ping")
if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
r = asyncio.run(agent.nats_cmd({"func": "ping"}, timeout=10))
if r == "timeout" or r == "error":
if r == "timeout" or r == "natsdown":
return Response({"name": agent.hostname, "status": "offline"})
if isinstance(r, bool) and r:
elif r == "pong":
return Response({"name": agent.hostname, "status": "online"})
else:
return Response({"name": agent.hostname, "status": "offline"})
@api_view(["DELETE"])
def uninstall(request):
agent = get_object_or_404(Agent, pk=request.data["pk"])
if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
asyncio.run(agent.nats_cmd({"func": "uninstall"}, wait=False))
salt_id = agent.salt_id
name = agent.hostname
agent.delete()
reload_nats()
uninstall_agent_task.delay(salt_id)
return Response(f"{name} will now be uninstalled.")
@@ -153,12 +154,11 @@ def agent_detail(request, pk):
@api_view()
def get_processes(request, pk):
agent = get_object_or_404(Agent, pk=pk)
r = agent.salt_api_cmd(timeout=20, func="win_agent.get_procs")
if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
r = asyncio.run(agent.nats_cmd(data={"func": "procs"}, timeout=5))
if r == "timeout":
return notify_error("Unable to contact the agent")
elif r == "error":
return notify_error("Something went wrong")
return Response(r)
@@ -166,15 +166,17 @@ def get_processes(request, pk):
@api_view()
def kill_proc(request, pk, pid):
agent = get_object_or_404(Agent, pk=pk)
r = agent.salt_api_cmd(timeout=25, func="ps.kill_pid", arg=int(pid))
if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
r = asyncio.run(
agent.nats_cmd({"func": "killproc", "procpid": int(pid)}, timeout=15)
)
if r == "timeout":
return notify_error("Unable to contact the agent")
elif r == "error":
return notify_error("Something went wrong")
if isinstance(r, bool) and not r:
return notify_error("Unable to kill the process")
elif r != "ok":
return notify_error(r)
return Response("ok")
@@ -182,32 +184,31 @@ def kill_proc(request, pk, pid):
@api_view()
def get_event_log(request, pk, logtype, days):
agent = get_object_or_404(Agent, pk=pk)
r = agent.salt_api_cmd(
timeout=30,
func="win_agent.get_eventlog",
arg=[logtype, int(days)],
)
if r == "timeout" or r == "error":
if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
data = {
"func": "eventlog",
"timeout": 30,
"payload": {
"logname": logtype,
"days": str(days),
},
}
r = asyncio.run(agent.nats_cmd(data, timeout=32))
if r == "timeout":
return notify_error("Unable to contact the agent")
return Response(json.loads(zlib.decompress(base64.b64decode(r["wineventlog"]))))
return Response(r)
@api_view(["POST"])
def power_action(request):
pk = request.data["pk"]
action = request.data["action"]
agent = get_object_or_404(Agent, pk=pk)
if action == "rebootnow":
logger.info(f"{agent.hostname} was scheduled for immediate reboot")
r = agent.salt_api_cmd(
timeout=30,
func="system.reboot",
arg=3,
kwargs={"in_seconds": True},
)
if r == "timeout" or r == "error" or (isinstance(r, bool) and not r):
agent = get_object_or_404(Agent, pk=request.data["pk"])
if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
if request.data["action"] == "rebootnow":
r = asyncio.run(agent.nats_cmd({"func": "rebootnow"}, timeout=10))
if r != "ok":
return notify_error("Unable to contact the agent")
return Response("ok")
@@ -216,21 +217,21 @@ def power_action(request):
@api_view(["POST"])
def send_raw_cmd(request):
agent = get_object_or_404(Agent, pk=request.data["pk"])
r = agent.salt_api_cmd(
timeout=request.data["timeout"],
func="cmd.run",
kwargs={
"cmd": request.data["cmd"],
if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
timeout = int(request.data["timeout"])
data = {
"func": "rawcmd",
"timeout": timeout,
"payload": {
"command": request.data["cmd"],
"shell": request.data["shell"],
"timeout": request.data["timeout"],
},
)
}
r = asyncio.run(agent.nats_cmd(data, timeout=timeout + 2))
if r == "timeout":
return notify_error("Unable to contact the agent")
elif r == "error" or not r:
return notify_error("Something went wrong")
AuditLog.audit_raw_command(
username=request.user.username,
@@ -239,7 +240,6 @@ def send_raw_cmd(request):
shell=request.data["shell"],
)
logger.info(f"The command {request.data['cmd']} was sent on agent {agent.hostname}")
return Response(r)
@@ -636,35 +636,60 @@ def install_agent(request):
@api_view(["POST"])
def recover(request):
agent = get_object_or_404(Agent, pk=request.data["pk"])
mode = request.data["mode"]
if pyver.parse(agent.version) <= pyver.parse("0.9.5"):
return notify_error("Only available in agent version greater than 0.9.5")
if not agent.has_nats:
if mode == "tacagent" or mode == "checkrunner" or mode == "rpc":
return notify_error("Requires agent version 1.1.0 or greater")
# attempt a realtime recovery if supported, otherwise fall back to old recovery method
if agent.has_nats:
if (
mode == "tacagent"
or mode == "checkrunner"
or mode == "salt"
or mode == "mesh"
):
data = {"func": "recover", "payload": {"mode": mode}}
r = asyncio.run(agent.nats_cmd(data, timeout=10))
if r == "ok":
return Response("Successfully completed recovery")
if agent.recoveryactions.filter(last_run=None).exists():
return notify_error(
"A recovery action is currently pending. Please wait for the next agent check-in."
)
if request.data["mode"] == "command" and not request.data["cmd"]:
if mode == "command" and not request.data["cmd"]:
return notify_error("Command is required")
# if we've made it this far and realtime recovery didn't work,
# tacagent service is the fallback recovery so we obv can't use that to recover itself if it's down
if mode == "tacagent":
return notify_error(
"Requires RPC service to be functional. Please recover that first"
)
# we should only get here if all other methods fail
RecoveryAction(
agent=agent,
mode=request.data["mode"],
command=request.data["cmd"] if request.data["mode"] == "command" else None,
mode=mode,
command=request.data["cmd"] if mode == "command" else None,
).save()
return Response(f"Recovery will be attempted on the agent's next check-in")
return Response("Recovery will be attempted on the agent's next check-in")
@api_view(["POST"])
def run_script(request):
agent = get_object_or_404(Agent, pk=request.data["pk"])
if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
script = get_object_or_404(Script, pk=request.data["scriptPK"])
output = request.data["output"]
args = request.data["args"]
req_timeout = int(request.data["timeout"]) + 3
AuditLog.audit_script_run(
@@ -673,75 +698,33 @@ def run_script(request):
script=script.name,
)
if output == "wait":
r = agent.salt_api_cmd(
timeout=req_timeout,
func="win_agent.run_script",
kwargs={
"filepath": script.filepath,
"filename": script.filename,
"shell": script.shell,
"timeout": request.data["timeout"],
"args": args,
},
)
if isinstance(r, dict):
if r["stdout"]:
return Response(r["stdout"])
elif r["stderr"]:
return Response(r["stderr"])
else:
try:
r["retcode"]
except KeyError:
return notify_error("Something went wrong")
return Response(f"Return code: {r['retcode']}")
else:
if r == "timeout":
return notify_error("Unable to contact the agent")
elif r == "error":
return notify_error("Something went wrong")
else:
return notify_error(str(r))
else:
data = {
"agentpk": agent.pk,
"scriptpk": script.pk,
"func": "runscript",
"timeout": request.data["timeout"],
"args": args,
"script_args": request.data["args"],
"payload": {
"code": script.code,
"shell": script.shell,
},
}
run_script_bg_task.delay(data)
return Response(f"{script.name} will now be run on {agent.hostname}")
@api_view()
def restart_mesh(request, pk):
agent = get_object_or_404(Agent, pk=pk)
r = agent.salt_api_cmd(func="service.restart", arg="mesh agent", timeout=30)
if r == "timeout" or r == "error":
return notify_error("Unable to contact the agent")
elif isinstance(r, bool) and r:
return Response(f"Restarted Mesh Agent on {agent.hostname}")
if output == "wait":
r = asyncio.run(agent.nats_cmd(data, timeout=req_timeout))
return Response(r)
else:
return notify_error(f"Failed to restart the Mesh Agent on {agent.hostname}")
asyncio.run(agent.nats_cmd(data, wait=False))
return Response(f"{script.name} will now be run on {agent.hostname}")
@api_view()
def recover_mesh(request, pk):
agent = get_object_or_404(Agent, pk=pk)
r = agent.salt_api_cmd(
timeout=60,
func="cmd.run",
kwargs={
"cmd": r'"C:\\Program Files\\TacticalAgent\\tacticalrmm.exe" -m recovermesh',
"timeout": 55,
},
)
if r == "timeout" or r == "error":
if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
data = {"func": "recover", "payload": {"mode": "mesh"}}
r = asyncio.run(agent.nats_cmd(data, timeout=45))
if r != "ok":
return notify_error("Unable to contact the agent")
return Response(f"Repaired mesh agent on {agent.hostname}")
@@ -805,73 +788,44 @@ def bulk(request):
return notify_error("Must select at least 1 agent")
if request.data["target"] == "client":
agents = Agent.objects.filter(site__client_id=request.data["client"])
q = Agent.objects.filter(site__client_id=request.data["client"])
elif request.data["target"] == "site":
agents = Agent.objects.filter(site_id=request.data["site"])
q = Agent.objects.filter(site_id=request.data["site"])
elif request.data["target"] == "agents":
agents = Agent.objects.filter(pk__in=request.data["agentPKs"])
q = Agent.objects.filter(pk__in=request.data["agentPKs"])
elif request.data["target"] == "all":
agents = Agent.objects.all()
q = Agent.objects.all()
else:
return notify_error("Something went wrong")
minions = [agent.salt_id for agent in agents]
minions = [agent.salt_id for agent in q]
agents = [agent.pk for agent in q]
AuditLog.audit_bulk_action(request.user, request.data["mode"], request.data)
if request.data["mode"] == "command":
r = Agent.salt_batch_async(
minions=minions,
func="cmd.run_bg",
kwargs={
"cmd": request.data["cmd"],
"shell": request.data["shell"],
"timeout": request.data["timeout"],
},
handle_bulk_command_task.delay(
agents, request.data["cmd"], request.data["shell"], request.data["timeout"]
)
if r == "timeout":
return notify_error("Salt API not running")
return Response(f"Command will now be run on {len(minions)} agents")
return Response(f"Command will now be run on {len(agents)} agents")
elif request.data["mode"] == "script":
script = get_object_or_404(Script, pk=request.data["scriptPK"])
if script.shell == "python":
r = Agent.salt_batch_async(
minions=minions,
func="win_agent.run_script",
kwargs={
"filepath": script.filepath,
"filename": script.filename,
"shell": script.shell,
"timeout": request.data["timeout"],
"args": request.data["args"],
"bg": True,
},
handle_bulk_script_task.delay(
script.pk, agents, request.data["args"], request.data["timeout"]
)
if r == "timeout":
return notify_error("Salt API not running")
else:
data = {
"minions": minions,
"scriptpk": script.pk,
"timeout": request.data["timeout"],
"args": request.data["args"],
}
run_bulk_script_task.delay(data)
return Response(f"{script.name} will now be run on {len(minions)} agents")
return Response(f"{script.name} will now be run on {len(agents)} agents")
elif request.data["mode"] == "install":
r = Agent.salt_batch_async(minions=minions, func="win_agent.install_updates")
if r == "timeout":
return notify_error("Salt API not running")
return Response(
f"Pending updates will now be installed on {len(minions)} agents"
f"Pending updates will now be installed on {len(agents)} agents"
)
elif request.data["mode"] == "scan":
bulk_check_for_updates_task.delay(minions=minions)
return Response(f"Patch status scan will now run on {len(minions)} agents")
return Response(f"Patch status scan will now run on {len(agents)} agents")
return notify_error("Something went wrong")

View File

@@ -1,3 +1,4 @@
import asyncio
import os
import requests
from loguru import logger
@@ -33,7 +34,7 @@ from agents.tasks import (
from winupdate.tasks import check_for_updates_task
from software.tasks import get_installed_software, install_chocolatey
from checks.utils import bytes2human
from tacticalrmm.utils import notify_error
from tacticalrmm.utils import notify_error, reload_nats
logger.configure(**settings.LOG_CONFIG)
@@ -97,6 +98,17 @@ class Hello(APIView):
recovery.save(update_fields=["last_run"])
return Response(recovery.send())
# handle agent update
if agent.pendingactions.filter(
action_type="agentupdate", status="pending"
).exists():
update = agent.pendingactions.filter(
action_type="agentupdate", status="pending"
).last()
update.status = "completed"
update.save(update_fields=["status"])
return Response(update.details)
# get any pending actions
if agent.pendingactions.filter(status="pending").exists():
agent.handle_pending_actions()
@@ -133,8 +145,6 @@ class CheckRunner(APIView):
def get(self, request, agentid):
agent = get_object_or_404(Agent, agent_id=agentid)
agent.last_seen = djangotime.now()
agent.save(update_fields=["last_seen"])
checks = Check.objects.filter(agent__pk=agent.pk, overriden_by_policy=False)
ret = {
@@ -333,21 +343,16 @@ class WinUpdater(APIView):
agent.save(update_fields=["needs_reboot"])
if reboot:
r = agent.salt_api_cmd(
timeout=15,
if agent.has_nats:
asyncio.run(agent.nats_cmd({"func": "rebootnow"}, wait=False))
else:
agent.salt_api_async(
func="system.reboot",
arg=7,
kwargs={"in_seconds": True},
)
if r == "timeout" or r == "error" or (isinstance(r, bool) and not r):
check_for_updates_task.apply_async(
queue="wupdate", kwargs={"pk": agent.pk, "wait": False}
)
else:
logger.info(
f"{agent.hostname} is rebooting after updates were installed."
)
logger.info(f"{agent.hostname} is rebooting after updates were installed.")
else:
check_for_updates_task.apply_async(
queue="wupdate", kwargs={"pk": agent.pk, "wait": False}
@@ -448,6 +453,8 @@ class NewAgent(APIView):
else:
WinUpdatePolicy(agent=agent).save()
reload_nats()
# Generate policies for new agent
agent.generate_checks_from_policies()
agent.generate_tasks_from_policies()

View File

@@ -206,6 +206,7 @@ def delete_win_task_schedule(pk, pending_action=False):
@app.task
def run_win_task(pk):
# TODO deprecated, remove this function once salt gone
task = AutomatedTask.objects.get(pk=pk)
r = task.agent.salt_api_async(func="task.run", arg=[f"name={task.win_task_name}"])
return "ok"

View File

@@ -181,10 +181,10 @@ class TestAutotaskViews(TacticalTestCase):
self.check_not_authenticated("delete", url)
@patch("autotasks.tasks.run_win_task.delay")
def test_run_autotask(self, run_win_task):
@patch("agents.models.Agent.nats_cmd")
def test_run_autotask(self, nats_cmd):
# setup data
agent = baker.make_recipe("agents.agent")
agent = baker.make_recipe("agents.agent", version="1.1.0")
task = baker.make("autotasks.AutomatedTask", agent=agent)
# test invalid url
@@ -195,7 +195,15 @@ class TestAutotaskViews(TacticalTestCase):
url = f"/tasks/runwintask/{task.id}/"
resp = self.client.get(url, format="json")
self.assertEqual(resp.status_code, 200)
run_win_task.assert_called_with(task.id)
nats_cmd.assert_called_with({"func": "runtask", "taskpk": task.id}, wait=False)
nats_cmd.reset_mock()
old_agent = baker.make_recipe("agents.agent", version="1.0.2")
task2 = baker.make("autotasks.AutomatedTask", agent=old_agent)
url = f"/tasks/runwintask/{task2.id}/"
resp = self.client.get(url, format="json")
self.assertEqual(resp.status_code, 400)
nats_cmd.assert_not_called()
self.check_not_authenticated("get", url)

View File

@@ -1,3 +1,4 @@
import asyncio
import pytz
from django.shortcuts import get_object_or_404
@@ -17,7 +18,6 @@ from .serializers import TaskSerializer, AutoTaskSerializer
from .tasks import (
create_win_task_schedule,
delete_win_task_schedule,
run_win_task,
enable_or_disable_win_task,
)
from tacticalrmm.utils import notify_error
@@ -114,5 +114,8 @@ class AutoTask(APIView):
@api_view()
def run_task(request, pk):
task = get_object_or_404(AutomatedTask, pk=pk)
run_win_task.delay(task.pk)
if not task.agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
asyncio.run(task.agent.nats_cmd({"func": "runtask", "taskpk": task.pk}, wait=False))
return Response(f"{task.name} will now be run on {task.agent.hostname}")

View File

@@ -1,9 +1,8 @@
import base64
import asyncio
import string
import os
import json
import pytz
import zlib
from statistics import mean
from django.db import models
@@ -306,12 +305,16 @@ class Check(BaseAuditModel):
self.status = "passing"
else:
if self.agent and self.restart_if_stopped:
r = self.agent.salt_api_cmd(
func="service.restart", arg=self.svc_name, timeout=45
)
if r == "timeout" or r == "error":
nats_data = {
"func": "winsvcaction",
"payload": {"name": self.svc_name, "action": "start"},
}
r = asyncio.run(self.agent.nats_cmd(nats_data, timeout=32))
if r == "timeout" or r == "natsdown":
self.status = "failing"
elif isinstance(r, bool) and r:
elif not r["success"] and r["errormsg"]:
self.status = "failing"
elif r["success"]:
self.status = "passing"
self.more_info = f"Status RUNNING"
else:
@@ -336,8 +339,7 @@ class Check(BaseAuditModel):
eventID = self.event_id
source = self.event_source
message = self.event_message
r = json.loads(zlib.decompress(base64.b64decode(data["log"])))
r = data["log"]
for i in r:
if i["eventType"] == eventType:

View File

@@ -56,10 +56,3 @@ def handle_check_sms_alert_task(pk):
check.save(update_fields=["text_sent"])
return "ok"
@app.task
def run_checks_task(pk):
agent = Agent.objects.get(pk=pk)
agent.salt_api_async(func="win_agent.run_manual_checks")
return "ok"

View File

@@ -1,3 +1,5 @@
import asyncio
from django.shortcuts import get_object_or_404
from rest_framework.views import APIView
@@ -13,7 +15,6 @@ from scripts.models import Script
from .serializers import CheckSerializer
from .tasks import run_checks_task
from automation.tasks import (
generate_agent_checks_from_policies_task,
@@ -178,7 +179,10 @@ class GetUpdateDeleteCheck(APIView):
@api_view()
def run_checks(request, pk):
agent = get_object_or_404(Agent, pk=pk)
run_checks_task.delay(agent.pk)
if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
asyncio.run(agent.nats_cmd({"func": "runchecks"}, wait=False))
return Response(agent.hostname)

View File

@@ -55,8 +55,7 @@ class Client(BaseAuditModel):
return True
if agent.overdue_email_alert or agent.overdue_text_alert:
if agent.status == "overdue":
return True
return agent.status == "overdue"
return False
@@ -116,8 +115,7 @@ class Site(BaseAuditModel):
return True
if agent.overdue_email_alert or agent.overdue_text_alert:
if agent.status == "overdue":
return True
return agent.status == "overdue"
return False

View File

@@ -3,7 +3,6 @@ from django.conf import settings
from core.models import CoreSettings
from .helpers import get_auth_token
import asyncio
import ssl
import websockets
import json
@@ -11,15 +10,15 @@ import json
class Command(BaseCommand):
help = "Sets up initial mesh central configuration"
async def websocket_call(self):
async def websocket_call(self, mesh_settings):
token = get_auth_token(
self.mesh_settings.mesh_username, self.mesh_settings.mesh_token
mesh_settings.mesh_username, mesh_settings.mesh_token
)
if settings.MESH_WS_URL:
uri = f"{settings.MESH_WS_URL}/control.ashx?auth={token}"
else:
site = self.mesh_settings.mesh_site.replace("https", "wss")
site = mesh_settings.mesh_site.replace("https", "wss")
uri = f"{site}/control.ashx?auth={token}"
async with websockets.connect(uri) as websocket:
@@ -45,5 +44,5 @@ class Command(BaseCommand):
break
def handle(self, *args, **kwargs):
self.mesh_settings = CoreSettings.objects.first()
asyncio.get_event_loop().run_until_complete(self.websocket_call())
mesh_settings = CoreSettings.objects.first()
asyncio.get_event_loop().run_until_complete(self.websocket_call(mesh_settings))

View File

@@ -10,16 +10,16 @@ import json
class Command(BaseCommand):
help = "Sets up initial mesh central configuration"
async def websocket_call(self):
async def websocket_call(self, mesh_settings):
token = get_auth_token(
self.mesh_settings.mesh_username, self.mesh_settings.mesh_token
mesh_settings.mesh_username, mesh_settings.mesh_token
)
if settings.MESH_WS_URL:
uri = f"{settings.MESH_WS_URL}/control.ashx?auth={token}"
else:
site = self.mesh_settings.mesh_site.replace("https", "wss")
site = mesh_settings.mesh_site.replace("https", "wss")
uri = f"{site}/control.ashx?auth={token}"
async with websockets.connect(uri) as websocket:
@@ -48,6 +48,36 @@ class Command(BaseCommand):
break
def handle(self, *args, **kwargs):
self.mesh_settings = CoreSettings.objects.first()
asyncio.get_event_loop().run_until_complete(self.websocket_call())
mesh_settings = CoreSettings.objects.first()
try:
# Check for Mesh Username
if not mesh_settings.mesh_username or settings.MESH_USERNAME != mesh_settings.mesh_username:
mesh_settings.mesh_username = settings.MESH_USERNAME
# Check for Mesh Site
if not mesh_settings.mesh_site or settings.MESH_SITE != mesh_settings.mesh_site:
mesh_settings.mesh_site = settings.MESH_SITE
# Check for Mesh Token
if (
not mesh_settings.mesh_token
or settings.MESH_TOKEN_KEY != mesh_settings.mesh_token
):
mesh_settings.mesh_token = settings.MESH_TOKEN_KEY
mesh_settings.save()
except AttributeError:
self.stdout.write(
"Mesh Setup was skipped because the configuration wasn't available. Needs to be setup manually."
)
return
try:
asyncio.get_event_loop().run_until_complete(self.websocket_call(mesh_settings))
self.stdout.write("Initial Mesh Central setup complete")
except websockets.exceptions.ConnectionClosedError:
self.stdout.write(
"Unable to connect to MeshCentral. Please verify it is online and the configuration is correct in the settings."
)

View File

@@ -0,0 +1,9 @@
from django.core.management.base import BaseCommand
from tacticalrmm.utils import reload_nats
class Command(BaseCommand):
help = "Reload Nats"
def handle(self, *args, **kwargs):
reload_nats()

View File

@@ -72,16 +72,14 @@ class CoreSettings(BaseAuditModel):
if not self.pk and CoreSettings.objects.exists():
raise ValidationError("There can only be one CoreSettings instance")
# Only runs on first create
# for install script
if not self.pk:
mesh_settings = self.get_initial_mesh_settings()
if "mesh_token" in mesh_settings:
self.mesh_token = mesh_settings["mesh_token"]
if "mesh_username" in mesh_settings:
self.mesh_username = mesh_settings["mesh_username"]
if "mesh_site" in mesh_settings:
self.mesh_site = mesh_settings["mesh_site"]
try:
self.mesh_site = settings.MESH_SITE
self.mesh_username = settings.MESH_USERNAME
self.mesh_token = settings.MESH_TOKEN_KEY
except:
pass
return super(CoreSettings, self).save(*args, **kwargs)
@@ -121,7 +119,7 @@ class CoreSettings(BaseAuditModel):
and self.smtp_port
):
return True
else:
return False
def send_mail(self, subject, body, test=False):
@@ -168,57 +166,6 @@ class CoreSettings(BaseAuditModel):
except Exception as e:
logger.error(f"SMS failed to send: {e}")
def get_initial_mesh_settings(self):
mesh_settings = {}
# Check for Mesh Username
try:
if settings.MESH_USERNAME:
mesh_settings["mesh_username"] = settings.MESH_USERNAME
else:
raise AttributeError("MESH_USERNAME doesn't exist")
except AttributeError:
pass
# Check for Mesh Site
try:
if settings.MESH_SITE:
mesh_settings["mesh_site"] = settings.MESH_SITE
else:
raise AttributeError("MESH_SITE doesn't exist")
except AttributeError:
pass
# Check for Mesh Token
try:
if settings.MESH_TOKEN_KEY:
mesh_settings["mesh_token"] = settings.MESH_TOKEN_KEY
else:
raise AttributeError("MESH_TOKEN_KEY doesn't exist")
except AttributeError:
filepath = "/token/token.key"
counter = 0
while counter < 12:
try:
with open(filepath, "r") as read_file:
key = read_file.readlines()
# Remove key file contents for security reasons
with open(filepath, "w") as write_file:
write_file.write("")
# readlines() returns an array. Get first item
mesh_settings["mesh_token"] = key[0].rstrip()
break
except (IOError, IndexError):
pass
counter = counter + 1
time.sleep(10)
return mesh_settings
@staticmethod
def serialize(core):
# serializes the core and returns json

View File

@@ -4,8 +4,6 @@ from loguru import logger
from django.conf import settings
from django.utils import timezone as djangotime
from tacticalrmm.celery import app
from accounts.models import User
from agents.models import Agent
from autotasks.models import AutomatedTask
from autotasks.tasks import delete_win_task_schedule
@@ -14,15 +12,6 @@ logger.configure(**settings.LOG_CONFIG)
@app.task
def core_maintenance_tasks():
# cleanup any leftover agent user accounts
agents = Agent.objects.values_list("agent_id", flat=True)
users = User.objects.exclude(username__in=agents).filter(last_login=None)
if users:
users.delete()
logger.info(
"Removed leftover agent user accounts:", str([i.username for i in users])
)
# cleanup expired runonce tasks
tasks = AutomatedTask.objects.filter(
task_type="runonce",

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.3 on 2020-11-19 08:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('logs', '0010_auto_20201110_2238'),
]
operations = [
migrations.AlterField(
model_name='pendingaction',
name='action_type',
field=models.CharField(blank=True, choices=[('schedreboot', 'Scheduled Reboot'), ('taskaction', 'Scheduled Task Action'), ('agentupdate', 'Agent Update')], max_length=255, null=True),
),
]

View File

@@ -7,6 +7,7 @@ from tacticalrmm.middleware import get_username, get_debug_info
ACTION_TYPE_CHOICES = [
("schedreboot", "Scheduled Reboot"),
("taskaction", "Scheduled Task Action"),
("agentupdate", "Agent Update"),
]
AUDIT_ACTION_TYPE_CHOICES = [
@@ -248,7 +249,7 @@ class PendingAction(models.Model):
obj = dt.datetime.strptime(self.details["time"], "%Y-%m-%d %H:%M:%S")
return dt.datetime.strftime(obj, "%B %d, %Y at %I:%M %p")
elif self.action_type == "taskaction":
elif self.action_type == "taskaction" or self.action_type == "agentupdate":
return "Next agent check-in"
@property
@@ -256,6 +257,9 @@ class PendingAction(models.Model):
if self.action_type == "schedreboot":
return "Device pending reboot"
elif self.action_type == "agentupdate":
return f"Agent update to {self.details['version']}"
elif self.action_type == "taskaction":
if self.details["action"] == "taskdelete":
return "Device pending task deletion"

View File

@@ -1,5 +1,6 @@
amqp==2.6.1
asgiref==3.3.0
asyncio-nats-client==0.11.4
billiard==3.6.3.0
celery==4.4.6
certifi==2020.11.8
@@ -28,6 +29,7 @@ redis==3.5.3
requests==2.24.0
six==1.15.0
sqlparse==0.4.1
tldextract==3.0.2
twilio==6.47.0
urllib3==1.25.11
uWSGI==2.0.19.1

View File

@@ -1,5 +1,6 @@
from django.db import models
from logs.models import BaseAuditModel
from django.conf import settings
SCRIPT_SHELLS = [
("powershell", "Powershell"),
@@ -38,9 +39,9 @@ class Script(BaseAuditModel):
@property
def file(self):
if self.script_type == "userdefined":
return f"/srv/salt/scripts/userdefined/{self.filename}"
return f"{settings.SCRIPTS_DIR}/userdefined/{self.filename}"
else:
return f"/srv/salt/scripts/{self.filename}"
return f"{settings.SCRIPTS_DIR}/{self.filename}"
@property
def code(self):
@@ -64,11 +65,11 @@ class Script(BaseAuditModel):
# files will be copied by the update script or in docker to /srv/salt/scripts
# for install script
try:
if not settings.DOCKER_BUILD:
scripts_dir = os.path.join(Path(settings.BASE_DIR).parents[1], "scripts")
# for docker
except:
scripts_dir = os.path.join(Path(settings.BASE_DIR).parents[0], "scripts")
else:
scripts_dir = settings.SCRIPTS_DIR
with open(
os.path.join(settings.BASE_DIR, "scripts/community_scripts.json")

View File

@@ -1,5 +1,6 @@
import os
from django.conf import settings
from rest_framework.serializers import ModelSerializer, ValidationError, ReadOnlyField
from .models import Script
@@ -27,7 +28,7 @@ class ScriptSerializer(ModelSerializer):
# but only if adding, not if editing since will overwrite if edit
if not self.instance:
script_path = os.path.join(
"/srv/salt/scripts/userdefined", val["filename"]
f"{settings.SCRIPTS_DIR}/userdefined", val["filename"]
)
if os.path.exists(script_path):
raise ValidationError(

View File

@@ -1,38 +1,73 @@
import asyncio
from tacticalrmm.celery import app
from agents.models import Agent
from .models import Script
from scripts.models import Script
@app.task
def run_script_bg_task(data):
agent = Agent.objects.get(pk=data["agentpk"])
script = Script.objects.get(pk=data["scriptpk"])
def handle_bulk_command_task(agentpks, cmd, shell, timeout):
agents = Agent.objects.filter(pk__in=agentpks)
agent.salt_api_async(
func="win_agent.run_script",
kwargs={
"filepath": script.filepath,
"filename": script.filename,
"shell": script.shell,
"timeout": data["timeout"],
"args": data["args"],
},
)
@app.task
def run_bulk_script_task(data):
# for powershell and batch scripts only, workaround for salt bg script bug
script = Script.objects.get(pk=data["scriptpk"])
agents_nats = [agent for agent in agents if agent.has_nats]
agents_salt = [agent for agent in agents if not agent.has_nats]
minions = [agent.salt_id for agent in agents_salt]
if minions:
Agent.salt_batch_async(
minions=data["minions"],
minions=minions,
func="cmd.run_bg",
kwargs={
"cmd": cmd,
"shell": shell,
"timeout": timeout,
},
)
if agents_nats:
nats_data = {
"func": "rawcmd",
"timeout": timeout,
"payload": {
"command": cmd,
"shell": shell,
},
}
for agent in agents_nats:
asyncio.run(agent.nats_cmd(nats_data, wait=False))
@app.task
def handle_bulk_script_task(scriptpk, agentpks, args, timeout):
script = Script.objects.get(pk=scriptpk)
agents = Agent.objects.filter(pk__in=agentpks)
agents_nats = [agent for agent in agents if agent.has_nats]
agents_salt = [agent for agent in agents if not agent.has_nats]
minions = [agent.salt_id for agent in agents_salt]
if minions:
Agent.salt_batch_async(
minions=minions,
func="win_agent.run_script",
kwargs={
"filepath": script.filepath,
"filename": script.filename,
"shell": script.shell,
"timeout": data["timeout"],
"args": data["args"],
"timeout": timeout,
"args": args,
"bg": True if script.shell == "python" else False, # salt bg script bug
},
)
nats_data = {
"func": "runscript",
"timeout": timeout,
"script_args": args,
"payload": {
"code": script.code,
"shell": script.shell,
},
}
for agent in agents_nats:
asyncio.run(agent.nats_cmd(nats_data, wait=False))

View File

@@ -94,7 +94,11 @@ class TestScriptViews(TacticalTestCase):
def test_load_community_scripts(self):
valid_shells = ["powershell", "python", "cmd"]
if not settings.DOCKER_BUILD:
scripts_dir = os.path.join(Path(settings.BASE_DIR).parents[1], "scripts")
else:
scripts_dir = settings.SCRIPTS_DIR
with open(
os.path.join(settings.BASE_DIR, "scripts/community_scripts.json")

View File

@@ -32,8 +32,8 @@ class TestServiceViews(TacticalTestCase):
self.check_not_authenticated("get", url)
@patch("agents.models.Agent.salt_api_cmd")
def test_get_refreshed_services(self, salt_api_cmd):
@patch("agents.models.Agent.nats_cmd")
def test_get_refreshed_services(self, nats_cmd):
# test a call where agent doesn't exist
resp = self.client.get("/services/500/refreshedservices/", format="json")
self.assertEqual(resp.status_code, 404)
@@ -41,7 +41,7 @@ class TestServiceViews(TacticalTestCase):
agent = baker.make_recipe("agents.agent_with_services")
url = f"/services/{agent.pk}/refreshedservices/"
salt_return = [
nats_return = [
{
"pid": 880,
"name": "AeLookupSvc",
@@ -65,30 +65,23 @@ class TestServiceViews(TacticalTestCase):
]
# test failed attempt
salt_api_cmd.return_value = "timeout"
nats_cmd.return_value = "timeout"
resp = self.client.get(url, format="json")
self.assertEqual(resp.status_code, 400)
salt_api_cmd.assert_called_with(timeout=15, func="win_agent.get_services")
salt_api_cmd.reset_mock()
# test failed attempt
salt_api_cmd.return_value = "error"
resp = self.client.get(url, format="json")
self.assertEqual(resp.status_code, 400)
salt_api_cmd.assert_called_with(timeout=15, func="win_agent.get_services")
salt_api_cmd.reset_mock()
nats_cmd.assert_called_with(data={"func": "winservices"}, timeout=10)
nats_cmd.reset_mock()
# test successful attempt
salt_api_cmd.return_value = salt_return
nats_cmd.return_value = nats_return
resp = self.client.get(url, format="json")
self.assertEqual(resp.status_code, 200)
salt_api_cmd.assert_called_with(timeout=15, func="win_agent.get_services")
self.assertEquals(Agent.objects.get(pk=agent.pk).services, salt_return)
nats_cmd.assert_called_with(data={"func": "winservices"}, timeout=10)
self.assertEquals(Agent.objects.get(pk=agent.pk).services, nats_return)
self.check_not_authenticated("get", url)
@patch("agents.models.Agent.salt_api_cmd")
def test_service_action(self, salt_api_cmd):
@patch("agents.models.Agent.nats_cmd")
def test_service_action(self, nats_cmd):
url = "/services/serviceaction/"
invalid_data = {"pk": 500, "sv_name": "AeLookupSvc", "sv_action": "restart"}
@@ -101,47 +94,37 @@ class TestServiceViews(TacticalTestCase):
data = {"pk": agent.pk, "sv_name": "AeLookupSvc", "sv_action": "restart"}
# test failed attempt
salt_api_cmd.return_value = "timeout"
nats_cmd.return_value = "timeout"
resp = self.client.post(url, data, format="json")
self.assertEqual(resp.status_code, 400)
salt_api_cmd.assert_called_with(
timeout=45,
func=f"service.restart",
arg="AeLookupSvc",
nats_cmd.assert_called_with(
{
"func": "winsvcaction",
"payload": {
"name": "AeLookupSvc",
"action": "stop",
},
},
timeout=32,
)
salt_api_cmd.reset_mock()
salt_api_cmd.return_value = "error"
resp = self.client.post(url, data, format="json")
self.assertEqual(resp.status_code, 400)
salt_api_cmd.assert_called_with(
timeout=45,
func=f"service.restart",
arg="AeLookupSvc",
)
salt_api_cmd.reset_mock()
nats_cmd.reset_mock()
# test successful attempt
salt_api_cmd.return_value = True
nats_cmd.return_value = {"success": True, "errormsg": ""}
resp = self.client.post(url, data, format="json")
self.assertEqual(resp.status_code, 200)
salt_api_cmd.assert_called_with(
timeout=45,
func=f"service.restart",
arg="AeLookupSvc",
)
self.check_not_authenticated("post", url)
@patch("agents.models.Agent.salt_api_cmd")
def test_service_detail(self, salt_api_cmd):
@patch("agents.models.Agent.nats_cmd")
def test_service_detail(self, nats_cmd):
# test a call where agent doesn't exist
resp = self.client.get(
"/services/500/doesntexist/servicedetail/", format="json"
)
self.assertEqual(resp.status_code, 404)
salt_return = {
nats_return = {
"pid": 812,
"name": "ALG",
"status": "stopped",
@@ -156,29 +139,27 @@ class TestServiceViews(TacticalTestCase):
url = f"/services/{agent.pk}/alg/servicedetail/"
# test failed attempt
salt_api_cmd.return_value = "timeout"
nats_cmd.return_value = "timeout"
resp = self.client.get(url, format="json")
self.assertEqual(resp.status_code, 400)
salt_api_cmd.assert_called_with(timeout=20, func="service.info", arg="alg")
salt_api_cmd.reset_mock()
salt_api_cmd.return_value = "error"
resp = self.client.get(url, format="json")
self.assertEqual(resp.status_code, 400)
salt_api_cmd.assert_called_with(timeout=20, func="service.info", arg="alg")
salt_api_cmd.reset_mock()
nats_cmd.assert_called_with(
{"func": "winsvcdetail", "payload": {"name": "alg"}}, timeout=10
)
nats_cmd.reset_mock()
# test successful attempt
salt_api_cmd.return_value = salt_return
nats_cmd.return_value = nats_return
resp = self.client.get(url, format="json")
self.assertEqual(resp.status_code, 200)
salt_api_cmd.assert_called_with(timeout=20, func="service.info", arg="alg")
self.assertEquals(resp.data, salt_return)
nats_cmd.assert_called_with(
{"func": "winsvcdetail", "payload": {"name": "alg"}}, timeout=10
)
self.assertEquals(resp.data, nats_return)
self.check_not_authenticated("get", url)
@patch("agents.models.Agent.salt_api_cmd")
def test_edit_service(self, salt_api_cmd):
@patch("agents.models.Agent.nats_cmd")
def test_edit_service(self, nats_cmd):
url = "/services/editservice/"
agent = baker.make_recipe("agents.agent_with_services")
@@ -189,64 +170,43 @@ class TestServiceViews(TacticalTestCase):
data = {"pk": agent.pk, "sv_name": "AeLookupSvc", "edit_action": "autodelay"}
# test failed attempt
salt_api_cmd.return_value = "timeout"
# test timeout
nats_cmd.return_value = "timeout"
resp = self.client.post(url, data, format="json")
self.assertEqual(resp.status_code, 400)
salt_api_cmd.assert_called_with(
timeout=20,
func="service.modify",
arg=data["sv_name"],
kwargs={"start_type": "auto", "start_delayed": True},
)
salt_api_cmd.reset_mock()
salt_api_cmd.return_value = "error"
resp = self.client.post(url, data, format="json")
self.assertEqual(resp.status_code, 400)
salt_api_cmd.assert_called_with(
timeout=20,
func="service.modify",
arg=data["sv_name"],
kwargs={"start_type": "auto", "start_delayed": True},
)
salt_api_cmd.reset_mock()
nats_cmd.reset_mock()
# test successful attempt autodelay
salt_api_cmd.return_value = True
nats_cmd.return_value = {"success": True, "errormsg": ""}
resp = self.client.post(url, data, format="json")
self.assertEqual(resp.status_code, 200)
salt_api_cmd.assert_called_with(
timeout=20,
func="service.modify",
arg=data["sv_name"],
kwargs={"start_type": "auto", "start_delayed": True},
nats_cmd.assert_called_with(
{
"func": "editwinsvc",
"payload": {
"name": "AeLookupSvc",
"startType": "autodelay",
},
},
timeout=10,
)
salt_api_cmd.reset_mock()
nats_cmd.reset_mock()
# test successful attempt with auto
# test error message from agent
data = {"pk": agent.pk, "sv_name": "AeLookupSvc", "edit_action": "auto"}
salt_api_cmd.return_value = True
nats_cmd.return_value = {
"success": False,
"errormsg": "The parameter is incorrect",
}
resp = self.client.post(url, data, format="json")
self.assertEqual(resp.status_code, 200)
salt_api_cmd.assert_called_with(
timeout=20,
func="service.modify",
arg=data["sv_name"],
kwargs={"start_type": "auto", "start_delayed": False},
)
salt_api_cmd.reset_mock()
self.assertEqual(resp.status_code, 400)
nats_cmd.reset_mock()
# test successful attempt with manual
data = {"pk": agent.pk, "sv_name": "AeLookupSvc", "edit_action": "manual"}
salt_api_cmd.return_value = True
# test catch all
data = {"pk": agent.pk, "sv_name": "AeLookupSvc", "edit_action": "auto"}
nats_cmd.return_value = {"success": False, "errormsg": ""}
resp = self.client.post(url, data, format="json")
self.assertEqual(resp.status_code, 200)
salt_api_cmd.assert_called_with(
timeout=20,
func="service.modify",
arg=data["sv_name"],
kwargs={"start_type": "manual"},
)
self.assertEqual(resp.status_code, 400)
self.assertEqual(resp.data, "Something went wrong")
self.check_not_authenticated("post", url)

View File

@@ -1,3 +1,4 @@
import asyncio
from loguru import logger
from rest_framework.response import Response
@@ -30,12 +31,12 @@ def default_services(request):
@api_view()
def get_refreshed_services(request, pk):
agent = get_object_or_404(Agent, pk=pk)
r = agent.salt_api_cmd(timeout=15, func="win_agent.get_services")
if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
r = asyncio.run(agent.nats_cmd(data={"func": "winservices"}, timeout=10))
if r == "timeout":
return notify_error("Unable to contact the agent")
elif r == "error" or not r:
return notify_error("Something went wrong")
agent.services = r
agent.save(update_fields=["services"])
@@ -44,64 +45,79 @@ def get_refreshed_services(request, pk):
@api_view(["POST"])
def service_action(request):
data = request.data
pk = data["pk"]
service_name = data["sv_name"]
service_action = data["sv_action"]
agent = get_object_or_404(Agent, pk=pk)
r = agent.salt_api_cmd(
timeout=45,
func=f"service.{service_action}",
arg=service_name,
)
agent = get_object_or_404(Agent, pk=request.data["pk"])
if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
action = request.data["sv_action"]
data = {
"func": "winsvcaction",
"payload": {
"name": request.data["sv_name"],
},
}
# response struct from agent: {success: bool, errormsg: string}
if action == "restart":
data["payload"]["action"] = "stop"
r = asyncio.run(agent.nats_cmd(data, timeout=32))
if r == "timeout":
return notify_error("Unable to contact the agent")
elif r == "error" or not r:
return notify_error("Something went wrong")
elif not r["success"] and r["errormsg"]:
return notify_error(r["errormsg"])
elif r["success"]:
data["payload"]["action"] = "start"
r = asyncio.run(agent.nats_cmd(data, timeout=32))
if r == "timeout":
return notify_error("Unable to contact the agent")
elif not r["success"] and r["errormsg"]:
return notify_error(r["errormsg"])
elif r["success"]:
return Response("ok")
else:
data["payload"]["action"] = action
r = asyncio.run(agent.nats_cmd(data, timeout=32))
if r == "timeout":
return notify_error("Unable to contact the agent")
elif not r["success"] and r["errormsg"]:
return notify_error(r["errormsg"])
elif r["success"]:
return Response("ok")
return notify_error("Something went wrong")
@api_view()
def service_detail(request, pk, svcname):
agent = get_object_or_404(Agent, pk=pk)
r = agent.salt_api_cmd(timeout=20, func="service.info", arg=svcname)
if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
data = {"func": "winsvcdetail", "payload": {"name": svcname}}
r = asyncio.run(agent.nats_cmd(data, timeout=10))
if r == "timeout":
return notify_error("Unable to contact the agent")
elif r == "error" or not r:
return notify_error("Something went wrong")
return Response(r)
@api_view(["POST"])
def edit_service(request):
data = request.data
pk = data["pk"]
service_name = data["sv_name"]
edit_action = data["edit_action"]
agent = get_object_or_404(Agent, pk=pk)
if edit_action == "autodelay":
kwargs = {"start_type": "auto", "start_delayed": True}
elif edit_action == "auto":
kwargs = {"start_type": "auto", "start_delayed": False}
else:
kwargs = {"start_type": edit_action}
r = agent.salt_api_cmd(
timeout=20,
func="service.modify",
arg=service_name,
kwargs=kwargs,
)
agent = get_object_or_404(Agent, pk=request.data["pk"])
if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
data = {
"func": "editwinsvc",
"payload": {
"name": request.data["sv_name"],
"startType": request.data["edit_action"],
},
}
r = asyncio.run(agent.nats_cmd(data, timeout=10))
# response struct from agent: {success: bool, errormsg: string}
if r == "timeout":
return notify_error("Unable to contact the agent")
elif r == "error" or not r:
return notify_error("Something went wrong")
elif not r["success"] and r["errormsg"]:
return notify_error(r["errormsg"])
elif r["success"]:
return Response("ok")
return notify_error("Something went wrong")

View File

@@ -1,3 +1,4 @@
import asyncio
import string
from time import sleep
from loguru import logger
@@ -89,35 +90,36 @@ def update_chocos():
@app.task
def get_installed_software(pk):
agent = Agent.objects.get(pk=pk)
r = agent.salt_api_cmd(
timeout=30,
func="pkg.list_pkgs",
kwargs={"include_components": False, "include_updates": False},
)
if not agent.has_nats:
logger.error(f"{agent.salt_id} software list only available in agent >= 1.1.0")
return
if r == "timeout" or r == "error":
logger.error(f"Timed out trying to get installed software on {agent.salt_id}")
r = asyncio.run(agent.nats_cmd({"func": "softwarelist"}, timeout=20))
if r == "timeout" or r == "natsdown":
logger.error(f"{agent.salt_id} {r}")
return
printable = set(string.printable)
try:
software = [
sw = []
for s in r:
sw.append(
{
"name": "".join(filter(lambda x: x in printable, k)),
"version": "".join(filter(lambda x: x in printable, v)),
"name": "".join(filter(lambda x: x in printable, s["name"])),
"version": "".join(filter(lambda x: x in printable, s["version"])),
"publisher": "".join(filter(lambda x: x in printable, s["publisher"])),
"install_date": s["install_date"],
"size": s["size"],
"source": s["source"],
"location": s["location"],
"uninstall": s["uninstall"],
}
for k, v in r.items()
]
except Exception as e:
logger.error(f"Unable to get installed software on {agent.salt_id}: {e}")
return
)
if not InstalledSoftware.objects.filter(agent=agent).exists():
InstalledSoftware(agent=agent, software=software).save()
InstalledSoftware(agent=agent, software=sw).save()
else:
s = agent.installedsoftware_set.get()
s.software = software
s = agent.installedsoftware_set.first()
s.software = sw
s.save(update_fields=["software"])
return "ok"

View File

@@ -62,72 +62,6 @@ class TestSoftwareViews(TacticalTestCase):
self.check_not_authenticated("get", url)
@patch("agents.models.Agent.salt_api_cmd")
def test_chocos_refresh(self, salt_api_cmd):
salt_return = {"git": "2.3.4", "docker": "1.0.2"}
# test a call where agent doesn't exist
resp = self.client.get("/software/refresh/500/", format="json")
self.assertEqual(resp.status_code, 404)
agent = baker.make_recipe("agents.agent")
url = f"/software/refresh/{agent.pk}/"
# test failed attempt
salt_api_cmd.return_value = "timeout"
resp = self.client.get(url, format="json")
self.assertEqual(resp.status_code, 400)
salt_api_cmd.assert_called_with(
timeout=20,
func="pkg.list_pkgs",
kwargs={"include_components": False, "include_updates": False},
)
salt_api_cmd.reset_mock()
salt_api_cmd.return_value = "error"
resp = self.client.get(url, format="json")
self.assertEqual(resp.status_code, 400)
salt_api_cmd.assert_called_with(
timeout=20,
func="pkg.list_pkgs",
kwargs={"include_components": False, "include_updates": False},
)
salt_api_cmd.reset_mock()
# test success and created new software object
salt_api_cmd.return_value = salt_return
resp = self.client.get(url, format="json")
self.assertEqual(resp.status_code, 200)
salt_api_cmd.assert_called_with(
timeout=20,
func="pkg.list_pkgs",
kwargs={"include_components": False, "include_updates": False},
)
self.assertTrue(InstalledSoftware.objects.filter(agent=agent).exists())
salt_api_cmd.reset_mock()
# test success and updates software object
salt_api_cmd.return_value = salt_return
resp = self.client.get(url, format="json")
self.assertEqual(resp.status_code, 200)
salt_api_cmd.assert_called_with(
timeout=20,
func="pkg.list_pkgs",
kwargs={"include_components": False, "include_updates": False},
)
software = agent.installedsoftware_set.get()
expected = [
{"name": "git", "version": "2.3.4"},
{"name": "docker", "version": "1.0.2"},
]
self.assertTrue(InstalledSoftware.objects.filter(agent=agent).exists())
self.assertEquals(software.software, expected)
self.check_not_authenticated("get", url)
class TestSoftwareTasks(TacticalTestCase):
@patch("agents.models.Agent.salt_api_cmd")
@@ -186,43 +120,57 @@ class TestSoftwareTasks(TacticalTestCase):
salt_api_cmd.assert_any_call(timeout=200, func="chocolatey.list")
self.assertEquals(salt_api_cmd.call_count, 2)
@patch("agents.models.Agent.salt_api_cmd")
def test_get_installed_software(self, salt_api_cmd):
@patch("agents.models.Agent.nats_cmd")
def test_get_installed_software(self, nats_cmd):
from .tasks import get_installed_software
agent = baker.make_recipe("agents.agent")
salt_return = {"git": "2.3.4", "docker": "1.0.2"}
# test failed attempt
salt_api_cmd.return_value = "timeout"
ret = get_installed_software(agent.pk)
self.assertFalse(ret)
salt_api_cmd.assert_called_with(
timeout=30,
func="pkg.list_pkgs",
kwargs={"include_components": False, "include_updates": False},
)
salt_api_cmd.reset_mock()
# test successful attempt
salt_api_cmd.return_value = salt_return
ret = get_installed_software(agent.pk)
self.assertTrue(ret)
salt_api_cmd.assert_called_with(
timeout=30,
func="pkg.list_pkgs",
kwargs={"include_components": False, "include_updates": False},
)
software = agent.installedsoftware_set.get()
expected = [
{"name": "git", "version": "2.3.4"},
{"name": "docker", "version": "1.0.2"},
nats_return = [
{
"name": "Mozilla Maintenance Service",
"size": "336.9 kB",
"source": "",
"version": "73.0.1",
"location": "",
"publisher": "Mozilla",
"uninstall": '"C:\\Program Files (x86)\\Mozilla Maintenance Service\\uninstall.exe"',
"install_date": "0001-01-01 00:00:00 +0000 UTC",
},
{
"name": "OpenVPN 2.4.9-I601-Win10 ",
"size": "8.7 MB",
"source": "",
"version": "2.4.9-I601-Win10",
"location": "C:\\Program Files\\OpenVPN\\",
"publisher": "OpenVPN Technologies, Inc.",
"uninstall": "C:\\Program Files\\OpenVPN\\Uninstall.exe",
"install_date": "0001-01-01 00:00:00 +0000 UTC",
},
{
"name": "Microsoft Office Professional Plus 2019 - en-us",
"size": "0 B",
"source": "",
"version": "16.0.10368.20035",
"location": "C:\\Program Files\\Microsoft Office",
"publisher": "Microsoft Corporation",
"uninstall": '"C:\\Program Files\\Common Files\\Microsoft Shared\\ClickToRun\\OfficeClickToRun.exe" scenario=install scenariosubtype=ARP sourcetype=None productstoremove=ProPlus2019Volume.16_en-us_x-none culture=en-us version.16=16.0',
"install_date": "0001-01-01 00:00:00 +0000 UTC",
},
]
self.assertTrue(InstalledSoftware.objects.filter(agent=agent).exists())
self.assertEquals(software.software, expected)
# test failed attempt
nats_cmd.return_value = "timeout"
ret = get_installed_software(agent.pk)
self.assertFalse(ret)
nats_cmd.assert_called_with({"func": "softwarelist"}, timeout=20)
nats_cmd.reset_mock()
# test successful attempt
nats_cmd.return_value = nats_return
ret = get_installed_software(agent.pk)
self.assertTrue(ret)
nats_cmd.assert_called_with({"func": "softwarelist"}, timeout=20)
@patch("agents.models.Agent.salt_api_cmd")
@patch("software.tasks.get_installed_software.delay")

View File

@@ -1,3 +1,4 @@
import asyncio
import string
from django.shortcuts import get_object_or_404
@@ -41,35 +42,34 @@ def get_installed(request, pk):
@api_view()
def refresh_installed(request, pk):
agent = get_object_or_404(Agent, pk=pk)
r = agent.salt_api_cmd(
timeout=20,
func="pkg.list_pkgs",
kwargs={"include_components": False, "include_updates": False},
)
if not agent.has_nats:
return notify_error("Requires agent version 1.1.0 or greater")
if r == "timeout":
r = asyncio.run(agent.nats_cmd({"func": "softwarelist"}, timeout=15))
if r == "timeout" or r == "natsdown":
return notify_error("Unable to contact the agent")
elif r == "error":
return notify_error("Something went wrong")
printable = set(string.printable)
try:
software = [
sw = []
for s in r:
sw.append(
{
"name": "".join(filter(lambda x: x in printable, k)),
"version": "".join(filter(lambda x: x in printable, v)),
"name": "".join(filter(lambda x: x in printable, s["name"])),
"version": "".join(filter(lambda x: x in printable, s["version"])),
"publisher": "".join(filter(lambda x: x in printable, s["publisher"])),
"install_date": s["install_date"],
"size": s["size"],
"source": s["source"],
"location": s["location"],
"uninstall": s["uninstall"],
}
for k, v in r.items()
]
except Exception:
return notify_error("Something went wrong")
)
if not InstalledSoftware.objects.filter(agent=agent).exists():
InstalledSoftware(agent=agent, software=software).save()
InstalledSoftware(agent=agent, software=sw).save()
else:
s = agent.installedsoftware_set.get()
s.software = software
s = agent.installedsoftware_set.first()
s.software = sw
s.save(update_fields=["software"])
return Response("ok")

View File

@@ -27,15 +27,15 @@ app.conf.beat_schedule = {
},
"auto-approve-win-updates": {
"task": "winupdate.tasks.auto_approve_updates_task",
"schedule": crontab(minute=0, hour="*/8"),
"schedule": crontab(minute=2, hour="*/8"),
},
"install-scheduled-win-updates": {
"task": "winupdate.tasks.check_agent_update_schedule_task",
"schedule": crontab(minute=0, hour="*"),
"schedule": crontab(minute=5, hour="*"),
},
"sync-modules": {
"task": "agents.tasks.batch_sync_modules_task",
"schedule": crontab(minute=40, hour="*/4"),
"schedule": crontab(minute=25, hour="*/4"),
},
"sys-info": {
"task": "agents.tasks.batch_sysinfo_task",
@@ -43,11 +43,11 @@ app.conf.beat_schedule = {
},
"update-salt": {
"task": "agents.tasks.update_salt_minion_task",
"schedule": crontab(minute=30, hour="*/6"),
"schedule": crontab(minute=20, hour="*/6"),
},
"agent-auto-update": {
"task": "agents.tasks.auto_self_agent_update_task",
"schedule": crontab(minute=50, hour="*/3"),
"schedule": crontab(minute=35, hour="*"),
},
}

View File

@@ -37,7 +37,6 @@ if not DEBUG:
)
})
SALT_USERNAME = "changeme"
SALT_PASSWORD = "changeme"
MESH_USERNAME = "changeme"

View File

@@ -1,8 +1,13 @@
import os
from pathlib import Path
from datetime import timedelta
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SCRIPTS_DIR = "/srv/salt/scripts"
DOCKER_BUILD = False
LOG_DIR = os.path.join(BASE_DIR, "tacticalrmm/private/log")
EXE_DIR = os.path.join(BASE_DIR, "tacticalrmm/private/exe")
@@ -10,25 +15,25 @@ EXE_DIR = os.path.join(BASE_DIR, "tacticalrmm/private/exe")
AUTH_USER_MODEL = "accounts.User"
# latest release
TRMM_VERSION = "0.1.8"
TRMM_VERSION = "0.2.0"
# bump this version everytime vue code is changed
# to alert user they need to manually refresh their browser
APP_VER = "0.0.90"
APP_VER = "0.0.91"
# https://github.com/wh1te909/salt
LATEST_SALT_VER = "1.1.0"
# https://github.com/wh1te909/rmmagent
LATEST_AGENT_VER = "1.0.2"
LATEST_AGENT_VER = "1.1.0"
MESH_VER = "0.6.84"
SALT_MASTER_VER = "3002.2"
# for the update script, bump when need to recreate venv or npm install
PIP_VER = "2"
NPM_VER = "1"
PIP_VER = "3"
NPM_VER = "2"
DL_64 = f"https://github.com/wh1te909/rmmagent/releases/download/v{LATEST_AGENT_VER}/winagent-v{LATEST_AGENT_VER}.exe"
DL_32 = f"https://github.com/wh1te909/rmmagent/releases/download/v{LATEST_AGENT_VER}/winagent-v{LATEST_AGENT_VER}-x86.exe"
@@ -118,15 +123,9 @@ AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",},
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",},
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",},
]
@@ -175,6 +174,7 @@ if "TRAVIS" in os.environ:
ADMIN_URL = "abc123456/"
SCRIPTS_DIR = os.path.join(Path(BASE_DIR).parents[1], "scripts")
SALT_USERNAME = "travis"
SALT_PASSWORD = "travis"
MESH_USERNAME = "travis"
@@ -207,6 +207,7 @@ if "AZPIPELINE" in os.environ:
ADMIN_URL = "abc123456/"
SCRIPTS_DIR = os.path.join(Path(BASE_DIR).parents[1], "scripts")
SALT_USERNAME = "pipeline"
SALT_PASSWORD = "pipeline"
MESH_USERNAME = "pipeline"

View File

@@ -1,4 +1,44 @@
import json
import os
import subprocess
import tldextract
from django.conf import settings
from rest_framework import status
from rest_framework.response import Response
from agents.models import Agent
notify_error = lambda msg: Response(msg, status=status.HTTP_400_BAD_REQUEST)
def reload_nats():
users = [{"user": "tacticalrmm", "password": settings.SECRET_KEY}]
agents = Agent.objects.prefetch_related("user").only("pk", "agent_id")
for agent in agents:
users.append({"user": agent.agent_id, "password": agent.user.auth_token.key})
if not settings.DOCKER_BUILD:
tld = tldextract.extract(settings.ALLOWED_HOSTS[0])
domain = tld.domain + "." + tld.suffix
cert_path = f"/etc/letsencrypt/live/{domain}"
else:
cert_path = "/opt/tactical/certs"
config = {
"tls": {
"cert_file": f"{cert_path}/fullchain.pem",
"key_file": f"{cert_path}/privkey.pem",
},
"authorization": {"users": users},
"max_payload": 2048576005,
}
conf = os.path.join(settings.BASE_DIR, "nats-rmm.conf")
with open(conf, "w") as f:
json.dump(config, f)
if not settings.DOCKER_BUILD:
subprocess.run(
["/usr/local/bin/nats-server", "-signal", "reload"], capture_output=True
)

18
backup.sh Normal file → Executable file
View File

@@ -1,7 +1,7 @@
#!/bin/bash
SCRIPT_VERSION="2"
SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/develop/backup.sh'
SCRIPT_VERSION="3"
SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/backup.sh'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
@@ -31,11 +31,25 @@ POSTGRES_PW="hunter2"
#####################################################
if [[ "$POSTGRES_USER" == "changeme" || "$POSTGRES_PW" == "hunter2" ]]; then
printf >&2 "${RED}You must change the postgres username/password at the top of this file.${NC}\n"
printf >&2 "${RED}Check the github readme for where to find them.${NC}\n"
exit 1
fi
if [ ! -d /rmmbackups ]; then
sudo mkdir /rmmbackups
sudo chown ${USER}:${USER} /rmmbackups
fi
if [ -d /meshcentral/meshcentral-backup ]; then
rm -f /meshcentral/meshcentral-backup/*
fi
if [ -d /meshcentral/meshcentral-coredumps ]; then
rm -f /meshcentral/meshcentral-coredumps/*
fi
dt_now=$(date '+%Y_%m_%d__%H_%M_%S')
tmp_dir=$(mktemp -d -t tacticalrmm-XXXXXXXXXXXXXXXXXXXXX)
sysd="/etc/systemd/system"

View File

@@ -1,24 +1,21 @@
MESH_HOST=mesh.example.com
MESH_USER=mesh
MESH_PASS=meshpass
EMAIL_USER=admin@example.com
IMAGE_REPO=tacticalrmm/
VERSION=latest
# tactical credentials (Used to login to dashboard)
TRMM_USER=tactical
TRMM_PASS=tactical
# dns settings
APP_HOST=app.example.com
API_HOST=api.example.com
MESH_HOST=mesh.example.com
# mesh settings
MESH_USER=meshcentral
MESH_PASS=meshcentralpass
MONGODB_USER=mongouser
MONGODB_PASSWORD=mongopass
# database settings
POSTGRES_USER=postgres
POSTGRES_PASS=pass
POSTGRES_HOST=db
APP_HOST=app.example.com
API_HOST=api.example.com
REDIS_HOST=redis
SALT_HOST=salt
SALT_USER=saltapi
SALT_PASS=password
ADMIN_URL=admin
DJANGO_SEKRET=secret12341234123412341234
DJANGO_DEBUG=False
POSTGRES_PASS=postgrespass

View File

@@ -1,64 +0,0 @@
user nginx;
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
server_tokens off;
upstream tacticalrmm {
server unix:///app/tacticalrmm.sock;
}
server {
listen 80;
#server_name ${API_HOST};
client_max_body_size 300M;
access_log /var/log/nginx/api-access.log;
error_log /var/log/nginx/api-error.log;
location /static/ {
root /app;
}
location /private/ {
internal;
add_header "Access-Control-Allow-Origin" "https://${APP_HOST}";
alias /app/tacticalrmm/private/;
}
location /saltscripts/ {
internal;
add_header "Access-Control-Allow-Origin" "https://${APP_HOST}";
alias /srv/salt/scripts/userdefined/;
}
location /builtin/ {
internal;
add_header "Access-Control-Allow-Origin" "https://${APP_HOST}";
alias /srv/salt/scripts/;
}
location / {
uwsgi_pass tacticalrmm;
include /etc/nginx/uwsgi_params;
uwsgi_read_timeout 9999s;
uwsgi_ignore_client_abort on;
}
}
}
daemon off;

View File

@@ -1,46 +0,0 @@
FROM tiangolo/uwsgi-nginx:python3.8
WORKDIR /app
ARG DJANGO_SEKRET
ARG DJANGO_DEBUG
ARG POSTGRES_USER
ARG POSTGRES_PASS
ARG POSTGRES_HOST
ARG SALT_HOST
ARG SALT_USER
ARG SALT_PASS
ARG REDIS_HOST
ARG MESH_USER
ARG MESH_HOST
ARG MESH_TOKEN_KEY
ARG APP_HOST
ARG API_HOST
ARG ADMIN_URL
EXPOSE 80
RUN apt-get update && apt-get install -y gettext-base wget ca-certificates
COPY ./api/tacticalrmm/requirements.txt .
RUN pip install --upgrade pip
RUN pip install --no-cache-dir setuptools==49.6.0 wheel==0.35.1
RUN pip install --no-cache-dir -r requirements.txt
RUN wget https://golang.org/dl/go1.15.linux-amd64.tar.gz -P /tmp
COPY ./api/tacticalrmm/ .
COPY ./scripts/ /scripts
COPY ./docker/api/prestart.sh .
COPY ./docker/api/uwsgi.ini .
COPY ./docker/api/api.conf /app/api.conf.tmp
COPY ./api/tacticalrmm/core/goinstaller/bin/goversioninfo /usr/local/bin/goversioninfo
RUN envsubst '\$APP_HOST, \$API_HOST' < /app/api.conf.tmp > /app/nginx.conf && \
rm /app/api.conf.tmp
COPY ./docker/api/local_settings.py.keep ./tacticalrmm/local_settings.py.tmp
RUN envsubst < /app/tacticalrmm/local_settings.py.tmp > /app/tacticalrmm/local_settings.py && rm /app/tacticalrmm/local_settings.py.tmp
RUN tar -xzf /tmp/go1.15.linux-amd64.tar.gz -C /tmp && \
mkdir /usr/local/rmmgo && \
mv /tmp/go /usr/local/rmmgo/ && \
rm -rf /tmp/go
RUN /usr/local/rmmgo/go/bin/go get github.com/josephspurrier/goversioninfo/cmd/goversioninfo && \
chmod +x /usr/local/bin/goversioninfo

View File

@@ -1,47 +0,0 @@
SECRET_KEY = '${DJANGO_SEKRET}'
ALLOWED_HOSTS = ['${API_HOST}']
ADMIN_URL = "${ADMIN_URL}"
CORS_ORIGIN_WHITELIST = ["https://${APP_HOST}",]
DEBUG = ${DJANGO_DEBUG}
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'tacticalrmm',
'USER': '${POSTGRES_USER}',
'PASSWORD': '${POSTGRES_PASS}',
'HOST': '${POSTGRES_HOST}',
'PORT': '5432',
}
}
REST_FRAMEWORK = {
'DATETIME_FORMAT': "%b-%d-%Y - %H:%M",
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
'DEFAULT_AUTHENTICATION_CLASSES': (
'knox.auth.TokenAuthentication',
),
}
if not DEBUG:
REST_FRAMEWORK.update({
'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.JSONRenderer',
)
})
SALT_USERNAME = "${SALT_USER}"
SALT_PASSWORD = "${SALT_PASS}"
MESH_USERNAME = "${MESH_USER}"
MESH_SITE = "https://${MESH_HOST}"
MESH_WS_URL="ws://meshcentral:443"
MESH_TOKEN_KEY = "${MESH_TOKEN_KEY}"
REDIS_HOST = "${REDIS_HOST}"
SALT_HOST = "${SALT_HOST}"

View File

@@ -1,10 +0,0 @@
#! /usr/bin/env bash
sleep 10
python manage.py migrate --no-input
python manage.py collectstatic --no-input
python manage.py initial_db_setup
python manage.py initial_mesh_setup
python manage.py load_chocos
python manage.py fix_salt_key
python manage.py load_community_scripts

View File

@@ -1,14 +0,0 @@
[uwsgi]
logto = /app/tacticalrmm/private/log/uwsgi.log
chdir = /app
wsgi-file = tacticalrmm/wsgi.py
master = true
processes = 4
threads = 2
socket = /app/tacticalrmm.sock
# clear environment on exit
vacuum = true
die-on-term = true
max-requests = 500
max-requests-delta = 1000

View File

@@ -1,2 +0,0 @@
PROD_URL = "https://${API_HOST}"
DEV_URL = "https://${API_HOST}"

View File

@@ -1,16 +0,0 @@
server {
listen 80;
#server_name ${APP_HOST};
charset utf-8;
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
add_header Cache-Control "no-store, no-cache, must-revalidate";
add_header Pragma "no-cache";
}
error_log /var/log/nginx/app-error.log;
access_log /var/log/nginx/app-access.log;
}

View File

@@ -1,19 +0,0 @@
FROM node:12-alpine AS builder
ARG APP_HOST
ARG API_HOST
EXPOSE 80
WORKDIR /home/node
RUN apk add gettext
COPY ./web/package.json .
RUN npm install
COPY ./docker/app/.env.keep /home/.env.tmp
RUN envsubst '\$API_HOST' < /home/.env.tmp > /home/node/.env && rm /home/.env.tmp
COPY ./docker/app/app.conf /home/node/app.conf.tmp
RUN envsubst '\$APP_HOST' < /home/node/app.conf.tmp > /home/node/app.conf
COPY ./web .
RUN npm run build
FROM nginx:alpine
WORKDIR /usr/share/nginx/html
COPY --from=builder /home/node/dist .
COPY --from=builder /home/node/app.conf /etc/nginx/conf.d/default.conf

View File

@@ -0,0 +1,29 @@
FROM node:12-alpine AS builder
WORKDIR /home/node/app
COPY ./web/package.json .
RUN npm install
COPY ./web .
# copy env file to set DOCKER_BUILD to true
RUN echo "DOCKER_BUILD=1" > .env
# modify index.html template to allow injection of js variables at runtime
RUN sed -i '/<\/head>/i <script src="\/env-config.js"><\/script>' src/index.template.html
RUN npm run build
FROM nginx:stable-alpine
ENV PUBLIC_DIR /usr/share/nginx/html
RUN apk add --no-cache bash
SHELL ["/bin/bash", "-c"]
COPY --from=builder /home/node/app/dist/ ${PUBLIC_DIR}
COPY docker/containers/tactical-frontend/entrypoint.sh /docker-entrypoint.d/
RUN chmod +x /docker-entrypoint.d/entrypoint.sh
EXPOSE 80

View File

@@ -0,0 +1,31 @@
#!/usr/bin/env bash
#
# https://www.freecodecamp.org/news/how-to-implement-runtime-environment-variables-with-create-react-app-docker-and-nginx-7f9d42a91d70/
#
# Recreate js config file on start
rm -rf ${PUBLIC_DIR}/env-config.js
touch ${PUBLIC_DIR}/env-config.js
# Add runtime base url assignment
echo "window._env_ = {PROD_URL: \"https://${API_HOST}\"}" >> ${PUBLIC_DIR}/env-config.js
nginx_config="$(cat << EOF
server {
listen 80;
charset utf-8;
location / {
root /usr/share/nginx/html;
try_files \$uri \$uri/ /index.html;
add_header Cache-Control "no-store, no-cache, must-revalidate";
add_header Pragma "no-cache";
}
error_log /var/log/nginx/app-error.log;
access_log /var/log/nginx/app-access.log;
}
EOF
)"
echo "${nginx_config}" > /etc/nginx/conf.d/default.conf

View File

@@ -0,0 +1,18 @@
FROM node:12-alpine
WORKDIR /home/node/app
ENV TACTICAL_DIR /opt/tactical
RUN apk add --no-cache bash
SHELL ["/bin/bash", "-c"]
RUN npm install meshcentral@0.6.62
COPY docker/containers/tactical-meshcentral/entrypoint.sh /
RUN chmod +x /entrypoint.sh
EXPOSE 80 443
ENTRYPOINT [ "/entrypoint.sh" ]

View File

@@ -0,0 +1,66 @@
#!/usr/bin/env bash
set -e
: "${MESH_USER:=meshcentral}"
: "${MESH_PASS:=meshcentralpass}"
: "${MONGODB_USER:=mongouser}"
: "${MONGODB_PASSWORD:=mongopass}"
: "${MONGODB_HOST:=tactical-mongodb}"
: "${MONGODB_PORT:=27017}"
: "${NGINX_HOST_IP:=172.20.0.20}"
mkdir -p /home/node/app/meshcentral-data
mkdir -p ${TACTICAL_DIR}/tmp
mesh_config="$(cat << EOF
{
"settings": {
"mongodb": "mongodb://${MONGODB_USER}:${MONGODB_PASSWORD}@${MONGODB_HOST}:${MONGODB_PORT}",
"Cert": "${MESH_HOST}",
"TLSOffload": "${NGINX_HOST_IP}",
"RedirPort": 80,
"WANonly": true,
"Minify": 1,
"Port": 443,
"AllowLoginToken": true,
"AllowFraming": true,
"_AgentPing": 60,
"AgentPong": 300,
"AllowHighQualityDesktop": true,
"MaxInvalidLogin": {
"time": 5,
"count": 5,
"coolofftime": 30
}
},
"domains": {
"": {
"Title": "Tactical RMM",
"Title2": "TacticalRMM",
"NewAccounts": false,
"mstsc": true,
"GeoLocation": true,
"CertUrl": "https://${NGINX_HOST_IP}:443",
"httpheaders": {
"Strict-Transport-Security": "max-age=360000",
"_x-frame-options": "sameorigin",
"Content-Security-Policy": "default-src 'none'; script-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; frame-src 'self'; media-src 'self'"
}
}
}
}
EOF
)"
echo "${mesh_config}" > /home/node/app/meshcentral-data/config.json
node node_modules/meshcentral --createaccount ${MESH_USER} --pass ${MESH_PASS} --email example@example.com
node node_modules/meshcentral --adminaccount ${MESH_USER}
if [ ! -f "${TACTICAL_DIR}/tmp/mesh_token" ]; then
node node_modules/meshcentral --logintokenkey > ${TACTICAL_DIR}/tmp/mesh_token
fi
# start mesh
node node_modules/meshcentral

View File

@@ -0,0 +1,15 @@
FROM nats:2.1-alpine
ENV TACTICAL_DIR /opt/tactical
ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready
RUN apk add --no-cache inotify-tools supervisor bash
SHELL ["/bin/bash", "-c"]
COPY docker/containers/tactical-nats/entrypoint.sh /
RUN chmod +x /entrypoint.sh
ENTRYPOINT [ "/entrypoint.sh" ]
EXPOSE 4222

View File

@@ -0,0 +1,37 @@
#!/usr/bin/env bash
set -e
sleep 15
until [ -f "${TACTICAL_READY_FILE}" ]; do
echo "waiting for init container to finish install or update..."
sleep 10
done
mkdir -p /var/log/supervisor
mkdir -p /etc/supervisor/conf.d
supervisor_config="$(cat << EOF
[supervisord]
nodaemon=true
[include]
files = /etc/supervisor/conf.d/*.conf
[program:nats-server]
command=nats-server -DVV --config "${TACTICAL_DIR}/api/nats-rmm.conf"
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
redirect_stderr=true
[program:config-watcher]
command="inotifywait -m -e close_write ${TACTICAL_DIR}/api/nats-rmm.conf"; | while read events; do "nats-server --signal reload"; done;
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
redirect_stderr=true
EOF
)"
echo "${supervisor_config}" > /etc/supervisor/conf.d/supervisor.conf
# run supervised processes
/usr/bin/supervisord -c /etc/supervisor/conf.d/supervisor.conf

View File

@@ -0,0 +1,12 @@
FROM nginx:stable-alpine
ENV TACTICAL_DIR /opt/tactical
RUN apk add --no-cache openssl bash
SHELL ["/bin/bash", "-c"]
COPY docker/containers/tactical-nginx/entrypoint.sh /docker-entrypoint.d/
RUN chmod +x /docker-entrypoint.d/entrypoint.sh
EXPOSE 443 80

View File

@@ -0,0 +1,173 @@
#!/usr/bin/env bash
set -e
CERT_PRIV_PATH=${TACTICAL_DIR}/certs/privkey.pem
CERT_PUB_PATH=${TACTICAL_DIR}/certs/fullchain.pem
mkdir -p "${TACTICAL_DIR}/certs"
# remove default config
rm -f /etc/nginx/conf.d/default.conf
# check for certificates in env variable
if [ ! -z "$CERT_PRIV_KEY" ] && [ ! -z "$CERT_PUB_KEY" ]; then
echo "${CERT_PRIV_KEY}" | base64 -d > ${CERT_PRIV_PATH}
echo "${CERT_PUB_KEY}" | base64 -d > ${CERT_PUB_PATH}
else
# generate a self signed cert
if [ ! -f "${CERT_PRIV_PATH}" ] || [ ! -f "${CERT_PUB_PATH}" ]; then
rootdomain=$(echo ${API_HOST} | cut -d "." -f2- )
openssl req -newkey rsa:4096 -x509 -sha256 -days 365 -nodes -out ${CERT_PUB_PATH} -keyout ${CERT_PRIV_PATH} -subj "/C=US/ST=Some-State/L=city/O=Internet Widgits Pty Ltd/CN=*.${rootdomain}"
fi
fi
nginx_config="$(cat << EOF
# backend config
server {
resolver 127.0.0.11 valid=30s;
server_name ${API_HOST};
location / {
#Using variable to disable start checks
set \$api http://tactical-backend;
proxy_pass \$api;
proxy_http_version 1.1;
proxy_cache_bypass \$http_upgrade;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_set_header X-Forwarded-Host \$host;
proxy_set_header X-Forwarded-Port \$server_port;
}
location /static/ {
root ${TACTICAL_DIR}/api;
}
location /private/ {
internal;
add_header "Access-Control-Allow-Origin" "https://${APP_HOST}";
alias ${TACTICAL_DIR}/api/tacticalrmm/private/;
}
location /saltscripts/ {
internal;
add_header "Access-Control-Allow-Origin" "https://${APP_HOST}";
alias ${TACTICAL_DIR}/scripts/userdefined/;
}
location /builtin/ {
internal;
add_header "Access-Control-Allow-Origin" "https://${APP_HOST}";
alias ${TACTICAL_DIR}/scripts/;
}
error_log /var/log/nginx/api-error.log;
access_log /var/log/nginx/api-access.log;
client_max_body_size 300M;
listen 443 ssl;
ssl_certificate ${CERT_PUB_PATH};
ssl_certificate_key ${CERT_PRIV_PATH};
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
}
server {
listen 80;
server_name ${API_HOST};
return 301 https://\$server_name\$request_uri;
}
# frontend config
server {
resolver 127.0.0.11 valid=30s;
server_name ${APP_HOST};
location / {
#Using variable to disable start checks
set \$app http://tactical-frontend;
proxy_pass \$app;
proxy_http_version 1.1;
proxy_cache_bypass \$http_upgrade;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_set_header X-Forwarded-Host \$host;
proxy_set_header X-Forwarded-Port \$server_port;
}
error_log /var/log/nginx/app-error.log;
access_log /var/log/nginx/app-access.log;
listen 443 ssl;
ssl_certificate ${CERT_PUB_PATH};
ssl_certificate_key ${CERT_PRIV_PATH};
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
}
server {
listen 80;
server_name ${APP_HOST};
return 301 https://\$server_name\$request_uri;
}
# meshcentral config
server {
resolver 127.0.0.11 valid=30s;
listen 443 ssl;
proxy_send_timeout 330s;
proxy_read_timeout 330s;
server_name ${MESH_HOST};
ssl_certificate ${CERT_PUB_PATH};
ssl_certificate_key ${CERT_PRIV_PATH};
ssl_session_cache shared:WEBSSL:10m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
location / {
#Using variable to disable start checks
set \$meshcentral http://tactical-meshcentral:443;
proxy_pass \$meshcentral;
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
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;
}
}
server {
resolver 127.0.0.11 valid=30s;
listen 80;
server_name ${MESH_HOST};
return 301 https://\$server_name\$request_uri;
}
EOF
)"
echo "${nginx_config}" > /etc/nginx/conf.d/default.conf

View File

@@ -0,0 +1,21 @@
FROM ubuntu:20.04
ENV TACTICAL_DIR /opt/tactical
ENV SALT_USER saltapi
RUN apt-get update && \
apt-get install -y ca-certificates wget gnupg2 tzdata supervisor && \
wget -O - https://repo.saltstack.com/py3/ubuntu/20.04/amd64/latest/SALTSTACK-GPG-KEY.pub | apt-key add - && \
echo 'deb http://repo.saltstack.com/py3/ubuntu/20.04/amd64/latest focal main' | tee /etc/apt/sources.list.d/saltstack.list && \
apt-get update && \
apt-get install -y salt-master salt-api && \
mkdir -p /var/log/supervisor && \
sed -i 's/msgpack_kwargs = {"raw": six.PY2}/msgpack_kwargs = {"raw": six.PY2, "max_buffer_size": 2147483647}/g' /usr/lib/python3/dist-packages/salt/transport/ipc.py && \
adduser --no-create-home --disabled-password --gecos "" ${SALT_USER}
EXPOSE 8123 4505 4506
COPY docker/containers/tactical-salt/entrypoint.sh /
RUN chmod +x /entrypoint.sh
ENTRYPOINT [ "/entrypoint.sh" ]

View File

@@ -0,0 +1,57 @@
#!/usr/bin/env bash
set -e
: "${SALT_USER:='saltapi'}"
# wait for salt password to be generated by tactical-init
until [ -f "${TACTICAL_DIR}/tmp/salt_pass" ]; do
echo "waiting for salt password to be generated..."
sleep 10
done
SALT_PASS=$(cat ${TACTICAL_DIR}/tmp/salt_pass)
echo "${SALT_USER}:${SALT_PASS}" | chpasswd
cherrypy_config="$(cat << EOF
module_dirs: ['/opt/tactical/_modules']
timeout: 20
gather_job_timeout: 25
max_event_size: 30485760
external_auth:
pam:
${SALT_USER}:
- .*
- '@runner'
- '@wheel'
- '@jobs'
rest_cherrypy:
port: 8123
disable_ssl: True
max_request_body_size: 30485760
EOF
)"
echo "${cherrypy_config}" > /etc/salt/master.d/rmm-salt.conf
supervisor_config="$(cat << EOF
[supervisord]
nodaemon=true
[include]
files = /etc/supervisor/conf.d/*.conf
[program:salt-master]
command=/bin/bash -c "salt-master -l debug"
redirect_stderr=true
[program:salt-api]
command=/bin/bash -c "salt-api -l debug"
redirect_stderr=true
EOF
)"
echo "${supervisor_config}" > /etc/supervisor/conf.d/supervisor.conf
# run salt and salt master
/usr/bin/supervisord

View File

@@ -0,0 +1,69 @@
# creates python virtual env
FROM python:3.8-slim AS CREATE_VENV_STAGE
ARG DEBIAN_FRONTEND=noninteractive
# # set env variables
ENV VIRTUAL_ENV /opt/venv
ENV TACTICAL_DIR /opt/tactical
ENV TACTICAL_TMP_DIR /tmp/tactical
RUN python3 -m venv $VIRTUAL_ENV
ENV PATH "${VIRTUAL_ENV}/bin:$PATH"
SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"]
COPY api/tacticalrmm/requirements.txt ${TACTICAL_TMP_DIR}/api/requirements.txt
RUN apt-get update && \
apt-get install -y --no-install-recommends gcc libc6-dev && \
rm -rf /var/lib/apt/lists/* && \
pip install --upgrade pip && \
pip install --no-cache-dir setuptools wheel gunicorn && \
sed -i '/uWSGI/d' ${TACTICAL_TMP_DIR}/api/requirements.txt && \
pip install --no-cache-dir -r ${TACTICAL_TMP_DIR}/api/requirements.txt
# runtime image
FROM python:3.8-slim
# set env variables
ENV VIRTUAL_ENV /opt/venv
ENV TACTICAL_DIR /opt/tactical
ENV TACTICAL_TMP_DIR /tmp/tactical
ENV TACTICAL_GO_DIR /usr/local/rmmgo
ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready
ENV TACTICAL_USER tactical
ENV PATH "${VIRTUAL_ENV}/bin:${TACTICAL_GO_DIR}/go/bin:$PATH"
# copy files from repo
COPY api/tacticalrmm ${TACTICAL_TMP_DIR}/api
COPY scripts ${TACTICAL_TMP_DIR}/scripts
COPY _modules ${TACTICAL_TMP_DIR}/_modules
# copy go install from build stage
COPY --from=golang:1.15 /usr/local/go ${TACTICAL_GO_DIR}/go
COPY --from=CREATE_VENV_STAGE ${VIRTUAL_ENV} ${VIRTUAL_ENV}
# install deps
RUN apt-get update && \
apt-get upgrade -y && \
apt-get install -y --no-install-recommends git && \
rm -rf /var/lib/apt/lists/* && \
go get github.com/josephspurrier/goversioninfo/cmd/goversioninfo && \
groupadd -g 1000 "${TACTICAL_USER}" && \
useradd -M -d "${TACTICAL_DIR}" -s /bin/bash -u 1000 -g 1000 "${TACTICAL_USER}"
SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"]
# overwrite goversioninfo file
COPY api/tacticalrmm/core/goinstaller/bin/goversioninfo /usr/local/bin/goversioninfo
RUN chmod +x /usr/local/bin/goversioninfo
# docker init
COPY docker/containers/tactical/entrypoint.sh /
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
WORKDIR ${TACTICAL_DIR}/api
EXPOSE 80

View File

@@ -0,0 +1,180 @@
#!/usr/bin/env bash
set -e
: "${TRMM_USER:=tactical}"
: "${TRMM_PASS:=tactical}"
: "${POSTGRES_HOST:=tactical-postgres}"
: "${POSTGRES_PORT:=5432}"
: "${POSTGRES_USER:=tactical}"
: "${POSTGRES_PASS:=tactical}"
: "${POSTGRES_DB:=tacticalrmm}"
: "${SALT_HOST:=tactical-salt}"
: "${SALT_USER:=saltapi}"
: "${MESH_CONTAINER:=tactical-meshcentral}"
: "${MESH_USER:=meshcentral}"
: "${MESH_PASS:=meshcentralpass}"
: "${MESH_HOST:=tactical-meshcentral}"
: "${API_HOST:=tactical-backend}"
: "${APP_HOST:=tactical-frontend}"
: "${REDIS_HOST:=tactical-redis}"
function check_tactical_ready {
sleep 15
until [ -f "${TACTICAL_READY_FILE}" ]; do
echo "waiting for init container to finish install or update..."
sleep 10
done
}
# tactical-init
if [ "$1" = 'tactical-init' ]; then
mkdir -p ${TACTICAL_DIR}/tmp
mkdir -p ${TACTICAL_DIR}/scripts/userdefined
test -f "${TACTICAL_READY_FILE}" && rm "${TACTICAL_READY_FILE}"
# copy container data to volume
cp -af ${TACTICAL_TMP_DIR}/. ${TACTICAL_DIR}/
until (echo > /dev/tcp/"${POSTGRES_HOST}"/"${POSTGRES_PORT}") &> /dev/null; do
echo "waiting for postgresql container to be ready..."
sleep 5
done
until (echo > /dev/tcp/"${MESH_CONTAINER}"/443) &> /dev/null; do
echo "waiting for meshcentral container to be ready..."
sleep 5
done
# configure django settings
MESH_TOKEN=$(cat ${TACTICAL_DIR}/tmp/mesh_token)
ADMINURL=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 70 | head -n 1)
DJANGO_SEKRET=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 80 | head -n 1)
# write salt pass to tmp dir
if [ ! -f "${TACTICAL__DIR}/tmp/salt_pass" ]; then
SALT_PASS=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 20 | head -n 1)
echo "${SALT_PASS}" > ${TACTICAL_DIR}/tmp/salt_pass
else
SALT_PASS=$(cat ${TACTICAL_DIR}/tmp/salt_pass)
fi
localvars="$(cat << EOF
SECRET_KEY = '${DJANGO_SEKRET}'
DEBUG = False
DOCKER_BUILD = True
SCRIPTS_DIR = '/opt/tactical/scripts'
ALLOWED_HOSTS = ['${API_HOST}']
ADMIN_URL = '${ADMINURL}/'
CORS_ORIGIN_WHITELIST = [
'https://${APP_HOST}'
]
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': '${POSTGRES_DB}',
'USER': '${POSTGRES_USER}',
'PASSWORD': '${POSTGRES_PASS}',
'HOST': '${POSTGRES_HOST}',
'PORT': '${POSTGRES_PORT}',
}
}
REST_FRAMEWORK = {
'DATETIME_FORMAT': '%b-%d-%Y - %H:%M',
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
'DEFAULT_AUTHENTICATION_CLASSES': (
'knox.auth.TokenAuthentication',
),
}
if not DEBUG:
REST_FRAMEWORK.update({
'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.JSONRenderer',
)
})
SALT_USERNAME = '${SALT_USER}'
SALT_PASSWORD = '${SALT_PASS}'
SALT_HOST = '${SALT_HOST}'
MESH_USERNAME = '${MESH_USER}'
MESH_SITE = 'https://${MESH_HOST}'
MESH_TOKEN_KEY = '${MESH_TOKEN}'
REDIS_HOST = '${REDIS_HOST}'
MESH_WS_URL = 'ws://${MESH_CONTAINER}:443'
EOF
)"
echo "${localvars}" > ${TACTICAL_DIR}/api/tacticalrmm/local_settings.py
# run migrations and init scripts
python manage.py migrate --no-input
python manage.py collectstatic --no-input
python manage.py initial_db_setup
python manage.py initial_mesh_setup
python manage.py load_chocos
python manage.py load_community_scripts
python manage.py reload_nats
# create super user
echo "from accounts.models import User; User.objects.create_superuser('${TRMM_USER}', 'admin@example.com', '${TRMM_PASS}') if not User.objects.filter(username='${TRMM_USER}').exists() else 0;" | python manage.py shell
# chown everything to tactical user
chown -R "${TACTICAL_USER}":"${TACTICAL_USER}" "${TACTICAL_DIR}"
# create install ready file
su -c "echo 'tactical-init' > ${TACTICAL_READY_FILE}" "${TACTICAL_USER}"
fi
# backend container
if [ "$1" = 'tactical-backend' ]; then
check_tactical_ready
# Prepare log files and start outputting logs to stdout
mkdir -p ${TACTICAL_DIR}/api/tacticalrmm/logs
touch ${TACTICAL_DIR}/api/tacticalrmm/logs/gunicorn.log
touch ${TACTICAL_DIR}/api/tacticalrmm/logs/gunicorn-access.log
tail -n 0 -f ${TACTICAL_DIR}/api/tacticalrmm/logs/gunicorn*.log &
export DJANGO_SETTINGS_MODULE=tacticalrmm.settings
exec gunicorn tacticalrmm.wsgi:application \
--name tactical-backend \
--bind 0.0.0.0:80 \
--workers 5 \
--log-level=info \
--log-file=${TACTICAL_DIR}/api/tacticalrmm/logs/gunicorn.log \
--access-logfile=${TACTICAL_DIR}/api/tacticalrmm/logs/gunicorn-access.log \
fi
if [ "$1" = 'tactical-celery' ]; then
check_tactical_ready
celery -A tacticalrmm worker
fi
if [ "$1" = 'tactical-celerybeat' ]; then
check_tactical_ready
test -f "${TACTICAL_DIR}/api/celerybeat.pid" && rm "${TACTICAL_DIR}/api/celerybeat.pid"
celery -A tacticalrmm beat
fi
if [ "$1" = 'tactical-celerywinupdate' ]; then
check_tactical_ready
celery -A tacticalrmm worker -Q wupdate
fi

View File

@@ -1,99 +0,0 @@
# FOR DEV
version: "3.7"
services:
# Container that hosts Vue frontend
app:
image: node:12
command: /bin/bash -c "npm install && npm run serve -- --host 0.0.0.0 --port 80 --public ${APP_HOST}"
working_dir: /home/node
volumes:
- ../web:/home/node
networks:
- proxy
# Builds Python Virtual Env to share between containers
venv:
image: python:3.8
command: /bin/bash -c "pip install virtualenv && python -m virtualenv env && ./env/bin/pip install -r requirements.txt && ./env/bin/pip install -r requirements-dev.txt"
working_dir: /app
volumes:
- ../api/tacticalrmm:/app
# Container for Django backend
api:
image: python:3.8
command: /bin/bash -c "python manage.py collectstatic --clear --no-input && python manage.py migrate && sleep 10s && python manage.py initial_db_setup && python manage.py initial_mesh_setup && python manage.py load_chocos && python manage.py runserver 0.0.0.0:80"
working_dir: /app
environment:
VIRTUAL_ENV: /app/env
PATH: /app/env/bin:/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
networks:
- proxy
- database
- redis
volumes:
- scripts:/srv
- mesh_token:/token
- ../api/tacticalrmm:/app
depends_on:
- db
- venv
- meshcentral
# Container for Celery worker service
celery-service:
image: python:3.8
command: celery -A tacticalrmm worker -l debug
working_dir: /app
environment:
VIRTUAL_ENV: /app/env
PATH: /app/env/bin:/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
volumes:
- ../api/tacticalrmm:/app
networks:
- redis
- proxy
- database
depends_on:
- db
- redis
- venv
# Container for Celery beat service
celery-beat:
image: python:3.8
command: celery -A tacticalrmm beat -l debug
working_dir: /app
environment:
VIRTUAL_ENV: /app/env
PATH: /app/env/bin:/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
volumes:
- ../api/tacticalrmm:/app
networks:
- redis
- proxy
- database
depends_on:
- db
- redis
- venv
# Container for Celery Winupdate tasks
celery-winupdate:
image: python:3.8
command: celery -A tacticalrmm worker -Q wupdate -l debug
working_dir: /app
environment:
VIRTUAL_ENV: /app/env
PATH: /app/env/bin:/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
volumes:
- ../api/tacticalrmm:/app
networks:
- redis
- proxy
- database
depends_on:
- db
- redis
- venv

View File

@@ -1,143 +0,0 @@
# FOR PROD
version: "3.7"
volumes:
# Gives access to the debug log for celery tasks
debug_log:
services:
# Container that hosts Vue frontend
app:
build:
context: ..
args:
- APP_HOST=${APP_HOST}
- API_HOST=${API_HOST}
dockerfile: "./docker/app/dockerfile"
networks:
- proxy
# Container for Django backend
api:
build:
context: ..
dockerfile: "./docker/api/dockerfile"
args:
- DJANGO_SEKRET=${DJANGO_SEKRET}
- DJANGO_DEBUG=${DJANGO_DEBUG}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASS=${POSTGRES_PASS}
- POSTGRES_HOST=${POSTGRES_HOST}
- SALT_PASS=${SALT_PASS}
- SALT_USER=${SALT_USER}
- SALT_HOST=${SALT_HOST}
- REDIS_HOST=${REDIS_HOST}
- MESH_USER=${MESH_USER}
- MESH_HOST=${MESH_HOST}
- APP_HOST=${APP_HOST}
- API_HOST=${API_HOST}
- ADMIN_URL=${ADMIN_URL}
networks:
- proxy
- database
- redis
volumes:
- scripts:/srv
- mesh_token:/token
- debug_log:/app/tacticalrmm/private/log
depends_on:
- db
- meshcentral
# Container for Celery worker service
celery-service:
build:
context: ..
dockerfile: "./docker/api/dockerfile"
args:
- DJANGO_SEKRET=${DJANGO_SEKRET}
- DJANGO_DEBUG=${DJANGO_DEBUG}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASS=${POSTGRES_PASS}
- POSTGRES_HOST=${POSTGRES_HOST}
- SALT_PASS=${SALT_PASS}
- SALT_USER=${SALT_USER}
- SALT_HOST=${SALT_HOST}
- REDIS_HOST=${REDIS_HOST}
- MESH_USER=${MESH_USER}
- MESH_HOST=${MESH_HOST}
- APP_HOST=${APP_HOST}
- API_HOST=${API_HOST}
- ADMIN_URL=${ADMIN_URL}
command: celery -A tacticalrmm worker -l debug
networks:
- redis
- proxy
- database
volumes:
- debug_log:/app/tacticalrmm/private/log
depends_on:
- db
- redis
# Container for Celery beat service
celery-beat:
build:
context: ..
dockerfile: "./docker/api/dockerfile"
args:
- DJANGO_SEKRET=${DJANGO_SEKRET}
- DJANGO_DEBUG=${DJANGO_DEBUG}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASS=${POSTGRES_PASS}
- POSTGRES_HOST=${POSTGRES_HOST}
- SALT_PASS=${SALT_PASS}
- SALT_USER=${SALT_USER}
- SALT_HOST=${SALT_HOST}
- REDIS_HOST=${REDIS_HOST}
- MESH_USER=${MESH_USER}
- MESH_HOST=${MESH_HOST}
- APP_HOST=${APP_HOST}
- API_HOST=${API_HOST}
- ADMIN_URL=${ADMIN_URL}
command: celery -A tacticalrmm beat -l debug
networks:
- redis
- proxy
- database
volumes:
- debug_log:/app/tacticalrmm/private/log
depends_on:
- db
- redis
# Container for Celery Winupdate tasks
celery-winupdate:
build:
context: ..
dockerfile: "./docker/api/dockerfile"
args:
- DJANGO_SEKRET=${DJANGO_SEKRET}
- DJANGO_DEBUG=${DJANGO_DEBUG}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASS=${POSTGRES_PASS}
- POSTGRES_HOST=${POSTGRES_HOST}
- SALT_PASS=${SALT_PASS}
- SALT_USER=${SALT_USER}
- SALT_HOST=${SALT_HOST}
- REDIS_HOST=${REDIS_HOST}
- MESH_USER=${MESH_USER}
- MESH_HOST=${MESH_HOST}
- APP_HOST=${APP_HOST}
- API_HOST=${API_HOST}
- ADMIN_URL=${ADMIN_URL}
command: celery -A tacticalrmm worker -Q wupdate -l debug
networks:
- redis
- proxy
- database
volumes:
- debug_log:/app/tacticalrmm/private/log
depends_on:
- db
- redis

View File

@@ -1,6 +1,6 @@
version: "3.7"
# Userdefined Networks
# networks
networks:
proxy:
driver: bridge
@@ -8,102 +8,202 @@ networks:
driver: default
config:
- subnet: 172.20.0.0/24
database:
api-db:
redis:
mesh-mongodb:
mesh-db:
# Docker managed persistent volumes
# docker managed persistent volumes
volumes:
# Volume for userdefined scripts
scripts:
# Volume for mesh token initial setup
mesh_token:
# Used to make the salt data persistent
tactical_data:
salt_data:
# Makes Postgres data persistent
postgres_data13:
# Makes mesh central data persistent
postgres_data:
mongo_data:
mesh_data:
services:
# Postgres Database for API service
db:
image: postgres:13
# postgres database for api service
tactical-postgres:
image: postgres:13-alpine
restart: always
environment:
POSTGRES_DB: tacticalrmm
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASS}
volumes:
- postgres_data13:/var/lib/postgresql/data
- postgres_data:/var/lib/postgresql/data
networks:
- database
- api-db
# Redis Container for Celery tasks
redis:
image: redis
# redis container for celery tasks
tactical-redis:
image: redis:6.0-alpine
restart: always
networks:
- redis
# Salt Master and API
salt:
build:
context: ..
dockerfile: ./docker/salt/dockerfile
args:
- SALT_USER=${SALT_USER}
- SALT_PASS=${SALT_PASS}
# used to initialize the docker environment
tactical-init:
image: ${IMAGE_REPO}tactical:${VERSION}
restart: on-failure
command: ["tactical-init"]
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASS: ${POSTGRES_PASS}
APP_HOST: ${APP_HOST}
API_HOST: ${API_HOST}
MESH_HOST: ${MESH_HOST}
TRMM_USER: ${TRMM_USER}
TRMM_PASS: ${TRMM_PASS}
depends_on:
- tactical-postgres
- tactical-meshcentral
networks:
- api-db
- proxy
volumes:
- tactical_data:/opt/tactical
# salt master and api
tactical-salt:
image: ${IMAGE_REPO}tactical-salt:${VERSION}
restart: always
ports:
- "4505:4505"
- "4506:4506"
volumes:
- scripts:/srv
- tactical_data:/opt/tactical
- salt_data:/etc/salt
networks:
- proxy
# MeshCentral Container
meshcentral:
build:
context: ./meshcentral
args:
- MESH_HOST=${MESH_HOST}
- MESH_USER=${MESH_USER}
- MESH_PASS=${MESH_PASS}
- EMAIL_USER=${EMAIL_USER}
- MONGODB_USER=${MONGODB_USER}
- MONGODB_PASSWORD=${MONGODB_PASSWORD}
# nats
tactical-nats:
image: ${IMAGE_REPO}tactical-nats:${VERSION}
restart: always
ports:
- "4222:4222"
volumes:
- tactical_data:/opt/tactical
networks:
proxy:
aliases:
- ${API_HOST}
# meshcentral container
tactical-meshcentral:
image: ${IMAGE_REPO}tactical-meshcentral:${VERSION}
restart: always
environment:
MESH_HOST: ${MESH_HOST}
MESH_USER: ${MESH_USER}
MESH_PASS: ${MESH_PASS}
MONGODB_USER: ${MONGODB_USER}
MONGODB_PASSWORD: ${MONGODB_PASSWORD}
networks:
- proxy
- mesh-mongodb
- mesh-db
volumes:
- mesh_token:/token
- tactical_data:/opt/tactical
- mesh_data:/home/node/app/meshcentral-data
depends_on:
- mesh-mongodb
- nginx-proxy
- tactical-mongodb
# MongoDB Container for MeshCentral
mesh-mongodb:
image: mongo
# mongodb container for meshcentral
tactical-mongodb:
image: mongo:4.4
restart: always
environment:
MONGO_INITDB_ROOT_USERNAME: ${MONGODB_USER}
MONGO_INITDB_ROOT_PASSWORD: ${MONGODB_PASSWORD}
MONGO_INITDB_DATABASE: meshcentral
networks:
- mesh-mongodb
- mesh-db
volumes:
- mongo_data:/data
- mongo_data:/data/db
# Nginx Container Reverse Proxy that handles all http/https traffic
nginx-proxy:
build:
context: ./nginx-proxy
args:
- APP_HOST=${APP_HOST}
- API_HOST=${API_HOST}
- MESH_HOST=${MESH_HOST}
ports:
- "80:80"
- "443:443"
# container that hosts vue frontend
tactical-frontend:
image: ${IMAGE_REPO}tactical-frontend:${VERSION}
restart: always
networks:
- proxy
environment:
API_HOST: ${API_HOST}
# container for django backend
tactical-backend:
image: ${IMAGE_REPO}tactical:${VERSION}
command: ["tactical-backend"]
restart: always
networks:
- proxy
- api-db
- redis
volumes:
- tactical_data:/opt/tactical
depends_on:
- tactical-postgres
tactical-nginx:
# container for tactical reverse proxy
image: ${IMAGE_REPO}tactical-nginx:${VERSION}
restart: always
environment:
APP_HOST: ${APP_HOST}
API_HOST: ${API_HOST}
MESH_HOST: ${MESH_HOST}
CERT_PUB_KEY: ${CERT_PUB_KEY}
CERT_PRIV_KEY: ${CERT_PRIV_KEY}
networks:
proxy:
ipv4_address: 172.20.0.20
ports:
- "80:80"
- "443:443"
volumes:
- tactical_data:/opt/tactical
# container for celery worker service
tactical-celery:
image: ${IMAGE_REPO}tactical:${VERSION}
command: ["tactical-celery"]
restart: always
networks:
- redis
- proxy
- api-db
volumes:
- tactical_data:/opt/tactical
depends_on:
- tactical-postgres
- tactical-redis
# container for celery beat service
tactical-celerybeat:
image: ${IMAGE_REPO}tactical:${VERSION}
command: ["tactical-celerybeat"]
restart: always
networks:
- proxy
- redis
- api-db
volumes:
- tactical_data:/opt/tactical
depends_on:
- tactical-postgres
- tactical-redis
# container for celery winupdate tasks
tactical-celerywinupdate:
image: ${IMAGE_REPO}tactical:${VERSION}
command: ["tactical-celerywinupdate"]
restart: always
networks:
- redis
- proxy
- api-db
volumes:
- tactical_data:/opt/tactical
depends_on:
- tactical-postgres
- tactical-redis

13
docker/image-build.sh Executable file
View File

@@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -o errexit
set -o pipefail
DOCKER_IMAGES="tactical tactical-frontend tactical-nginx tactical-meshcentral tactical-salt tactical-nats"
cd ..
for DOCKER_IMAGE in ${DOCKER_IMAGES}; do
echo "Building Tactical Image: ${DOCKER_IMAGE}..."
docker build --pull --no-cache -t "${DOCKER_IMAGE}" -f "docker/containers/${DOCKER_IMAGE}/dockerfile" .
done

View File

@@ -1,36 +0,0 @@
{
"settings": {
"mongodb": "mongodb://${MONGODB_USER}:${MONGODB_PASSWORD}@mesh-mongodb:27017",
"Cert": "${MESH_HOST}",
"TLSOffload": "172.20.0.20",
"RedirPort": 80,
"WANonly": true,
"Minify": 1,
"Port": 443,
"AllowLoginToken": true,
"AllowFraming": true,
"_AgentPing": 60,
"AgentPong": 300,
"AllowHighQualityDesktop": true,
"MaxInvalidLogin": {
"time": 5,
"count": 5,
"coolofftime": 30
}
},
"domains": {
"": {
"Title": "Dev RMM",
"Title2": "DevRMM",
"NewAccounts": false,
"mstsc": true,
"GeoLocation": true,
"CertUrl": "https://172.20.0.20:443",
"httpheaders": {
"Strict-Transport-Security": "max-age=360000",
"_x-frame-options": "sameorigin",
"Content-Security-Policy": "default-src 'none'; script-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; frame-src 'self'; media-src 'self'"
}
}
}
}

View File

@@ -1,21 +0,0 @@
FROM node:stretch
WORKDIR /home/node/app
ARG MESH_HOST
ARG MESH_USER
ARG MESH_PASS
ARG EMAIL_USER
ARG MONGODB_USER
ARG MONGODB_PASSWORD
RUN apt-get update && apt-get install -y gettext-base
RUN npm install meshcentral@0.6.62
COPY config.json ./meshcentral-data/config.json.tmp
RUN envsubst '\$MESH_HOST, \$MONGODB_USER, \$MONGODB_PASSWORD' < /home/node/app/meshcentral-data/config.json.tmp > /home/node/app/meshcentral-data/config.json && \
rm /home/node/app/meshcentral-data/config.json.tmp
COPY entry.sh ./entry.sh.tmp
RUN envsubst < /home/node/app/entry.sh.tmp > /home/node/app/entry.sh && \
rm /home/node/app/entry.sh.tmp && \
chmod +x entry.sh
CMD ./entry.sh

View File

@@ -1,11 +0,0 @@
#!/bin/bash
node node_modules/meshcentral --createaccount ${MESH_USER} --pass ${MESH_PASS} --email ${EMAIL_USER}
node node_modules/meshcentral --adminaccount ${MESH_USER}
FILE=/token/token.key
if [ ! -f "$FILE" ]; then
node ./node_modules/meshcentral --logintokenkey > /token/token.key
fi
node node_modules/meshcentral

View File

@@ -1,41 +0,0 @@
server {
resolver 127.0.0.11 valid=30s;
server_name ${API_HOST};
location / {
#Using variable to disable start checks
set $api http://api;
proxy_pass $api;
proxy_http_version 1.1;
proxy_cache_bypass $http_upgrade;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
}
error_log /var/log/nginx/api-error.log;
access_log /var/log/nginx/api-access.log;
client_max_body_size 300M;
listen 443 ssl;
ssl_certificate /cert/fullchain.pem;
ssl_certificate_key /cert/privkey.pem;
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
}
server {
listen 80;
server_name ${API_HOST};
return 301 https://$server_name$request_uri;
}

View File

@@ -1,40 +0,0 @@
server {
resolver 127.0.0.11 valid=30s;
server_name ${APP_HOST};
location / {
#Using variable to disable start checks
set $app http://app;
proxy_pass $app;
proxy_http_version 1.1;
proxy_cache_bypass $http_upgrade;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
}
error_log /var/log/nginx/app-error.log;
access_log /var/log/nginx/app-access.log;
listen 443 ssl;
ssl_certificate /cert/fullchain.pem;
ssl_certificate_key /cert/privkey.pem;
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
}
server {
listen 80;
server_name ${APP_HOST};
return 301 https://$server_name$request_uri;
}

View File

@@ -1,27 +0,0 @@
FROM nginx
WORKDIR /etc/nginx/conf.d
ARG APP_HOST
ARG API_HOST
ARG MESH_HOST
EXPOSE 80
EXPOSE 443
#Remove default NGINX config
RUN rm /etc/nginx/conf.d/default.conf
#Copy APP config
COPY app.conf ./app.conf.tmp
RUN envsubst '\$APP_HOST' < /etc/nginx/conf.d/app.conf.tmp > /etc/nginx/conf.d/app.conf && rm /etc/nginx/conf.d/app.conf.tmp
#Copy API config
COPY api.conf ./api.conf.tmp
RUN envsubst '\$API_HOST' < /etc/nginx/conf.d/api.conf.tmp > /etc/nginx/conf.d/api.conf && rm /etc/nginx/conf.d/api.conf.tmp
#Copy Mesh config
COPY mesh.conf ./mesh.conf.tmp
RUN envsubst '\$MESH_HOST' < /etc/nginx/conf.d/mesh.conf.tmp > /etc/nginx/conf.d/mesh.conf && rm /etc/nginx/conf.d/mesh.conf.tmp
#Copy Certs
COPY ./cert/*.pem /cert/

View File

@@ -1,39 +0,0 @@
server {
resolver 127.0.0.11 valid=30s;
listen 443 ssl;
proxy_send_timeout 330s;
proxy_read_timeout 330s;
server_name ${MESH_HOST};
ssl_certificate /cert/fullchain.pem;
ssl_certificate_key /cert/privkey.pem;
ssl_session_cache shared:WEBSSL:10m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
location / {
#Using variable to disable start checks
set $meshcentral http://meshcentral:443;
proxy_pass $meshcentral;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
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;
}
}
server {
resolver 127.0.0.11 valid=30s;
listen 80;
server_name ${MESH_HOST};
return 301 https://$server_name$request_uri;
}

View File

@@ -1,91 +1,72 @@
# Docker Setup
- install docker and docker-compose
- Obtain wildcard cert or individual certs for each subdomain
- You can copy any wildcard cert public and private key to the docker/nginx-proxy/certs folder.
## Generate certificates with certbot (Optional if you already have the certs)
- Install docker and docker-compose
- Obtain valid wildcard certificate for your domain. If certificates are not provided, a self-signed certificate will be generated and most agent functions won't work. See below on how to generate a free Let's Encrypt!
## Generate certificates with certbot
Install Certbot
```
sudo add-apt-repository ppa:certbot/certbot
sudo apt-get install certbot
```
Generate the wildcard certificate. Add the DNS entry for domain validation.
Generate the wildcard certificate. Add the DNS entry for domain validation. Replace `example.com` with your root doamin
```
sudo certbot certonly --manual -d *.example.com --agree-tos --no-bootstrap --manual-public-ip-logging-ok --preferred-challenges dns
```
Copy the fullchain.pem and privkey.pem to the nginx-proxy/cert directory.
## Configure DNS and Firewall
## Configure DNS and firewall
You will need to add DNS entries so that the three subdomains resolve to the IP of the docker host. There is a reverse proxy running that will route the hostnames to the correct container. On the host, you will need to ensure the firewall is open on tcp ports 80, 443, 4505, 4506.
You will need to add DNS entries so that the three subdomains resolve to the IP of the docker host. There is a reverse proxy running that will route the hostnames to the correct container. On the host, you will need to ensure the firewall is open on tcp ports 80, 443, 4222, 4505, 4506.
## Run the environment with Docker
## Setting up the environment
Copy the .env.example to .env then
change values in .env to match your environment
Get the docker-compose and .env.example file on the host you which to install on
```
wget https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/docker/docker-compose.yml
wget https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/docker/.env.example
mv .env.example .env
```
Change the values in .env to match your environment.
If you are supplying certificates through Let's Encrypt or another source, see the section below about base64 encoding the certificate files.
## Base64 encoding certificates to pass as env variables
Use the below command to add the the correct values to the .env.
Running this command multiple times will add redundant entries, so those will need to be removed.
Let's encrypt certs paths are below. Replace ${rootdomain} with your own.
public key
`/etc/letsencrypt/live/${rootdomain}/fullchain.pem`
private key
`/etc/letsencrypt/live/${rootdomain}/privkey.pem`
```
echo "CERT_PUB_KEY=$(sudo base64 -w 0 /path/to/pub/key)" >> .env
echo "CERT_PRIV_KEY=$(sudo base64 -w 0 /path/to/priv/key)" >> .env
```
## Starting the environment
Run the below command to start the environment.
```
cd docker
sudo docker-compose up -d
```
You may need to run this twice if some containers fail to start
## Create a super user
```
sudo docker-compose exec api python manage.py createsuperuser
```
Removing the -d will start the containers in the foreground and is useful for debugging.
## Get MeshCentral EXE download link
Run the below command to get the download link for the mesh central exe. The dashboard will ask for this when you first sign in
Run the below command to get the download link for the mesh central exe. This needs to be uploaded on first successful signin.
```
sudo docker-compose exec api python manage.py get_mesh_exe_url
sudo docker-compose exec tactical-backend python manage.py get_mesh_exe_url
```
## Connect to a container instance shell
The below command opens up a shell to the api service.
```
sudo docker-compose exec api /bin/bash
```
If /bin/bash doesn't work then /bin/sh might need to be used.
## Using Docker for Dev (optional)
This allows you to edit the files locally and those changes will be presented to the containers. Hot Module Reload (Vue/webpack) and the Python equivalent will also work!
### Setup
Files that need to be manually created are:
- api/tacticalrmm/tacticalrmm/local_settings.py
- web/.env
Make sure to add `MESH_WS_URL="ws://meshcentral:443"` in the local_settings.py file. This is needed for the mesh central setup
For HMR to work with vue you can copy .env.example and modify the setting to fit your dev environment.
### Create Python Virtual Env
Each python container shares the same virtual env to make spinning up faster. It is located in api/tacticalrmm/env.
There is a container dedicated to creating and keeping this up to date. Prior to spinning up the environment you can run `docker-compose -f docker-compose.yml -f docker-compose.dev.yml up venv` to make sure the virtual env is ready. Otherwise the api and celery containers will fail to start.
### Spinup the environment
Now run `docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d` to spin everything else up
This will mount the local vue and python files in the app container with hot reload. Does not require rebuilding when changes to code are made and the changes will take effect immediately!
### Other Considerations
- It is recommended that you use the vscode docker plugin to manage containers. Docker desktop works well too on Windows.

View File

@@ -1,14 +0,0 @@
timeout: 20
gather_job_timeout: 25
max_event_size: 30485760
external_auth:
pam:
${SALT_USER}:
- .*
- '@runner'
- '@wheel'
- '@jobs'
rest_cherrypy:
port: 8123
disable_ssl: True
max_request_body_size: 30485760

View File

@@ -1,30 +0,0 @@
FROM ubuntu:20.04
ARG SALT_USER
ARG SALT_PASS
RUN adduser --no-create-home --disabled-password --gecos "" ${SALT_USER} && \
echo "${SALT_USER}:${SALT_PASS}" | chpasswd && \
apt-get update && \
apt-get install -y ca-certificates wget gnupg2 gettext-base tzdata supervisor && \
wget -O - https://repo.saltstack.com/py3/ubuntu/20.04/amd64/latest/SALTSTACK-GPG-KEY.pub | apt-key add - && \
echo 'deb http://repo.saltstack.com/py3/ubuntu/20.04/amd64/latest focal main' | tee /etc/apt/sources.list.d/saltstack.list && \
apt-get update && \
apt-get install -y salt-master salt-api && \
mkdir -p /var/log/supervisor
COPY ./docker/salt/api.conf /etc/salt/master.d/rmm-salt.tmp
RUN envsubst '\$SALT_USER' < /etc/salt/master.d/rmm-salt.tmp | tee /etc/salt/master.d/rmm-salt.conf && \
rm /etc/salt/master.d/rmm-salt.tmp
RUN sed -i 's/msgpack_kwargs = {"raw": six.PY2}/msgpack_kwargs = {"raw": six.PY2, "max_buffer_size": 2147483647}/g' /usr/lib/python3/dist-packages/salt/transport/ipc.py
COPY ./docker/salt/supervisor.conf /etc/supervisor/conf.d/supervisor.conf
COPY ./_modules /srv/salt/_modules
COPY ./scripts /srv/salt/scripts
RUN mkdir -p /srv/salt/scripts/userdefined && \
chown -R 1000:102 /srv/salt/scripts/userdefined && \
chmod -R 771 /srv/salt/scripts/userdefined
EXPOSE 8123 4505 4506
CMD ["/usr/bin/supervisord"]

View File

@@ -1,12 +0,0 @@
[supervisord]
nodaemon=true
[include]
files = /etc/supervisor/conf.d/*.conf
[program:salt-master]
command=/bin/bash -c "salt-master -l debug"
redirect_stderr=true
[program:salt-api]
command=/bin/bash -c "salt-api -l debug"
redirect_stderr=true

98
install.sh Executable file → Normal file
View File

@@ -1,7 +1,7 @@
#!/bin/bash
SCRIPT_VERSION="18"
SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/develop/install.sh'
SCRIPT_VERSION="23"
SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/install.sh'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
@@ -14,8 +14,9 @@ curl -s -L "${SCRIPT_URL}" > ${TMP_FILE}
NEW_VER=$(grep "^SCRIPT_VERSION" "$TMP_FILE" | awk -F'[="]' '{print $3}')
if [ "${SCRIPT_VERSION}" -ne "${NEW_VER}" ]; then
printf >&2 "${YELLOW}A newer version of this installer script is available.${NC}\n"
printf >&2 "${YELLOW}Please download the latest version from ${GREEN}${SCRIPT_URL}${YELLOW} and re-run.${NC}\n"
printf >&2 "${YELLOW}Old install script detected, downloading and replacing with the latest version...${NC}\n"
wget -q "${SCRIPT_URL}" -O install.sh
printf >&2 "${YELLOW}Script updated! Please re-run ./install.sh${NC}\n"
rm -f $TMP_FILE
exit 1
fi
@@ -139,35 +140,19 @@ sudo apt install -y software-properties-common
sudo apt update
sudo apt install -y certbot openssl
until [[ $LETS_ENCRYPT =~ (y|n) ]]; do
echo -ne "${YELLOW}Do you want to generate a Let's Encrypt certificate?[y,n]${NC}: "
read LETS_ENCRYPT
print_green 'Getting wildcard cert'
sudo certbot certonly --manual -d *.${rootdomain} --agree-tos --no-bootstrap --manual-public-ip-logging-ok --preferred-challenges dns -m ${letsemail} --no-eff-email
while [[ $? -ne 0 ]]
do
sudo certbot certonly --manual -d *.${rootdomain} --agree-tos --no-bootstrap --manual-public-ip-logging-ok --preferred-challenges dns -m ${letsemail} --no-eff-email
done
if [[ $LETS_ENCRYPT == "y" ]]; then
CERT_PRIV_KEY=/etc/letsencrypt/live/${rootdomain}/privkey.pem
CERT_PUB_KEY=/etc/letsencrypt/live/${rootdomain}/fullchain.pem
print_green 'Getting wildcard cert'
sudo certbot certonly --manual -d *.${rootdomain} --agree-tos --no-bootstrap --manual-public-ip-logging-ok --preferred-challenges dns -m ${letsemail} --no-eff-email
while [[ $? -ne 0 ]]
do
sudo certbot certonly --manual -d *.${rootdomain} --agree-tos --no-bootstrap --manual-public-ip-logging-ok --preferred-challenges dns -m ${letsemail} --no-eff-email
done
CERT_PRIV_KEY=/etc/letsencrypt/live/${rootdomain}/privkey.pem
CERT_PUB_KEY=/etc/letsencrypt/live/${rootdomain}/fullchain.pem
else
echo -ne "\n${GREEN}We will generate a self-signed certificate for you.${NC}\n"
echo -ne "\n${GREEN}You can replace this certificate later by generating the certificates and editing the nginx configuration${NC}\n"
read -n 1 -s -r -p "Press any key to continue..."
sudo mkdir -p /certs/${rootdomain}
sudo openssl req -newkey rsa:4096 -x509 -sha256 -days 365 -nodes -out /certs/${rootdomain}/pubkey.pem -keyout /certs/${rootdomain}/privkey.pem -subj "/C=US/ST=Some-State/L=city/O=Internet Widgits Pty Ltd/CN=*.${rootdomain}"
CERT_PRIV_KEY=/certs/${rootdomain}/privkey.pem
CERT_PUB_KEY=/certs/${rootdomain}/pubkey.pem
fi
sudo chown ${USER}:${USER} -R /etc/letsencrypt
sudo chmod 775 -R /etc/letsencrypt
print_green 'Creating saltapi user'
@@ -180,13 +165,25 @@ sudo apt install -y curl wget
sudo mkdir -p /usr/local/rmmgo
go_tmp=$(mktemp -d -t rmmgo-XXXXXXXXXX)
wget https://golang.org/dl/go1.15.linux-amd64.tar.gz -P ${go_tmp}
wget https://golang.org/dl/go1.15.5.linux-amd64.tar.gz -P ${go_tmp}
tar -xzf ${go_tmp}/go1.15.linux-amd64.tar.gz -C ${go_tmp}
tar -xzf ${go_tmp}/go1.15.5.linux-amd64.tar.gz -C ${go_tmp}
sudo mv ${go_tmp}/go /usr/local/rmmgo/
rm -rf ${go_tmp}
print_green 'Downloading NATS'
nats_tmp=$(mktemp -d -t nats-XXXXXXXXXX)
wget https://github.com/nats-io/nats-server/releases/download/v2.1.9/nats-server-v2.1.9-linux-amd64.tar.gz -P ${nats_tmp}
tar -xzf ${nats_tmp}/nats-server-v2.1.9-linux-amd64.tar.gz -C ${nats_tmp}
sudo mv ${nats_tmp}/nats-server-v2.1.9-linux-amd64/nats-server /usr/local/bin/
sudo chmod +x /usr/local/bin/nats-server
sudo chown ${USER}:${USER} /usr/local/bin/nats-server
rm -rf ${nats_tmp}
print_green 'Installing Nginx'
sudo apt install -y nginx
@@ -283,6 +280,8 @@ sudo mkdir -p /var/log/celery
sudo chown ${USER}:${USER} /var/log/celery
git clone https://github.com/wh1te909/tacticalrmm.git /rmm/
cd /rmm
git config user.email "admin@example.com"
git config user.name "Bob"
git checkout master
localvars="$(cat << EOF
@@ -373,7 +372,7 @@ read -n 1 -s -r -p "Press any key to continue..."
uwsgini="$(cat << EOF
[uwsgi]
logto = /rmm/api/tacticalrmm/tacticalrmm/private/log/uwsgi.log
# logto = /rmm/api/tacticalrmm/tacticalrmm/private/log/uwsgi.log
chdir = /rmm/api/tacticalrmm
module = tacticalrmm.wsgi
home = /rmm/api/env
@@ -414,6 +413,28 @@ EOF
)"
echo "${rmmservice}" | sudo tee /etc/systemd/system/rmm.service > /dev/null
natsservice="$(cat << EOF
[Unit]
Description=NATS Server
After=network.target ntp.service
[Service]
PrivateTmp=true
Type=simple
ExecStart=/usr/local/bin/nats-server -c /rmm/api/tacticalrmm/nats-rmm.conf
ExecReload=/usr/bin/kill -s HUP \$MAINPID
ExecStop=/usr/bin/kill -s SIGINT \$MAINPID
User=${USER}
Group=www-data
Restart=always
RestartSec=5s
[Install]
WantedBy=multi-user.target
EOF
)"
echo "${natsservice}" | sudo tee /etc/systemd/system/nats.service > /dev/null
nginxrmm="$(cat << EOF
server_tokens off;
@@ -422,6 +443,12 @@ upstream tacticalrmm {
server unix:////rmm/api/tacticalrmm/tacticalrmm.sock;
}
map \$http_user_agent \$ignore_ua {
"~python-requests.*" 0;
"~go-resty.*" 0;
default 1;
}
server {
listen 80;
server_name ${rmmdomain};
@@ -820,10 +847,13 @@ node node_modules/meshcentral/meshctrl.js --url wss://${meshdomain}:443 --loginu
sleep 5
MESHEXE=$(node node_modules/meshcentral/meshctrl.js --url wss://${meshdomain}:443 --loginuser ${meshusername} --loginpass ${MESHPASSWD} GenerateInviteLink --group TacticalRMM --hours 8)
sudo systemctl enable nats.service
cd /rmm/api/tacticalrmm
source /rmm/api/env/bin/activate
python manage.py initial_db_setup
python manage.py reload_nats
deactivate
sudo systemctl start nats.service
print_green 'Restarting services'
@@ -852,7 +882,7 @@ if [ "$BEHIND_NAT" = true ]; then
echo -ne "${GREEN}If you will be accessing the web interface of the RMM from the same LAN as this server,${NC}\n"
echo -ne "${GREEN}you'll need to make sure your 3 subdomains resolve to ${IPV4}${NC}\n"
echo -ne "${GREEN}This also applies to any agents that will be on the same local network as the rmm.${NC}\n"
echo -ne "${GREEN}You'll also need to setup port forwarding in your router on ports 80, 443, 4505 and 4506 tcp.${NC}\n\n"
echo -ne "${GREEN}You'll also need to setup port forwarding in your router on ports 80, 443, 4505, 4506 and 4222 tcp.${NC}\n\n"
fi
printf >&2 "${YELLOW}Please refer to the github README for next steps${NC}\n\n"

View File

@@ -7,8 +7,8 @@ pgpw="hunter2"
#####################################################
SCRIPT_VERSION="5"
SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/develop/restore.sh'
SCRIPT_VERSION="6"
SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/restore.sh'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'

View File

@@ -1,7 +1,7 @@
#!/bin/bash
SCRIPT_VERSION="95"
SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/develop/update.sh'
SCRIPT_VERSION="98"
SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/update.sh'
LATEST_SETTINGS_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/api/tacticalrmm/tacticalrmm/settings.py'
YELLOW='\033[1;33m'
GREEN='\033[0;32m'
@@ -13,8 +13,9 @@ curl -s -L "${SCRIPT_URL}" > ${TMP_FILE}
NEW_VER=$(grep "^SCRIPT_VERSION" "$TMP_FILE" | awk -F'[="]' '{print $3}')
if [ "${SCRIPT_VERSION}" -ne "${NEW_VER}" ]; then
printf >&2 "${YELLOW}A newer version of this update script is available.${NC}\n"
printf >&2 "${YELLOW}Please download the latest version from ${GREEN}${SCRIPT_URL}${YELLOW} and re-run.${NC}\n"
printf >&2 "${YELLOW}Old update script detected, downloading and replacing with the latest version...${NC}\n"
wget -q "${SCRIPT_URL}" -O update.sh
printf >&2 "${YELLOW}Script updated! Please re-run ./update.sh${NC}\n"
rm -f $TMP_FILE
exit 1
fi
@@ -55,13 +56,84 @@ LATEST_SALT_VER=$(grep "^SALT_MASTER_VER" "$TMP_SETTINGS" | awk -F'[= "]' '{prin
CURRENT_PIP_VER=$(grep "^PIP_VER" "$SETTINGS_FILE" | awk -F'[= "]' '{print $5}')
CURRENT_NPM_VER=$(grep "^NPM_VER" "$SETTINGS_FILE" | awk -F'[= "]' '{print $5}')
for i in nginx rmm celery celerybeat celery-winupdate
NEEDS_NATS=false
if [ ! -f /etc/systemd/system/nats.service ]; then
if [ ! -d /etc/letsencrypt ]; then
printf >&2 "${RED}ERROR: no letsencrypt cert detected. The RMM now requires a valid TLS certificate${NC}\n"
printf >&2 "${RED}Please send us a message in our discord for instructions on how to proceed, or open a github ticket.${NC}\n"
exit 1
fi
NEEDS_NATS=true
fi
if [ "$NEEDS_NATS" = true ]; then
printf "\033c"
printf >&2 "\n\n"
printf >&2 "${YELLOW}WARNING: BREAKING CHANGES AHEAD${NC}\n\n"
printf >&2 "${YELLOW}In order to continue with this update, please open up port 4222 TCP in ufw${NC}\n\n"
printf >&2 "${YELLOW}If you are running behind NAT, make sure to also setup the necessary port forwards in your router${NC}\n\n"
printf >&2 "${YELLOW}Run the following command to open the port in ufw firewall:\n\n${GREEN}sudo ufw allow proto tcp from any to any port 4222 && sudo ufw reload${NC}\n\n\n"
printf >&2 "${YELLOW}Many of your agent functions will stop working until your agents are updated to at least version 1.1.0${NC}\n"
printf >&2 "${YELLOW}This will happen shortly after this update completes, as long as you have auto agent updated enabled in Global Settings${NC}\n"
printf >&2 "${YELLOW}A background job also runs every hour to auto update agents.\nIf you do not want to wait, you may manually trigger an agent update from the Agents > Update Agents menu in the web ui.${NC}\n\n\n"
until [[ "$CONFIRM_NATS" == "yes" ]]; do
echo -ne "${RED}If you have not opened port 4222 yet, please Ctrl+C to cancel this script, open the port and then re-run${NC}"
printf >&2 "\n\n"
echo -ne "${YELLOW}Confirm you have port 4222 TCP open? [type 'yes' to confirm]${NC}: "
read CONFIRM_NATS
done
printf >&2 "\n"
fi
if [ "$NEEDS_NATS" = true ]; then
printf >&2 "${Green}Downloading nats${NC}\n\n"
nats_tmp=$(mktemp -d -t nats-XXXXXXXXXX)
wget https://github.com/nats-io/nats-server/releases/download/v2.1.9/nats-server-v2.1.9-linux-amd64.tar.gz -P ${nats_tmp}
tar -xzf ${nats_tmp}/nats-server-v2.1.9-linux-amd64.tar.gz -C ${nats_tmp}
sudo mv ${nats_tmp}/nats-server-v2.1.9-linux-amd64/nats-server /usr/local/bin/
sudo chmod +x /usr/local/bin/nats-server
sudo chown ${USER}:${USER} /usr/local/bin/nats-server
rm -rf ${nats_tmp}
natsservice="$(cat << EOF
[Unit]
Description=NATS Server
After=network.target ntp.service
[Service]
PrivateTmp=true
Type=simple
ExecStart=/usr/local/bin/nats-server -c /rmm/api/tacticalrmm/nats-rmm.conf
ExecReload=/usr/bin/kill -s HUP \$MAINPID
ExecStop=/usr/bin/kill -s SIGINT \$MAINPID
User=${USER}
Group=www-data
Restart=always
RestartSec=5s
[Install]
WantedBy=multi-user.target
EOF
)"
echo "${natsservice}" | sudo tee /etc/systemd/system/nats.service > /dev/null
sudo systemctl daemon-reload
sudo systemctl enable nats.service
fi
for i in nginx rmm celery celerybeat celery-winupdate nats
do
printf >&2 "${GREEN}Stopping ${i} service...${NC}\n"
sudo systemctl stop ${i}
done
cd /rmm
git config user.email "admin@example.com"
git config user.name "Bob"
git fetch
git checkout master
git reset --hard FETCH_HEAD
@@ -95,6 +167,8 @@ sudo chown -R $USER:$GROUP /home/${USER}/.npm
sudo chown -R $USER:$GROUP /home/${USER}/.config
sudo chown -R $USER:$GROUP /home/${USER}/.cache
sudo chmod 750 /srv/salt/scripts/userdefined
sudo chown ${USER}:${USER} -R /etc/letsencrypt
sudo chmod 775 -R /etc/letsencrypt
cp /rmm/_modules/* /srv/salt/_modules/
cp /rmm/scripts/* /srv/salt/scripts/
@@ -115,6 +189,7 @@ if [[ "${CURRENT_PIP_VER}" != "${LATEST_PIP_VER}" ]]; then
else
source /rmm/api/env/bin/activate
cd /rmm/api/tacticalrmm
pip install -r requirements.txt
fi
python manage.py pre_update_tasks
@@ -122,10 +197,12 @@ python manage.py migrate
python manage.py delete_tokens
python manage.py fix_salt_key
python manage.py collectstatic --no-input
python manage.py reload_nats
python manage.py post_update_tasks
deactivate
rm -rf /rmm/web/dist
rm -rf /rmm/web/.quasar
cd /rmm/web
if [[ "${CURRENT_NPM_VER}" != "${LATEST_NPM_VER}" ]]; then
rm -rf /rmm/web/node_modules
@@ -137,7 +214,7 @@ sudo rm -rf /var/www/rmm/dist
sudo cp -pr /rmm/web/dist /var/www/rmm/
sudo chown www-data:www-data -R /var/www/rmm/dist
for i in celery celerybeat celery-winupdate rmm nginx
for i in celery celerybeat celery-winupdate rmm nginx nats
do
printf >&2 "${GREEN}Starting ${i} service${NC}\n"
sudo systemctl start ${i}

2143
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,10 +13,10 @@
"axios": "^0.21.0",
"dotenv": "^8.2.0",
"qrcode.vue": "^1.7.0",
"quasar": "^1.14.2"
"quasar": "^1.14.5"
},
"devDependencies": {
"@quasar/app": "^2.1.5",
"@quasar/app": "^2.1.8",
"@quasar/cli": "^1.1.2",
"@quasar/quasar-app-extension-testing": "^1.0.0",
"@quasar/quasar-app-extension-testing-unit-jest": "^1.0.1",

View File

@@ -29,7 +29,7 @@ module.exports = function () {
// Full list of options: https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-build
build: {
env: { DEV_API: process.env.DEV_URL, PROD_API: process.env.PROD_URL },
env: { DEV_API: process.env.DEV_URL, PROD_API: process.env.PROD_URL, DOCKER_BUILD: process.env.DOCKER_BUILD },
vueRouterMode: 'history', // available values: 'hash', 'history'
distDir: "dist/",

View File

@@ -1,17 +1,25 @@
import Vue from 'vue';
import axios from 'axios';
export const getBaseUrl = () => {
if (process.env.NODE_ENV === "production") {
if (process.env.DOCKER_BUILD) {
return window._env_.PROD_URL;
} else {
return process.env.PROD_API;
}
} else {
return process.env.DEV_API;
}
};
export default function ({ router, store }) {
Vue.prototype.$axios = axios;
axios.defaults.baseURL =
process.env.NODE_ENV === "production"
? process.env.PROD_API
: process.env.DEV_API;
axios.interceptors.request.use(
function (config) {
config.baseURL = getBaseUrl()
const token = store.state.token;
if (token != null) {
config.headers.Authorization = `Token ${token}`;

View File

@@ -478,7 +478,7 @@ export default {
axios
.get(`/checks/runchecks/${pk}/`)
.then(r => this.notifySuccess(`Checks will now be re-run on ${r.data}`))
.catch(e => this.notifyError("Something went wrong"));
.catch(e => this.notifyError(e.response.data));
},
removeAgent(pk, name) {
this.$q
@@ -536,7 +536,7 @@ export default {
})
.catch(e => {
this.$q.loading.hide();
this.notifyError("Something went wrong");
this.notifyError(e.response.data);
});
},
rebootNow(pk, hostname) {

View File

@@ -62,6 +62,7 @@ import axios from "axios";
import mixins from "@/mixins/mixins";
import NewDeployment from "@/components/modals/clients/NewDeployment";
import { copyToClipboard } from "quasar";
import { getBaseUrl } from "@/boot/axios";
export default {
name: "Deployment",
@@ -118,7 +119,7 @@ export default {
});
},
copyLink(props) {
const api = axios.defaults.baseURL;
const api = getBaseUrl();
copyToClipboard(`${api}/clients/${props.row.uid}/deploy/`).then(() => {
this.notifySuccess("Link copied to clipboard", 1500);
});

View File

@@ -1,23 +1,9 @@
<template>
<div v-if="!selectedAgentPk">No agent selected</div>
<div v-else class="q-pa-xs">
<q-btn
size="sm"
color="grey-5"
icon="fas fa-plus"
label="Add Note"
text-color="black"
@click="addNote"
/>
<q-btn size="sm" color="grey-5" icon="fas fa-plus" label="Add Note" text-color="black" @click="addNote" />
<q-btn dense flat push @click="refreshNotes" icon="refresh" />
<q-btn
class="q-ml-xl"
size="sm"
color="primary"
icon-right="archive"
label="Export to csv"
@click="exportTable"
/>
<q-btn class="q-ml-xl" size="sm" color="primary" icon-right="archive" label="Export to csv" @click="exportTable" />
<template v-if="notes === undefined || notes.length === 0">
<p>No Notes</p>
</template>
@@ -25,7 +11,7 @@
<q-table
grid
class="tabs-tbl-sticky"
:style="{'max-height': tabsTableHeight}"
:style="{ 'max-height': tabsTableHeight }"
:data="notes"
:columns="columns"
:visible-columns="visibleColumns"
@@ -47,14 +33,7 @@
<q-btn color="grey-7" round flat icon="more_vert">
<q-menu cover auto-close>
<q-list>
<q-btn
color="primary"
push
flat
icon="edit"
label="Edit"
@click="editNote(props.row.id)"
/>
<q-btn color="primary" push flat icon="edit" label="Edit" @click="editNote(props.row.id)" />
<br />
<q-btn
color="negative"

View File

@@ -15,6 +15,25 @@
<template v-slot:top>
<q-btn v-if="poll" dense flat push @click="stopPoll" icon="stop" label="Stop Live Refresh" />
<q-btn v-else dense flat push @click="startPoll" icon="play_arrow" label="Resume Live Refresh" />
<q-space />
<div class="q-pa-md q-gutter-sm">
<q-btn dense push icon="add" color="primary" @click="intervalChanged('add')" />
<q-btn
:disable="pollInterval === 1"
dense
@click="intervalChanged('subtract')"
push
icon="remove"
color="negative"
/>
</div>
<div class="text-overline">
<q-badge align="middle" class="text-h6" color="blue" :label="pollInterval" />
Refresh interval (seconds)
</div>
<q-space />
<q-input v-model="filter" outlined label="Search" dense clearable>
<template v-slot:prepend>
@@ -35,8 +54,10 @@
</q-list>
</q-menu>
<q-td>{{ props.row.name }}</q-td>
<q-td>{{ Math.ceil(props.row.cpu_percent) }}%</q-td>
<q-td>{{ convert(props.row.memory_percent) }} MB</q-td>
<!-- <q-td>{{ Math.ceil(props.row.cpu_percent) }}%</q-td>
<q-td>{{ convert(props.row.memory_percent) }} MB</q-td> -->
<q-td>{{ props.row.cpu_percent }}%</q-td>
<q-td>{{ props.row.memory_percent }}</q-td>
<q-td>{{ props.row.username }}</q-td>
<q-td>{{ props.row.pid }}</q-td>
<q-td>{{ props.row.status }}</q-td>
@@ -47,7 +68,6 @@
</template>
<script>
import axios from "axios";
import mixins from "@/mixins/mixins";
export default {
@@ -57,6 +77,7 @@ export default {
data() {
return {
polling: null,
pollInterval: 2,
mem: null,
poll: true,
procs: [],
@@ -80,6 +101,7 @@ export default {
field: "cpu_percent",
align: "left",
sortable: true,
sort: (a, b, rowA, rowB) => parseInt(b) < parseInt(a),
},
{
name: "memory_percent",
@@ -116,7 +138,7 @@ export default {
getProcesses() {
this.procs = [];
this.$q.loading.show({ message: "Loading Processes..." });
axios
this.$axios
.get(`/agents/${this.pk}/getprocs/`)
.then(r => {
this.procs = r.data;
@@ -124,20 +146,20 @@ export default {
})
.catch(e => {
this.$q.loading.hide();
this.notifyError(e.response.data);
this.notifyError(e.response.data, 4000);
});
},
refreshProcs() {
this.polling = setInterval(() => {
axios
this.$axios
.get(`/agents/${this.pk}/getprocs/`)
.then(r => (this.procs = r.data))
.catch(() => this.notifyError("Unable to contact the agent"));
}, 10000);
.catch(() => console.log("Unable to contact the agent"));
}, this.pollInterval * 1000);
},
killProc(pid, name) {
this.$q.loading.show({ message: `Attempting to kill process ${name}` });
axios
this.$axios
.get(`/agents/${this.pk}/${pid}/killproc/`)
.then(r => {
this.$q.loading.hide();
@@ -152,13 +174,27 @@ export default {
this.poll = false;
clearInterval(this.polling);
},
intervalChanged(action) {
if (action === "subtract" && this.pollInterval <= 1) {
this.stopPoll();
this.startPoll();
return;
}
if (action === "add") {
this.pollInterval++;
} else {
this.pollInterval--;
}
this.stopPoll();
this.startPoll();
},
startPoll() {
this.poll = true;
axios.get(`/agents/${this.pk}/getprocs/`).then(r => (this.procs = r.data));
this.$axios.get(`/agents/${this.pk}/getprocs/`).then(r => (this.procs = r.data));
this.refreshProcs();
},
getAgent() {
axios.get(`/agents/${this.pk}/agentdetail/`).then(r => (this.mem = r.data.total_ram));
this.$axios.get(`/agents/${this.pk}/agentdetail/`).then(r => (this.mem = r.data.total_ram));
},
convert(percent) {
const mb = this.mem * 1024;

View File

@@ -55,7 +55,7 @@
<q-dialog v-model="serviceDetailsModal">
<q-card style="width: 600px; max-width: 80vw">
<q-card-section>
<div class="text-h6">Service Details - {{ serviceData.DisplayName }}</div>
<div class="text-h6">Service Details - {{ serviceData.display_name }}</div>
</q-card-section>
<q-card-section>
@@ -66,20 +66,20 @@
<br />
<div class="row">
<div class="col-3">Display name:</div>
<div class="col-9">{{ serviceData.DisplayName }}</div>
<div class="col-9">{{ serviceData.display_name }}</div>
</div>
<br />
<div class="row">
<div class="col-3">Description:</div>
<div class="col-9">
<q-field outlined :color="$q.dark.isActive ? 'white' : 'black'">{{ serviceData.Description }}</q-field>
<q-field outlined :color="$q.dark.isActive ? 'white' : 'black'">{{ serviceData.description }}</q-field>
</div>
</div>
<br />
<div class="row">
<div class="col-3">Path:</div>
<div class="col-9">
<code>{{ serviceData.BinaryPath }}</code>
<code>{{ serviceData.binpath }}</code>
</div>
</div>
<br />
@@ -102,7 +102,7 @@
<q-card-section>
<div class="row">
<div class="col-3">Service status:</div>
<div class="col-9">{{ serviceData.Status }}</div>
<div class="col-9">{{ serviceData.status }}</div>
</div>
<br />
<div class="row">
@@ -113,7 +113,7 @@
:text-color="$q.dark.isActive ? 'white' : 'black'"
push
label="Start"
@click="serviceAction(serviceData.svc_name, 'start', serviceData.DisplayName)"
@click="serviceAction(serviceData.svc_name, 'start', serviceData.display_name)"
/>
<q-btn
color="gray"
@@ -121,7 +121,7 @@
:text-color="$q.dark.isActive ? 'white' : 'black'"
push
label="Stop"
@click="serviceAction(serviceData.svc_name, 'stop', serviceData.DisplayName)"
@click="serviceAction(serviceData.svc_name, 'stop', serviceData.display_name)"
/>
<q-btn
color="gray"
@@ -129,7 +129,7 @@
:text-color="$q.dark.isActive ? 'white' : 'black'"
push
label="Restart"
@click="serviceAction(serviceData.svc_name, 'restart', serviceData.DisplayName)"
@click="serviceAction(serviceData.svc_name, 'restart', serviceData.display_name)"
/>
</q-btn-group>
</div>
@@ -152,7 +152,6 @@
</template>
<script>
import axios from "axios";
import mixins from "@/mixins/mixins";
export default {
@@ -238,7 +237,7 @@ export default {
edit_action: changed,
};
this.serviceDetailVisible = true;
axios
this.$axios
.post("/services/editservice/", data)
.then(r => {
this.serviceDetailVisible = false;
@@ -258,15 +257,15 @@ export default {
this.saveServiceDetailButton = true;
this.serviceDetailsModal = true;
this.serviceDetailVisible = true;
axios
this.$axios
.get(`/services/${this.pk}/${name}/servicedetail/`)
.then(r => {
this.serviceData = r.data;
this.serviceData.svc_name = name;
this.startupType = this.serviceData.StartType;
if (this.serviceData.StartType === "Auto" && this.serviceData.StartTypeDelayed === true) {
this.startupType = this.serviceData.start_type;
if (this.serviceData.start_type === "Automatic" && this.serviceData.autodelay === true) {
this.startupType = "Automatic (Delayed Start)";
} else if (this.serviceData.StartType === "Auto" && this.serviceData.StartTypeDelayed === false) {
} else if (this.serviceData.start_type === "Automatic" && this.serviceData.autodelay === false) {
this.startupType = "Automatic";
}
this.serviceDetailVisible = false;
@@ -274,7 +273,7 @@ export default {
.catch(e => {
this.serviceDetailVisible = false;
this.serviceDetailsModal = false;
this.notifyError(e.response.data);
this.notifyError(e.response.data, 3000);
});
},
serviceAction(name, action, fullname) {
@@ -301,7 +300,7 @@ export default {
sv_name: name,
sv_action: action,
};
axios
this.$axios
.post("/services/serviceaction/", data)
.then(r => {
this.refreshServices();
@@ -310,20 +309,22 @@ export default {
})
.catch(e => {
this.$q.loading.hide();
this.notifyError(e.response.data, 3000);
});
},
getServices() {
this.$axios
.get(`/services/${this.pk}/services/`)
.then(r => {
this.servicesData = [r.data][0].services;
})
.catch(e => {
this.notifyError(e.response.data);
});
},
async getServices() {
try {
let r = await axios.get(`/services/${this.pk}/services/`);
this.servicesData = [r.data][0].services;
} catch (e) {
console.log(`ERROR!: ${e}`);
}
},
refreshServices() {
this.$q.loading.show({ message: "Reloading services..." });
axios
this.$axios
.get(`/services/${this.pk}/refreshedservices/`)
.then(r => {
this.servicesData = [r.data][0].services;

View File

@@ -2,8 +2,9 @@
<div v-if="!this.selectedAgentPk">No agent selected</div>
<div v-else-if="!Array.isArray(software) || !software.length">No software</div>
<div v-else>
<div class="row q-pt-xs items-start">
<q-btn
size="sm"
size="xs"
color="grey-5"
icon="fas fa-plus"
label="Install Software"
@@ -11,6 +12,14 @@
@click="showInstallSoftware = true"
/>
<q-btn dense flat push @click="refreshSoftware" icon="refresh" />
<q-space />
<q-input v-model="filter" outlined label="Search" dense clearable>
<template v-slot:prepend>
<q-icon name="search" color="primary" />
</template>
</q-input>
</div>
<q-table
:table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }"
class="tabs-tbl-sticky"
@@ -18,6 +27,7 @@
dense
:data="software"
:columns="columns"
:filter="filter"
:pagination.sync="pagination"
binary-state-sort
hide-bottom
@@ -51,6 +61,7 @@ export default {
return {
showInstallSoftware: false,
loading: false,
filter: "",
pagination: {
rowsPerPage: 0,
sortBy: "name",
@@ -64,6 +75,27 @@ export default {
field: "name",
sortable: true,
},
{
name: "publisher",
align: "left",
label: "Publisher",
field: "publisher",
sortable: true,
},
{
name: "install_date",
align: "left",
label: "Installed On",
field: "install_date",
sortable: false,
},
{
name: "size",
align: "left",
label: "Size",
field: "size",
sortable: false,
},
{
name: "version",
align: "left",

View File

@@ -1,5 +1,5 @@
<template>
<q-card style="min-width: 50vw;">
<q-card style="min-width: 50vw">
<q-card-section class="row items-center">
<div class="text-h6">{{ hostname }} Agent Recovery</div>
<q-space />
@@ -10,20 +10,32 @@
<q-card-section>
<div class="q-gutter-sm">
<q-radio dense v-model="mode" val="mesh" label="Mesh Agent" />
<q-radio dense v-model="mode" val="rpc" label="Tactical RPC" />
<q-radio dense v-model="mode" val="tacagent" label="Tactical Agent" />
<q-radio dense v-model="mode" val="checkrunner" label="Tactical Checkrunner" />
<q-radio dense v-model="mode" val="salt" label="Salt Minion" />
<q-radio dense v-model="mode" val="command" label="Shell Command" />
</div>
</q-card-section>
<q-card-section v-show="mode === 'mesh'">
<p>Fix issues with the Mesh Agent which handles take control, terminal and file browser.</p>
<p>Fix issues with the Mesh Agent which handles take control, live terminal and file browser.</p>
</q-card-section>
<q-card-section v-show="mode === 'tacagent'">
<p>Fix issues with the TacticalAgent windows service which handles agent check-in and os info.</p>
</q-card-section>
<q-card-section v-show="mode === 'checkrunner'">
<p>Fix issues with the Tactical Checkrunner windows service which handles running all checks.</p>
</q-card-section>
<q-card-section v-show="mode === 'salt'">
<p>Fix issues with the salt-minion (do this if getting alot of errors about not being able to contact the agent even if it's online).</p>
<p>Fix issues with the salt-minion which handles windows updates, chocolatey and scheduled tasks.</p>
</q-card-section>
<q-card-section v-show="mode === 'rpc'">
<p>Fix issues with the Tactical RPC service which handles most of the agent's realtime functions.</p>
</q-card-section>
<q-card-section v-show="mode === 'command'">
<p>Run a shell command on the agent.</p>
<p>You should use the 'Send Command' feature from the agent's context menu for sending shell commands.</p>
<p>Only use this as a last resort if unable to recover the salt-minion.</p>
<p>Only use this as a last resort if unable to recover the Tactical RPC service.</p>
<q-input
ref="input"
v-model="cmd"
@@ -72,6 +84,7 @@ export default {
},
methods: {
recover() {
this.$q.loading.show();
const data = {
pk: this.pk,
cmd: this.cmd,
@@ -80,10 +93,12 @@ export default {
this.$axios
.post("/agents/recover/", data)
.then(r => {
this.$q.loading.hide();
this.$emit("close");
this.notifySuccess(r.data, 5000);
})
.catch(e => {
this.$q.loading.hide();
this.notifyError(e.response.data, 5000);
});
},

View File

@@ -81,6 +81,7 @@
import axios from "axios";
import mixins from "@/mixins/mixins";
import AgentDownload from "@/components/modals/agents/AgentDownload";
import { getBaseUrl } from "@/boot/axios";
export default {
name: "InstallAgent",
@@ -119,7 +120,7 @@ export default {
});
},
addAgent() {
const api = axios.defaults.baseURL;
const api = getBaseUrl();
const clientStripped = this.client.label
.replace(/\s/g, "")
.toLowerCase()

View File

@@ -23,11 +23,16 @@
<br />
<hr />
<q-checkbox v-model="selectAll" label="Select All" @input="selectAllAction" />
<q-btn v-show="group.length !== 0" label="Update" color="primary" @click="update" class="q-ml-xl" />
<hr />
<q-option-group v-model="group" :options="agentOptions" color="green" type="checkbox" />
</q-card-section>
<q-card-section>
<q-btn v-show="group.length !== 0" label="Update" color="primary" @click="update" />
<q-option-group
v-model="group"
:options="agentOptions"
color="green"
type="checkbox"
style="max-height: 60vh; max-width: 40vw"
class="scroll"
/>
</q-card-section>
</q-card>
</template>

View File

@@ -61,6 +61,9 @@
<q-td v-else-if="props.row.action_type === 'taskaction'">
<q-icon name="fas fa-tasks" size="sm" />
</q-td>
<q-td v-else-if="props.row.action_type === 'agentupdate'">
<q-icon name="update" size="sm" />
</q-td>
<q-td>{{ props.row.due }}</q-td>
<q-td>{{ props.row.description }}</q-td>
<q-td v-show="!!!agentpk">{{ props.row.hostname }}</q-td>

View File

@@ -75,13 +75,19 @@ export default {
restart() {
this.visible = false;
this.$q.loading.show({ message: "Restarting Mesh Agent" });
const data = {
pk: this.$route.params.pk,
sv_name: "mesh agent",
sv_action: "restart",
};
this.$axios
.get(`/agents/${this.$route.params.pk}/restartmesh/`)
.post("/services/serviceaction/", data)
.then(r => {
setTimeout(() => {
this.visible = true;
this.$q.loading.hide();
this.notifySuccess(r.data);
this.notifySuccess("Mesh agent service was restarted");
}, 500);
})
.catch(e => {