Compare commits

..

73 Commits

Author SHA1 Message Date
wh1te909
1432853b39 Release 0.4.32 2021-03-31 18:35:05 +00:00
wh1te909
77b1d964b5 bump versions 2021-03-31 18:33:43 +00:00
wh1te909
549936fc09 add logging and timeout to deployment gen 2021-03-31 18:24:27 +00:00
wh1te909
c9c32f09c5 public docs on push to master instead of develop 2021-03-31 18:11:14 +00:00
sadnub
77f7778d4a fix being ablwe to add/edit automation and alert templates on sites and clients 2021-03-31 12:03:26 -04:00
wh1te909
84b6be9364 un-hide custom fields 2021-03-31 07:29:28 +00:00
wh1te909
1e43b55804 Release 0.4.31 2021-03-31 07:20:46 +00:00
wh1te909
ba9bdaae0a bump versions 2021-03-31 07:09:20 +00:00
wh1te909
7dfd7bde8e fix update 2021-03-31 07:02:35 +00:00
Dan
5e6c4161d0 Merge pull request #355 from silversword411/develop
Scripts choco update
2021-03-30 23:26:17 -07:00
wh1te909
d75d56dfc9 hide customfields in ui for now 2021-03-31 06:22:53 +00:00
silversword411
1d9d350091 Merge branch 'develop' of https://github.com/silversword411/tacticalrmm into develop 2021-03-31 02:19:57 -04:00
silversword411
5744053c6f Scripts choco update 2021-03-31 02:14:18 -04:00
wh1te909
65589b6ca2 clients manager table ui fixes 2021-03-31 06:13:30 +00:00
silversword411
e03a9d1137 Scripts choco update 2021-03-31 00:19:44 -04:00
Dan
29f80f2276 Merge pull request #354 from silversword411/develop
2 script updates, one removal
2021-03-30 21:01:22 -07:00
silversword411
a9b74aa69b Commit the file renames 2021-03-30 23:43:18 -04:00
silversword411
63ebfd3210 2 script updates, one removal 2021-03-30 23:20:58 -04:00
wh1te909
87fa5ff7a6 feat: add default timeout in script manager closes #352 2021-03-31 03:01:46 +00:00
sadnub
b686b53a9c Update dockerfile 2021-03-30 17:11:06 -04:00
wh1te909
258261dc64 refactor goinstaller to prepare for code signing 2021-03-30 20:52:03 +00:00
sadnub
9af5c9ead9 remove console.log entries 2021-03-29 18:34:40 -04:00
wh1te909
382654188c update install docs 2021-03-29 22:02:30 +00:00
Dan
fa1df082b7 Merge pull request #345 from silversword411/develop
Updating scripts
2021-03-29 14:12:00 -07:00
sadnub
5c227d8f80 formatting 2021-03-29 15:31:29 -04:00
sadnub
81dabdbfb7 fix tests 2021-03-29 15:27:08 -04:00
sadnub
91f89f5a33 custom fields finish 2021-03-29 15:14:20 -04:00
silversword411
9f92746aa0 Adding Install All Updates and extra categories 2021-03-29 09:22:51 -04:00
wh1te909
5d6e6f9441 fix custom fields 2021-03-29 11:14:04 +00:00
wh1te909
01395a2726 isort 2021-03-29 10:23:54 +00:00
sadnub
465d75c65d formatting 2021-03-28 18:22:04 -04:00
sadnub
4634f8927e add tests for clients changes and custom fields 2021-03-28 18:17:35 -04:00
sadnub
74a287f9fe update containers to node 14 and reconfigure nats-api 2021-03-28 09:23:06 -04:00
wh1te909
7ff6c79835 remove natsapi from normal install 2021-03-27 20:00:47 +00:00
wh1te909
3629982237 refactor natsapi 2021-03-27 19:21:52 +00:00
silversword411
ddb610f1bc Bitlocker script update 2021-03-27 00:47:11 -04:00
silversword411
f899905d27 Updating script to std format: AD 2021-03-27 00:41:51 -04:00
silversword411
3e4531b5c5 Merge remote-tracking branch 'upstream/develop' into develop 2021-03-27 00:35:13 -04:00
wh1te909
a9e189e51d fix edit client, more tests 2021-03-27 00:25:45 -04:00
Dan
58ba08a8f3 Merge pull request #342 from silversword411/develop
Updating labels from (s) to (seconds)
2021-03-26 20:59:09 -07:00
silversword411
9078ff27d8 Updating (s) in labels to (seconds) 2021-03-26 22:48:13 -04:00
silversword411
6f43e61c24 Disk label v2 2021-03-26 18:48:40 -04:00
silversword411
4be0d3f212 Updating Disk check label 2021-03-26 18:39:03 -04:00
wh1te909
00e47e5a27 fix edit client, more tests 2021-03-26 22:25:13 +00:00
Dan
152e145b32 Merge pull request #341 from silversword411/develop
Script Update to standardized format
2021-03-26 14:45:37 -07:00
wh1te909
54e55e8f57 update drf 2021-03-26 21:44:05 +00:00
wh1te909
05b8707f9e black 2021-03-26 21:23:34 +00:00
wh1te909
543e952023 start fixing tests 2021-03-26 21:20:39 +00:00
silversword411
6e5f40ea06 Update community_scripts.json 2021-03-26 10:29:58 -04:00
silversword411
bbafb0be87 bios script update 2021-03-26 10:23:46 -04:00
silversword411
1c9c5232fe Rename bios_check.ps1 to Win_Bios_Check.ps1 2021-03-26 10:17:44 -04:00
wh1te909
598d79a502 fix error msg 2021-03-26 07:48:35 +00:00
wh1te909
37d8360b77 add creation date to deployment closes #340 2021-03-26 06:58:36 +00:00
wh1te909
82d9ca3317 go 1.16.2 2021-03-26 06:48:58 +00:00
wh1te909
4e4238d486 update to nodejs v14 2021-03-26 06:32:24 +00:00
wh1te909
c77dbe44dc remove old salt check 2021-03-26 06:03:04 +00:00
wh1te909
e03737f15f drop upgrade support for trmm < 0.3.0 2021-03-26 05:51:23 +00:00
Dan
a02629bcd7 Merge pull request #337 from sadnub/develop
clients and sites rework and custom fields
2021-03-25 22:24:43 -07:00
sadnub
6c3fc23d78 fix adding clients/sites/agents with custom fields 2021-03-25 23:21:57 -04:00
sadnub
0fe40f9ccb add custom fields to forms and get saving to work 2021-03-25 23:21:57 -04:00
sadnub
9bd7c8edd1 clients and sites rework and custom fields 2021-03-25 23:21:57 -04:00
wh1te909
83ba480863 Merge branch 'master' of https://github.com/wh1te909/tacticalrmm 2021-03-25 23:14:38 +00:00
wh1te909
f158ea25e9 Release 0.4.30 2021-03-25 23:14:16 +00:00
wh1te909
0227519eab bump versions 2021-03-25 23:13:41 +00:00
wh1te909
616a9685fa update reqs 2021-03-25 22:15:58 +00:00
wh1te909
fe61b01320 fix celery async errors 2021-03-24 22:13:02 +00:00
wh1te909
7b25144311 add docs for django admin 2021-03-24 07:12:26 +00:00
sadnub
9d42fbbdd7 exclude mesh agent and debug logs 2021-03-23 10:41:15 -04:00
sadnub
39ac5b088b Update entrypoint.sh 2021-03-23 10:41:04 -04:00
sadnub
c14ffd08a0 exclude mesh agent and debug logs 2021-03-23 09:04:26 -04:00
sadnub
6e1239340b Update entrypoint.sh 2021-03-23 08:56:43 -04:00
wh1te909
a297dc8b3b re-run update.sh when old version detected 2021-03-23 07:39:06 +00:00
wh1te909
8d4ecc0898 update reqs 2021-03-23 07:10:45 +00:00
129 changed files with 3461 additions and 1858 deletions

View File

@@ -1,7 +1,6 @@
FROM python:3.9.2-slim
ENV TACTICAL_DIR /opt/tactical
ENV TACTICAL_GO_DIR /usr/local/rmmgo
ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready
ENV WORKSPACE_DIR /workspace
ENV TACTICAL_USER tactical
@@ -14,9 +13,6 @@ EXPOSE 8000
RUN groupadd -g 1000 tactical && \
useradd -u 1000 -g 1000 tactical
# Copy Go Files
COPY --from=golang:1.16 /usr/local/go ${TACTICAL_GO_DIR}/go
# Copy Dev python reqs
COPY ./requirements.txt /

View File

@@ -21,9 +21,9 @@ services:
- tactical-backend
app-dev:
image: node:12-alpine
image: node:14-alpine
restart: always
command: /bin/sh -c "npm install && npm run serve -- --host 0.0.0.0 --port ${APP_PORT}"
command: /bin/sh -c "npm install npm@latest -g && npm install && npm run serve -- --host 0.0.0.0 --port ${APP_PORT}"
working_dir: /workspace/web
volumes:
- ..:/workspace:cached

View File

@@ -150,9 +150,6 @@ EOF
fi
if [ "$1" = 'tactical-api' ]; then
cp "${WORKSPACE_DIR}"/api/tacticalrmm/core/goinstaller/bin/goversioninfo /usr/local/bin/goversioninfo
chmod +x /usr/local/bin/goversioninfo
check_tactical_ready
"${VIRTUAL_ENV}"/bin/python manage.py runserver 0.0.0.0:"${API_PORT}"
fi

View File

@@ -2,7 +2,7 @@ name: Deploy Docs
on:
push:
branches:
- develop
- master
defaults:
run:

View File

@@ -1,7 +1,8 @@
from django.contrib import admin
from .models import Agent, Note, RecoveryAction
from .models import Agent, AgentCustomField, Note, RecoveryAction
admin.site.register(Agent)
admin.site.register(RecoveryAction)
admin.site.register(Note)
admin.site.register(AgentCustomField)

View File

@@ -0,0 +1,24 @@
# Generated by Django 3.1.7 on 2021-03-17 14:45
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0014_customfield'),
('agents', '0031_agent_alert_template'),
]
operations = [
migrations.CreateModel(
name='AgentCustomField',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('value', models.TextField(blank=True, null=True)),
('agent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='custom_fields', to='agents.agent')),
('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='agent_fields', to='core.customfield')),
],
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 3.1.7 on 2021-03-29 02:51
import django.contrib.postgres.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agents', '0032_agentcustomfield'),
]
operations = [
migrations.AddField(
model_name='agentcustomfield',
name='multiple_value',
field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(blank=True, null=True), blank=True, default=list, null=True, size=None),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.7 on 2021-03-29 03:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agents', '0033_agentcustomfield_multiple_value'),
]
operations = [
migrations.AddField(
model_name='agentcustomfield',
name='checkbox_value',
field=models.BooleanField(blank=True, default=False),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.1.7 on 2021-03-29 17:09
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('agents', '0034_agentcustomfield_checkbox_value'),
]
operations = [
migrations.RenameField(
model_name='agentcustomfield',
old_name='checkbox_value',
new_name='bool_value',
),
migrations.RenameField(
model_name='agentcustomfield',
old_name='value',
new_name='string_value',
),
]

View File

@@ -4,7 +4,8 @@ import re
import time
from collections import Counter
from distutils.version import LooseVersion
from typing import Any, Union
from typing import Any
from django.contrib.postgres.fields import ArrayField
import msgpack
import validators
@@ -18,7 +19,6 @@ from django.utils import timezone as djangotime
from loguru import logger
from nats.aio.client import Client as NATS
from nats.aio.errors import ErrTimeout
from packaging import version as pyver
from core.models import TZ_CHOICES, CoreSettings
from logs.models import BaseAuditModel
@@ -837,3 +837,38 @@ class Note(models.Model):
def __str__(self):
return self.agent.hostname
class AgentCustomField(models.Model):
agent = models.ForeignKey(
Agent,
related_name="custom_fields",
on_delete=models.CASCADE,
)
field = models.ForeignKey(
"core.CustomField",
related_name="agent_fields",
on_delete=models.CASCADE,
)
string_value = models.TextField(null=True, blank=True)
bool_value = models.BooleanField(blank=True, default=False)
multiple_value = ArrayField(
models.TextField(null=True, blank=True),
null=True,
blank=True,
default=list,
)
def __str__(self):
return self.field
@property
def value(self):
if self.field.type == "multiple":
return self.multiple_value
elif self.field.type == "checkbox":
return self.bool_value
else:
return self.string_value

View File

@@ -4,7 +4,7 @@ from rest_framework import serializers
from clients.serializers import ClientSerializer
from winupdate.serializers import WinUpdatePolicySerializer
from .models import Agent, Note
from .models import Agent, AgentCustomField, Note
class AgentSerializer(serializers.ModelSerializer):
@@ -119,10 +119,30 @@ class AgentTableSerializer(serializers.ModelSerializer):
depth = 2
class AgentCustomFieldSerializer(serializers.ModelSerializer):
class Meta:
model = AgentCustomField
fields = (
"id",
"field",
"agent",
"value",
"string_value",
"bool_value",
"multiple_value",
)
extra_kwargs = {
"string_value": {"write_only": True},
"bool_value": {"write_only": True},
"multiple_value": {"write_only": True},
}
class AgentEditSerializer(serializers.ModelSerializer):
winupdatepolicy = WinUpdatePolicySerializer(many=True, read_only=True)
all_timezones = serializers.SerializerMethodField()
client = ClientSerializer(read_only=True)
custom_fields = AgentCustomFieldSerializer(many=True, read_only=True)
def get_all_timezones(self, obj):
return pytz.all_timezones
@@ -146,6 +166,7 @@ class AgentEditSerializer(serializers.ModelSerializer):
"all_timezones",
"winupdatepolicy",
"policy",
"custom_fields",
]

View File

@@ -1,6 +1,9 @@
import asyncio
import datetime as dt
import json
import random
import subprocess
import tempfile
from time import sleep
from typing import Union
@@ -252,3 +255,48 @@ def run_script_email_results_task(
server.quit()
except Exception as e:
logger.error(e)
def _get_nats_config() -> dict:
return {
"key": settings.SECRET_KEY,
"natsurl": f"tls://{settings.ALLOWED_HOSTS[0]}:4222",
}
@app.task
def monitor_agents_task() -> None:
agents = Agent.objects.only(
"pk", "agent_id", "last_seen", "overdue_time", "offline_time"
)
ret = [i.agent_id for i in agents if i.status != "online"]
config = _get_nats_config()
config["agents"] = ret
with tempfile.NamedTemporaryFile() as fp:
with open(fp.name, "w") as f:
json.dump(config, f)
cmd = ["/usr/local/bin/nats-api", "-c", fp.name, "-m", "monitor"]
try:
subprocess.run(cmd, capture_output=True, timeout=30)
except Exception as e:
logger.error(e)
@app.task
def get_wmi_task() -> None:
agents = Agent.objects.only(
"pk", "agent_id", "last_seen", "overdue_time", "offline_time"
)
ret = [i.agent_id for i in agents if i.status == "online"]
config = _get_nats_config()
config["agents"] = ret
with tempfile.NamedTemporaryFile() as fp:
with open(fp.name, "w") as f:
json.dump(config, f)
cmd = ["/usr/local/bin/nats-api", "-c", fp.name, "-m", "wmi"]
try:
subprocess.run(cmd, capture_output=True, timeout=30)
except Exception as e:
logger.error(e)

View File

@@ -12,7 +12,7 @@ from tacticalrmm.test import TacticalTestCase
from winupdate.models import WinUpdatePolicy
from winupdate.serializers import WinUpdatePolicySerializer
from .models import Agent
from .models import Agent, AgentCustomField
from .serializers import AgentSerializer
from .tasks import auto_self_agent_update_task
@@ -363,8 +363,7 @@ class TestAgentViews(TacticalTestCase):
self.check_not_authenticated("patch", url)
@patch("os.path.exists")
@patch("subprocess.run")
def test_install_agent(self, mock_subprocess, mock_file_exists):
def test_install_agent(self, mock_file_exists):
url = f"/agents/installagent/"
site = baker.make("clients.Site")
@@ -382,29 +381,20 @@ class TestAgentViews(TacticalTestCase):
}
mock_file_exists.return_value = False
mock_subprocess.return_value.returncode = 0
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 406)
mock_file_exists.return_value = True
mock_subprocess.return_value.returncode = 1
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 413)
mock_file_exists.return_value = True
mock_subprocess.return_value.returncode = 0
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 200)
data["arch"] = "32"
mock_subprocess.return_value.returncode = 0
mock_file_exists.return_value = False
r = self.client.post(url, data, format="json")
self.assertEqual(r.status_code, 415)
data["installMethod"] = "manual"
data["arch"] = "64"
mock_subprocess.return_value.returncode = 0
mock_file_exists.return_value = True
r = self.client.post(url, data, format="json")
self.assertIn("rdp", r.json()["cmd"])
@@ -534,6 +524,35 @@ class TestAgentViews(TacticalTestCase):
data = WinUpdatePolicySerializer(policy).data
self.assertEqual(data["run_time_days"], [2, 3, 6])
# test adding custom fields
field = baker.make("core.CustomField", model="agent", type="number")
edit = {
"id": self.agent.pk,
"site": site.id, # type: ignore
"description": "asjdk234andasd",
"custom_fields": [{"field": field.id, "string_value": "123"}], # type: ignore
}
r = self.client.patch(url, edit, format="json")
self.assertEqual(r.status_code, 200)
self.assertTrue(
AgentCustomField.objects.filter(agent=self.agent, field=field).exists()
)
# test edit custom field
edit = {
"id": self.agent.pk,
"site": site.id, # type: ignore
"description": "asjdk234andasd",
"custom_fields": [{"field": field.id, "string_value": "456"}], # type: ignore
}
r = self.client.patch(url, edit, format="json")
self.assertEqual(r.status_code, 200)
self.assertEqual(
AgentCustomField.objects.get(agent=agent, field=field).value,
"456",
)
self.check_not_authenticated("patch", url)
@patch("agents.models.Agent.get_login_token")

View File

@@ -19,7 +19,6 @@ from logs.models import AuditLog, PendingAction
from scripts.models import Script
from scripts.tasks import handle_bulk_command_task, handle_bulk_script_task
from tacticalrmm.utils import (
generate_installer_exe,
get_default_timezone,
notify_error,
reload_nats,
@@ -27,8 +26,9 @@ from tacticalrmm.utils import (
from winupdate.serializers import WinUpdatePolicySerializer
from winupdate.tasks import bulk_check_for_updates_task, bulk_install_updates_task
from .models import Agent, Note, RecoveryAction
from .models import Agent, AgentCustomField, Note, RecoveryAction
from .serializers import (
AgentCustomFieldSerializer,
AgentEditSerializer,
AgentHostnameSerializer,
AgentOverdueActionSerializer,
@@ -87,7 +87,7 @@ def uninstall(request):
return Response(f"{name} will now be uninstalled.")
@api_view(["PATCH"])
@api_view(["PATCH", "PUT"])
def edit_agent(request):
agent = get_object_or_404(Agent, pk=request.data["id"])
@@ -103,6 +103,29 @@ def edit_agent(request):
p_serializer.is_valid(raise_exception=True)
p_serializer.save()
if "custom_fields" in request.data.keys():
for field in request.data["custom_fields"]:
custom_field = field
custom_field["agent"] = agent.id # type: ignore
if AgentCustomField.objects.filter(
field=field["field"], agent=agent.id # type: ignore
):
value = AgentCustomField.objects.get(
field=field["field"], agent=agent.id # type: ignore
)
serializer = AgentCustomFieldSerializer(
instance=value, data=custom_field
)
serializer.is_valid(raise_exception=True)
serializer.save()
else:
serializer = AgentCustomFieldSerializer(data=custom_field)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("ok")
@@ -357,26 +380,21 @@ def install_agent(request):
f"winagent-v{version}.exe" if arch == "64" else f"winagent-v{version}-x86.exe"
)
download_url = settings.DL_64 if arch == "64" else settings.DL_32
goarch = "amd64" if arch == "64" else "386"
_, token = AuthToken.objects.create(
user=request.user, expiry=dt.timedelta(hours=request.data["expires"])
)
if request.data["installMethod"] == "exe":
return generate_installer_exe(
file_name="rmm-installer.exe",
goarch="amd64" if arch == "64" else "386",
inno=inno,
api=request.data["api"],
client_id=client_id,
site_id=site_id,
atype=request.data["agenttype"],
rdp=request.data["rdp"],
ping=request.data["ping"],
power=request.data["power"],
download_url=download_url,
token=token,
)
ret = {
"token": token,
"url": download_url,
"inno": inno,
"goarch": goarch,
"genurl": settings.EXE_GEN_URL,
}
return Response(ret)
elif request.data["installMethod"] == "manual":
cmd = [

View File

@@ -1,7 +1,9 @@
from django.contrib import admin
from .models import Client, Deployment, Site
from .models import Client, ClientCustomField, Deployment, Site, SiteCustomField
admin.site.register(Client)
admin.site.register(Site)
admin.site.register(Deployment)
admin.site.register(ClientCustomField)
admin.site.register(SiteCustomField)

View File

@@ -0,0 +1,33 @@
# Generated by Django 3.1.7 on 2021-03-17 14:45
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0014_customfield'),
('clients', '0009_auto_20210212_1408'),
]
operations = [
migrations.CreateModel(
name='SiteCustomField',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('value', models.TextField(blank=True, null=True)),
('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='site_fields', to='core.customfield')),
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='custom_fields', to='clients.site')),
],
),
migrations.CreateModel(
name='ClientCustomField',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('value', models.TextField(blank=True, null=True)),
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='custom_fields', to='clients.client')),
('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='client_fields', to='core.customfield')),
],
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 3.1.7 on 2021-03-21 15:11
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('clients', '0010_clientcustomfield_sitecustomfield'),
]
operations = [
migrations.AlterUniqueTogether(
name='site',
unique_together={('client', 'name')},
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.7 on 2021-03-26 06:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('clients', '0011_auto_20210321_1511'),
]
operations = [
migrations.AddField(
model_name='deployment',
name='created',
field=models.DateTimeField(auto_now_add=True, null=True),
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 3.1.7 on 2021-03-29 02:51
import django.contrib.postgres.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('clients', '0012_deployment_created'),
]
operations = [
migrations.AddField(
model_name='clientcustomfield',
name='multiple_value',
field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(blank=True, null=True), blank=True, default=list, null=True, size=None),
),
migrations.AddField(
model_name='sitecustomfield',
name='multiple_value',
field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(blank=True, null=True), blank=True, default=list, null=True, size=None),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.1.7 on 2021-03-29 03:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('clients', '0013_auto_20210329_0251'),
]
operations = [
migrations.AddField(
model_name='clientcustomfield',
name='checkbox_value',
field=models.BooleanField(blank=True, default=False),
),
migrations.AddField(
model_name='sitecustomfield',
name='checkbox_value',
field=models.BooleanField(blank=True, default=False),
),
]

View File

@@ -0,0 +1,27 @@
# Generated by Django 3.1.7 on 2021-03-29 17:09
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('clients', '0014_auto_20210329_0301'),
]
operations = [
migrations.RenameField(
model_name='clientcustomfield',
old_name='checkbox_value',
new_name='bool_value',
),
migrations.RenameField(
model_name='clientcustomfield',
old_name='value',
new_name='string_value',
),
migrations.RemoveField(
model_name='sitecustomfield',
name='value',
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.1.7 on 2021-03-29 18:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('clients', '0015_auto_20210329_1709'),
]
operations = [
migrations.RenameField(
model_name='sitecustomfield',
old_name='checkbox_value',
new_name='bool_value',
),
migrations.AddField(
model_name='sitecustomfield',
name='string_value',
field=models.TextField(blank=True, null=True),
),
]

View File

@@ -1,6 +1,7 @@
import uuid
from django.db import models
from django.contrib.postgres.fields import ArrayField
from agents.models import Agent
from logs.models import BaseAuditModel
@@ -159,6 +160,7 @@ class Site(BaseAuditModel):
class Meta:
ordering = ("name",)
unique_together = (("client", "name"),)
def __str__(self):
return self.name
@@ -225,6 +227,7 @@ class Deployment(models.Model):
)
arch = models.CharField(max_length=255, choices=ARCH_CHOICES, default="64")
expiry = models.DateTimeField(null=True, blank=True)
created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
auth_token = models.ForeignKey(
"knox.AuthToken", related_name="deploytokens", on_delete=models.CASCADE
)
@@ -233,3 +236,73 @@ class Deployment(models.Model):
def __str__(self):
return f"{self.client} - {self.site} - {self.mon_type}"
class ClientCustomField(models.Model):
client = models.ForeignKey(
Client,
related_name="custom_fields",
on_delete=models.CASCADE,
)
field = models.ForeignKey(
"core.CustomField",
related_name="client_fields",
on_delete=models.CASCADE,
)
string_value = models.TextField(null=True, blank=True)
bool_value = models.BooleanField(blank=True, default=False)
multiple_value = ArrayField(
models.TextField(null=True, blank=True),
null=True,
blank=True,
default=list,
)
def __str__(self):
return self.field.name
@property
def value(self):
if self.field.type == "multiple":
return self.multiple_value
elif self.field.type == "checkbox":
return self.bool_value
else:
return self.string_value
class SiteCustomField(models.Model):
site = models.ForeignKey(
Site,
related_name="custom_fields",
on_delete=models.CASCADE,
)
field = models.ForeignKey(
"core.CustomField",
related_name="site_fields",
on_delete=models.CASCADE,
)
string_value = models.TextField(null=True, blank=True)
bool_value = models.BooleanField(blank=True, default=False)
multiple_value = ArrayField(
models.TextField(null=True, blank=True),
null=True,
blank=True,
default=list,
)
def __str__(self):
return self.field.name
@property
def value(self):
if self.field.type == "multiple":
return self.multiple_value
elif self.field.type == "checkbox":
return self.bool_value
else:
return self.string_value

View File

@@ -1,42 +1,87 @@
from rest_framework.serializers import ModelSerializer, ReadOnlyField, ValidationError
from .models import Client, Deployment, Site
from .models import Client, ClientCustomField, Deployment, Site, SiteCustomField
class SiteCustomFieldSerializer(ModelSerializer):
class Meta:
model = SiteCustomField
fields = (
"id",
"field",
"site",
"value",
"string_value",
"bool_value",
"multiple_value",
)
extra_kwargs = {
"string_value": {"write_only": True},
"bool_value": {"write_only": True},
"multiple_value": {"write_only": True},
}
class SiteSerializer(ModelSerializer):
client_name = ReadOnlyField(source="client.name")
custom_fields = SiteCustomFieldSerializer(many=True, read_only=True)
class Meta:
model = Site
fields = "__all__"
fields = (
"id",
"name",
"server_policy",
"workstation_policy",
"alert_template",
"client_name",
"client",
"custom_fields",
)
def validate(self, val):
if "name" in val.keys() and "|" in val["name"]:
raise ValidationError("Site name cannot contain the | character")
if self.context:
client = Client.objects.get(pk=self.context["clientpk"])
if Site.objects.filter(client=client, name=val["name"]).exists():
raise ValidationError(f"Site {val['name']} already exists")
return val
class ClientCustomFieldSerializer(ModelSerializer):
class Meta:
model = ClientCustomField
fields = (
"id",
"field",
"client",
"value",
"string_value",
"bool_value",
"multiple_value",
)
extra_kwargs = {
"string_value": {"write_only": True},
"bool_value": {"write_only": True},
"multiple_value": {"write_only": True},
}
class ClientSerializer(ModelSerializer):
sites = SiteSerializer(many=True, read_only=True)
custom_fields = ClientCustomFieldSerializer(many=True, read_only=True)
class Meta:
model = Client
fields = "__all__"
fields = (
"id",
"name",
"server_policy",
"workstation_policy",
"alert_template",
"sites",
"custom_fields",
)
def validate(self, val):
if "site" in self.context:
if "|" in self.context["site"]:
raise ValidationError("Site name cannot contain the | character")
if len(self.context["site"]) > 255:
raise ValidationError("Site name too long")
if "name" in val.keys() and "|" in val["name"]:
raise ValidationError("Client name cannot contain the | character")
@@ -83,4 +128,5 @@ class DeploymentSerializer(ModelSerializer):
"arch",
"expiry",
"install_flags",
"created",
]

View File

@@ -1,11 +1,12 @@
import uuid
from unittest.mock import patch
from model_bakery import baker
from rest_framework.serializers import ValidationError
from tacticalrmm.test import TacticalTestCase
from .models import Client, Deployment, Site
from .models import Client, ClientCustomField, Deployment, Site, SiteCustomField
from .serializers import (
ClientSerializer,
ClientTreeSerializer,
@@ -28,18 +29,29 @@ class TestClientViews(TacticalTestCase):
r = self.client.get(url, format="json")
serializer = ClientSerializer(clients, many=True)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data, serializer.data)
self.assertEqual(r.data, serializer.data) # type: ignore
self.check_not_authenticated("get", url)
def test_add_client(self):
url = "/clients/clients/"
payload = {"client": "Company 1", "site": "Site 1"}
# test successfull add client
payload = {
"client": {"name": "Client1"},
"site": {"name": "Site1"},
"custom_fields": [],
}
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 200)
payload["client"] = "Company1|askd"
serializer = ClientSerializer(data={"name": payload["client"]}, context=payload)
# test add client with | in name
payload = {
"client": {"name": "Client2|d"},
"site": {"name": "Site1"},
"custom_fields": [],
}
serializer = ClientSerializer(data=payload["client"])
with self.assertRaisesMessage(
ValidationError, "Client name cannot contain the | character"
):
@@ -48,19 +60,22 @@ class TestClientViews(TacticalTestCase):
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 400)
payload = {"client": "Company 156", "site": "Site2|a34"}
serializer = ClientSerializer(data={"name": payload["client"]}, context=payload)
with self.assertRaisesMessage(
ValidationError, "Site name cannot contain the | character"
):
self.assertFalse(serializer.is_valid(raise_exception=True))
# test add client with | in Site name
payload = {
"client": {"name": "Client2"},
"site": {"name": "Site1|fds"},
"custom_fields": [],
}
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 400)
# test unique
payload = {"client": "Company 1", "site": "Site 1"}
serializer = ClientSerializer(data={"name": payload["client"]}, context=payload)
payload = {
"client": {"name": "Client1"},
"site": {"name": "Site1"},
"custom_fields": [],
}
serializer = ClientSerializer(data=payload["client"])
with self.assertRaisesMessage(
ValidationError, "client with this name already exists."
):
@@ -69,67 +84,129 @@ class TestClientViews(TacticalTestCase):
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 400)
# test long site name
payload = {"client": "Company 2394", "site": "Site123" * 100}
serializer = ClientSerializer(data={"name": payload["client"]}, context=payload)
with self.assertRaisesMessage(ValidationError, "Site name too long"):
self.assertFalse(serializer.is_valid(raise_exception=True))
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 400)
# test initial setup
payload = {
"client": {"client": "Company 4", "site": "HQ"},
"initialsetup": True,
"client": {"name": "Setup Client"},
"site": {"name": "Setup Site"},
"timezone": "America/Los_Angeles",
"initialsetup": True,
}
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 200)
# test add with custom fields
field = baker.make("core.CustomField", model="client", type="text")
payload = {
"client": {"name": "Custom Field Client"},
"site": {"name": "Setup Site"},
"custom_fields": [{"field": field.id, "string_value": "new Value"}], # type: ignore
}
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 200)
client = Client.objects.get(name="Custom Field Client")
self.assertTrue(
ClientCustomField.objects.filter(client=client, field=field).exists()
)
self.check_not_authenticated("post", url)
def test_get_client(self):
# setup data
client = baker.make("clients.Client")
url = f"/clients/{client.id}/client/" # type: ignore
r = self.client.get(url, format="json")
serializer = ClientSerializer(client)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data, serializer.data) # type: ignore
self.check_not_authenticated("get", url)
def test_edit_client(self):
# setup data
client = baker.make("clients.Client")
client = baker.make("clients.Client", name="OldClientName")
# test invalid id
r = self.client.put("/clients/500/client/", format="json")
self.assertEqual(r.status_code, 404)
data = {"id": client.id, "name": "New Name"}
url = f"/clients/{client.id}/client/"
# test successfull edit client
data = {"client": {"name": "NewClientName"}, "custom_fields": []}
url = f"/clients/{client.id}/client/" # type: ignore
r = self.client.put(url, data, format="json")
self.assertEqual(r.status_code, 200)
self.assertTrue(Client.objects.filter(name="New Name").exists())
self.assertTrue(Client.objects.filter(name="NewClientName").exists())
self.assertFalse(Client.objects.filter(name="OldClientName").exists())
self.check_not_authenticated("put", url)
def test_delete_client(self):
# setup data
client = baker.make("clients.Client")
site = baker.make("clients.Site", client=client)
agent = baker.make_recipe("agents.agent", site=site)
# test invalid id
r = self.client.delete("/clients/500/client/", format="json")
self.assertEqual(r.status_code, 404)
url = f"/clients/{client.id}/client/"
# test deleting with agents under client
r = self.client.delete(url, format="json")
# test edit client with | in name
data = {"client": {"name": "NewClie|ntName"}, "custom_fields": []}
url = f"/clients/{client.id}/client/" # type: ignore
r = self.client.put(url, data, format="json")
self.assertEqual(r.status_code, 400)
# test successful deletion
agent.delete()
r = self.client.delete(url, format="json")
# test add with custom fields new value
field = baker.make("core.CustomField", model="client", type="checkbox")
payload = {
"client": {
"id": client.id, # type: ignore
"name": "Custom Field Client",
},
"custom_fields": [{"field": field.id, "bool_value": True}], # type: ignore
}
r = self.client.put(url, payload, format="json")
self.assertEqual(r.status_code, 200)
self.assertFalse(Client.objects.filter(pk=client.id).exists())
self.assertFalse(Site.objects.filter(pk=site.id).exists())
client = Client.objects.get(name="Custom Field Client")
self.assertTrue(
ClientCustomField.objects.filter(client=client, field=field).exists()
)
# edit custom field value
payload = {
"client": {
"id": client.id, # type: ignore
"name": "Custom Field Client",
},
"custom_fields": [{"field": field.id, "bool_value": False}], # type: ignore
}
r = self.client.put(url, payload, format="json")
self.assertEqual(r.status_code, 200)
self.assertFalse(
ClientCustomField.objects.get(client=client, field=field).value
)
self.check_not_authenticated("put", url)
@patch("automation.tasks.generate_all_agent_checks_task.delay")
@patch("automation.tasks.generate_all_agent_checks_task.delay")
def test_delete_client(self, task1, task2):
from agents.models import Agent
task1.return_value = "ok"
task2.return_value = "ok"
# setup data
client_to_delete = baker.make("clients.Client")
client_to_move = baker.make("clients.Client")
site_to_move = baker.make("clients.Site", client=client_to_move)
agent = baker.make_recipe("agents.agent", site=site_to_move)
# test invalid id
r = self.client.delete("/clients/334/953/", format="json")
self.assertEqual(r.status_code, 404)
url = f"/clients/{client_to_delete.id}/{site_to_move.id}/" # type: ignore
# test successful deletion
r = self.client.delete(url, format="json")
self.assertEqual(r.status_code, 200)
agent_moved = Agent.objects.get(pk=agent.pk)
self.assertEqual(agent_moved.site.id, site_to_move.id) # type: ignore
self.assertFalse(Client.objects.filter(pk=client_to_delete.id).exists()) # type: ignore
self.check_not_authenticated("delete", url)
def test_get_sites(self):
# setup data
baker.make("clients.Site", _quantity=5)
@@ -139,29 +216,31 @@ class TestClientViews(TacticalTestCase):
r = self.client.get(url, format="json")
serializer = SiteSerializer(sites, many=True)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data, serializer.data)
self.assertEqual(r.data, serializer.data) # type: ignore
self.check_not_authenticated("get", url)
def test_add_site(self):
# setup data
site = baker.make("clients.Site")
client = baker.make("clients.Client")
site = baker.make("clients.Site", client=client)
url = "/clients/sites/"
# test success add
payload = {"client": site.client.id, "name": "LA Office"}
payload = {
"site": {"client": client.id, "name": "LA Office"}, # type: ignore
"custom_fields": [],
}
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 200)
self.assertTrue(
Site.objects.filter(
name="LA Office", client__name=site.client.name
).exists()
)
# test with | symbol
payload = {"client": site.client.id, "name": "LA Off|ice |*&@#$"}
serializer = SiteSerializer(data=payload, context={"clientpk": site.client.id})
payload = {
"site": {"client": client.id, "name": "LA Office |*&@#$"}, # type: ignore
"custom_fields": [],
}
serializer = SiteSerializer(data=payload["site"])
with self.assertRaisesMessage(
ValidationError, "Site name cannot contain the | character"
):
@@ -171,55 +250,139 @@ class TestClientViews(TacticalTestCase):
self.assertEqual(r.status_code, 400)
# test site already exists
payload = {"client": site.client.id, "name": "LA Office"}
serializer = SiteSerializer(data=payload, context={"clientpk": site.client.id})
with self.assertRaisesMessage(ValidationError, "Site LA Office already exists"):
payload = {
"site": {"client": site.client.id, "name": "LA Office"}, # type: ignore
"custom_fields": [],
}
serializer = SiteSerializer(data=payload["site"])
with self.assertRaisesMessage(
ValidationError, "The fields client, name must make a unique set."
):
self.assertFalse(serializer.is_valid(raise_exception=True))
# test add with custom fields
field = baker.make(
"core.CustomField",
model="site",
type="single",
options=["one", "two", "three"],
)
payload = {
"site": {"client": client.id, "name": "Custom Field Site"}, # type: ignore
"custom_fields": [{"field": field.id, "string_value": "one"}], # type: ignore
}
r = self.client.post(url, payload, format="json")
self.assertEqual(r.status_code, 200)
site = Site.objects.get(name="Custom Field Site")
self.assertTrue(SiteCustomField.objects.filter(site=site, field=field).exists())
self.check_not_authenticated("post", url)
def test_edit_site(self):
def test_get_site(self):
# setup data
site = baker.make("clients.Site")
url = f"/clients/sites/{site.id}/" # type: ignore
r = self.client.get(url, format="json")
serializer = SiteSerializer(site)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data, serializer.data) # type: ignore
self.check_not_authenticated("get", url)
def test_edit_site(self):
# setup data
client = baker.make("clients.Client")
site = baker.make("clients.Site", client=client)
# test invalid id
r = self.client.put("/clients/500/site/", format="json")
r = self.client.put("/clients/sites/688/", format="json")
self.assertEqual(r.status_code, 404)
data = {"id": site.id, "name": "New Name", "client": site.client.id}
data = {
"site": {"client": client.id, "name": "New Site Name"}, # type: ignore
"custom_fields": [],
}
url = f"/clients/{site.id}/site/"
url = f"/clients/sites/{site.id}/" # type: ignore
r = self.client.put(url, data, format="json")
self.assertEqual(r.status_code, 200)
self.assertTrue(Site.objects.filter(name="New Name").exists())
self.assertTrue(
Site.objects.filter(client=client, name="New Site Name").exists()
)
# test add with custom fields new value
field = baker.make(
"core.CustomField",
model="site",
type="multiple",
options=["one", "two", "three"],
)
payload = {
"site": {
"id": site.id, # type: ignore
"client": site.client.id, # type: ignore
"name": "Custom Field Site",
},
"custom_fields": [{"field": field.id, "multiple_value": ["two", "three"]}], # type: ignore
}
r = self.client.put(url, payload, format="json")
self.assertEqual(r.status_code, 200)
site = Site.objects.get(name="Custom Field Site")
self.assertTrue(SiteCustomField.objects.filter(site=site, field=field).exists())
# edit custom field value
payload = {
"site": {
"id": site.id, # type: ignore
"client": client.id, # type: ignore
"name": "Custom Field Site",
},
"custom_fields": [{"field": field.id, "multiple_value": ["one"]}], # type: ignore
}
r = self.client.put(url, payload, format="json")
self.assertEqual(r.status_code, 200)
self.assertTrue(
SiteCustomField.objects.get(site=site, field=field).value,
["one"],
)
self.check_not_authenticated("put", url)
def test_delete_site(self):
@patch("automation.tasks.generate_all_agent_checks_task.delay")
@patch("automation.tasks.generate_all_agent_checks_task.delay")
def test_delete_site(self, task1, task2):
from agents.models import Agent
task1.return_value = "ok"
task2.return_value = "ok"
# setup data
site = baker.make("clients.Site")
agent = baker.make_recipe("agents.agent", site=site)
client = baker.make("clients.Client")
site_to_delete = baker.make("clients.Site", client=client)
site_to_move = baker.make("clients.Site")
agent = baker.make_recipe("agents.agent", site=site_to_delete)
# test invalid id
r = self.client.delete("/clients/500/site/", format="json")
r = self.client.delete("/clients/500/445/", format="json")
self.assertEqual(r.status_code, 404)
url = f"/clients/{site.id}/site/"
url = f"/clients/sites/{site_to_delete.id}/{site_to_move.id}/" # type: ignore
# test deleting with last site under client
r = self.client.delete(url, format="json")
self.assertEqual(r.status_code, 400)
# test deletion when agents exist under site
baker.make("clients.Site", client=site.client)
r = self.client.delete(url, format="json")
self.assertEqual(r.status_code, 400)
self.assertEqual(r.json(), "A client must have at least 1 site.")
# test successful deletion
agent.delete()
site_to_move.client = client # type: ignore
site_to_move.save(update_fields=["client"]) # type: ignore
r = self.client.delete(url, format="json")
self.assertEqual(r.status_code, 200)
self.assertFalse(Site.objects.filter(pk=site.id).exists())
agent_moved = Agent.objects.get(pk=agent.pk)
self.assertEqual(agent_moved.site.id, site_to_move.id) # type: ignore
self.check_not_authenticated("delete", url)
@@ -233,7 +396,7 @@ class TestClientViews(TacticalTestCase):
r = self.client.get(url, format="json")
serializer = ClientTreeSerializer(clients, many=True)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data, serializer.data)
self.assertEqual(r.data, serializer.data) # type: ignore
self.check_not_authenticated("get", url)
@@ -245,7 +408,7 @@ class TestClientViews(TacticalTestCase):
r = self.client.get(url)
serializer = DeploymentSerializer(deployments, many=True)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data, serializer.data)
self.assertEqual(r.data, serializer.data) # type: ignore
self.check_not_authenticated("get", url)
@@ -255,8 +418,8 @@ class TestClientViews(TacticalTestCase):
url = "/clients/deployments/"
payload = {
"client": site.client.id,
"site": site.id,
"client": site.client.id, # type: ignore
"site": site.id, # type: ignore
"expires": "2037-11-23 18:53",
"power": 1,
"ping": 0,
@@ -284,10 +447,10 @@ class TestClientViews(TacticalTestCase):
url = "/clients/deployments/"
url = f"/clients/{deployment.id}/deployment/"
url = f"/clients/{deployment.id}/deployment/" # type: ignore
r = self.client.delete(url)
self.assertEqual(r.status_code, 200)
self.assertFalse(Deployment.objects.filter(pk=deployment.id).exists())
self.assertFalse(Deployment.objects.filter(pk=deployment.id).exists()) # type: ignore
url = "/clients/32348/deployment/"
r = self.client.delete(url)
@@ -301,7 +464,7 @@ class TestClientViews(TacticalTestCase):
r = self.client.get(url)
self.assertEqual(r.status_code, 400)
self.assertEqual(r.data, "invalid")
self.assertEqual(r.data, "invalid") # type: ignore
uid = uuid.uuid4()
url = f"/clients/{uid}/deploy/"

View File

@@ -4,10 +4,12 @@ from . import views
urlpatterns = [
path("clients/", views.GetAddClients.as_view()),
path("<int:pk>/client/", views.GetUpdateDeleteClient.as_view()),
path("<int:pk>/client/", views.GetUpdateClient.as_view()),
path("<int:pk>/<int:sitepk>/", views.DeleteClient.as_view()),
path("tree/", views.GetClientTree.as_view()),
path("sites/", views.GetAddSites.as_view()),
path("<int:pk>/site/", views.GetUpdateDeleteSite.as_view()),
path("sites/<int:pk>/", views.GetUpdateSite.as_view()),
path("sites/<int:pk>/<int:sitepk>/", views.DeleteSite.as_view()),
path("deployments/", views.AgentDeployment.as_view()),
path("<int:pk>/deployment/", views.AgentDeployment.as_view()),
path("<str:uid>/deploy/", views.GenerateAgent.as_view()),

View File

@@ -6,22 +6,27 @@ import pytz
from django.conf import settings
from django.shortcuts import get_object_or_404
from django.utils import timezone as djangotime
from loguru import logger
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.views import APIView
from agents.models import Agent
from core.models import CoreSettings
from tacticalrmm.utils import generate_installer_exe, notify_error
from tacticalrmm.utils import notify_error
from .models import Client, Deployment, Site
from .models import Client, ClientCustomField, Deployment, Site, SiteCustomField
from .serializers import (
ClientCustomFieldSerializer,
ClientSerializer,
ClientTreeSerializer,
DeploymentSerializer,
SiteCustomFieldSerializer,
SiteSerializer,
)
logger.configure(**settings.LOG_CONFIG)
class GetAddClients(APIView):
def get(self, request):
@@ -29,45 +34,99 @@ class GetAddClients(APIView):
return Response(ClientSerializer(clients, many=True).data)
def post(self, request):
# create client
client_serializer = ClientSerializer(data=request.data["client"])
client_serializer.is_valid(raise_exception=True)
client = client_serializer.save()
if "initialsetup" in request.data:
client = {"name": request.data["client"]["client"].strip()}
site = {"name": request.data["client"]["site"].strip()}
serializer = ClientSerializer(data=client, context=request.data["client"])
serializer.is_valid(raise_exception=True)
# create site
site_serializer = SiteSerializer(
data={"client": client.id, "name": request.data["site"]["name"]}
)
# make sure site serializer doesn't return errors and save
if site_serializer.is_valid():
site_serializer.save()
else:
# delete client since site serializer was invalid
client.delete()
site_serializer.is_valid(raise_exception=True)
if "initialsetup" in request.data.keys():
core = CoreSettings.objects.first()
core.default_time_zone = request.data["timezone"]
core.save(update_fields=["default_time_zone"])
else:
client = {"name": request.data["client"].strip()}
site = {"name": request.data["site"].strip()}
serializer = ClientSerializer(data=client, context=request.data)
serializer.is_valid(raise_exception=True)
obj = serializer.save()
Site(client=obj, name=site["name"]).save()
# save custom fields
if "custom_fields" in request.data.keys():
for field in request.data["custom_fields"]:
return Response(f"{obj} was added!")
custom_field = field
custom_field["client"] = client.id
serializer = ClientCustomFieldSerializer(data=custom_field)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(f"{client} was added!")
class GetUpdateDeleteClient(APIView):
class GetUpdateClient(APIView):
def get(self, request, pk):
client = get_object_or_404(Client, pk=pk)
return Response(ClientSerializer(client).data)
def put(self, request, pk):
client = get_object_or_404(Client, pk=pk)
serializer = ClientSerializer(data=request.data, instance=client, partial=True)
serializer = ClientSerializer(
data=request.data["client"], instance=client, partial=True
)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("The Client was renamed")
# update custom fields
if "custom_fields" in request.data.keys():
for field in request.data["custom_fields"]:
custom_field = field
custom_field["client"] = pk
if ClientCustomField.objects.filter(field=field["field"], client=pk):
value = ClientCustomField.objects.get(
field=field["field"], client=pk
)
serializer = ClientCustomFieldSerializer(
instance=value, data=custom_field
)
serializer.is_valid(raise_exception=True)
serializer.save()
else:
serializer = ClientCustomFieldSerializer(data=custom_field)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("The Client was updated")
class DeleteClient(APIView):
def delete(self, request, pk, sitepk):
from automation.tasks import generate_all_agent_checks_task
def delete(self, request, pk):
client = get_object_or_404(Client, pk=pk)
agent_count = Agent.objects.filter(site__client=client).count()
if agent_count > 0:
agents = Agent.objects.filter(site__client=client)
if not sitepk:
return notify_error(
f"Cannot delete {client} while {agent_count} agents exist in it. Move the agents to another client first."
"There needs to be a site specified to move existing agents to"
)
site = get_object_or_404(Site, pk=sitepk)
agents.update(site=site)
generate_all_agent_checks_task.delay("workstation", create_tasks=True)
generate_all_agent_checks_task.delay("server", create_tasks=True)
client.delete()
return Response(f"{client.name} was deleted!")
@@ -84,39 +143,90 @@ class GetAddSites(APIView):
return Response(SiteSerializer(sites, many=True).data)
def post(self, request):
name = request.data["name"].strip()
serializer = SiteSerializer(data=request.data["site"])
serializer.is_valid(raise_exception=True)
site = serializer.save()
# save custom fields
if "custom_fields" in request.data.keys():
for field in request.data["custom_fields"]:
custom_field = field
custom_field["site"] = site.id
serializer = SiteCustomFieldSerializer(data=custom_field)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(f"Site {site.name} was added!")
class GetUpdateSite(APIView):
def get(self, request, pk):
site = get_object_or_404(Site, pk=pk)
return Response(SiteSerializer(site).data)
def put(self, request, pk):
site = get_object_or_404(Site, pk=pk)
if "client" in request.data["site"].keys() and (
site.client.id != request.data["site"]["client"]
and site.client.sites.count() == 1
):
return notify_error("A client must have at least one site")
serializer = SiteSerializer(
data={"name": name, "client": request.data["client"]},
context={"clientpk": request.data["client"]},
instance=site, data=request.data["site"], partial=True
)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("ok")
# update custom field
if "custom_fields" in request.data.keys():
for field in request.data["custom_fields"]:
custom_field = field
custom_field["site"] = pk
if SiteCustomField.objects.filter(field=field["field"], site=pk):
value = SiteCustomField.objects.get(field=field["field"], site=pk)
serializer = SiteCustomFieldSerializer(
instance=value, data=custom_field, partial=True
)
serializer.is_valid(raise_exception=True)
serializer.save()
else:
serializer = SiteCustomFieldSerializer(data=custom_field)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("Site was edited!")
class GetUpdateDeleteSite(APIView):
def put(self, request, pk):
class DeleteSite(APIView):
def delete(self, request, pk, sitepk):
from automation.tasks import generate_all_agent_checks_task
site = get_object_or_404(Site, pk=pk)
serializer = SiteSerializer(instance=site, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("ok")
def delete(self, request, pk):
site = get_object_or_404(Site, pk=pk)
if site.client.sites.count() == 1:
return notify_error(f"A client must have at least 1 site.")
return notify_error("A client must have at least 1 site.")
agent_count = Agent.objects.filter(site=site).count()
agents = Agent.objects.filter(site=site)
if agent_count > 0:
if not sitepk:
return notify_error(
f"Cannot delete {site.name} while {agent_count} agents exist in it. Move the agents to another site first."
"There needs to be a site specified to move the agents to"
)
agent_site = get_object_or_404(Site, pk=sitepk)
agents.update(site=agent_site)
generate_all_agent_checks_task.delay("workstation", create_tasks=True)
generate_all_agent_checks_task.delay("server", create_tasks=True)
site.delete()
return Response(f"{site.name} was deleted!")
@@ -173,6 +283,10 @@ class GenerateAgent(APIView):
permission_classes = (AllowAny,)
def get(self, request, uid):
import tempfile
import requests
from django.http import FileResponse
try:
_ = uuid.UUID(uid, version=4)
except ValueError:
@@ -190,18 +304,44 @@ class GenerateAgent(APIView):
client = re.sub(r"([^a-zA-Z0-9]+)", "", client)
site = re.sub(r"([^a-zA-Z0-9]+)", "", site)
ext = ".exe" if d.arch == "64" else "-x86.exe"
file_name = f"rmm-{client}-{site}-{d.mon_type}{ext}"
return generate_installer_exe(
file_name=f"rmm-{client}-{site}-{d.mon_type}{ext}",
goarch="amd64" if d.arch == "64" else "386",
inno=inno,
api=f"https://{request.get_host()}",
client_id=d.client.pk,
site_id=d.site.pk,
atype=d.mon_type,
rdp=d.install_flags["rdp"],
ping=d.install_flags["ping"],
power=d.install_flags["power"],
download_url=settings.DL_64 if d.arch == "64" else settings.DL_32,
token=d.token_key,
)
data = {
"client": d.client.pk,
"site": d.site.pk,
"agenttype": d.mon_type,
"rdp": str(d.install_flags["rdp"]),
"ping": str(d.install_flags["ping"]),
"power": str(d.install_flags["power"]),
"goarch": "amd64" if d.arch == "64" else "386",
"token": d.token_key,
"inno": inno,
"url": settings.DL_64 if d.arch == "64" else settings.DL_32,
"api": f"https://{request.get_host()}",
}
headers = {"Content-type": "application/json"}
with tempfile.NamedTemporaryFile() as fp:
try:
r = requests.post(
settings.EXE_GEN_URL,
json=data,
headers=headers,
stream=True,
timeout=900,
)
except Exception as e:
logger.error(str(e))
return notify_error(
"Something went wrong. Check debug error log for exact error message"
)
with open(fp.name, "wb") as f:
for chunk in r.iter_content(chunk_size=1024):
if chunk:
f.write(chunk)
del r
response = FileResponse(
open(fp.name, "rb"), as_attachment=True, filename=file_name
)
return response

View File

@@ -1,5 +1,6 @@
from django.contrib import admin
from .models import CoreSettings
from .models import CoreSettings, CustomField
admin.site.register(CoreSettings)
admin.site.register(CustomField)

View File

@@ -1,5 +0,0 @@
module github.com/wh1te909/goinstaller
go 1.16
require github.com/josephspurrier/goversioninfo v1.2.0 // indirect

View File

@@ -1,10 +0,0 @@
github.com/akavel/rsrc v0.8.0 h1:zjWn7ukO9Kc5Q62DOJCcxGpXC18RawVtYAGdz2aLlfw=
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/josephspurrier/goversioninfo v1.2.0 h1:tpLHXAxLHKHg/dCU2AAYx08A4m+v9/CWg6+WUvTF4uQ=
github.com/josephspurrier/goversioninfo v1.2.0/go.mod h1:AGP2a+Y/OVJZ+s6XM4IwFUpkETwvn0orYurY8qpw1+0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
type="win32"
name="TacticalRMMInstaller"
version="1.0.0.0"
processorArchitecture="*"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel
level="requireAdministrator"
uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
</assembly>

View File

@@ -1,186 +0,0 @@
//go:generate goversioninfo -icon=onit.ico -manifest=goversioninfo.exe.manifest -gofile=versioninfo.go
package main
import (
"bufio"
"flag"
"fmt"
"io"
"net"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)
var (
Inno string
Api string
Client string
Site string
Atype string
Power string
Rdp string
Ping string
Token string
DownloadUrl string
)
var netTransport = &http.Transport{
Dial: (&net.Dialer{
Timeout: 5 * time.Second,
}).Dial,
TLSHandshakeTimeout: 5 * time.Second,
}
var netClient = &http.Client{
Timeout: time.Second * 900,
Transport: netTransport,
}
func downloadAgent(filepath string) (err error) {
out, err := os.Create(filepath)
if err != nil {
return err
}
defer out.Close()
resp, err := netClient.Get(DownloadUrl)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("Bad response: %s", resp.Status)
}
_, err = io.Copy(out, resp.Body)
if err != nil {
return err
}
return nil
}
func main() {
debugLog := flag.String("log", "", "Verbose output")
localMesh := flag.String("local-mesh", "", "Use local mesh agent")
silent := flag.Bool("silent", false, "Do not popup any message boxes during installation")
cert := flag.String("cert", "", "Path to ca.pem")
flag.Parse()
var debug bool = false
if strings.TrimSpace(strings.ToLower(*debugLog)) == "debug" {
debug = true
}
agentBinary := filepath.Join(os.Getenv("windir"), "Temp", Inno)
tacrmm := filepath.Join(os.Getenv("PROGRAMFILES"), "TacticalAgent", "tacticalrmm.exe")
cmdArgs := []string{
"-m", "install", "--api", Api, "--client-id",
Client, "--site-id", Site, "--agent-type", Atype,
"--auth", Token,
}
if debug {
cmdArgs = append(cmdArgs, "-log", "debug")
}
if *silent {
cmdArgs = append(cmdArgs, "-silent")
}
if len(strings.TrimSpace(*localMesh)) != 0 {
cmdArgs = append(cmdArgs, "-local-mesh", *localMesh)
}
if len(strings.TrimSpace(*cert)) != 0 {
cmdArgs = append(cmdArgs, "-cert", *cert)
}
if Rdp == "1" {
cmdArgs = append(cmdArgs, "-rdp")
}
if Ping == "1" {
cmdArgs = append(cmdArgs, "-ping")
}
if Power == "1" {
cmdArgs = append(cmdArgs, "-power")
}
if debug {
fmt.Println("Installer:", agentBinary)
fmt.Println("Tactical Agent:", tacrmm)
fmt.Println("Download URL:", DownloadUrl)
fmt.Println("Install command:", tacrmm, strings.Join(cmdArgs, " "))
}
fmt.Println("Downloading agent...")
dl := downloadAgent(agentBinary)
if dl != nil {
fmt.Println("ERROR: unable to download agent from", DownloadUrl)
fmt.Println(dl)
os.Exit(1)
}
defer os.Remove(agentBinary)
fmt.Println("Extracting files...")
winagentCmd := exec.Command(agentBinary, "/VERYSILENT", "/SUPPRESSMSGBOXES")
err := winagentCmd.Run()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
time.Sleep(5 * time.Second)
fmt.Println("Installation starting.")
cmd := exec.Command(tacrmm, cmdArgs...)
cmdReader, err := cmd.StdoutPipe()
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
cmdErrReader, oerr := cmd.StderrPipe()
if oerr != nil {
fmt.Fprintln(os.Stderr, oerr)
return
}
scanner := bufio.NewScanner(cmdReader)
escanner := bufio.NewScanner(cmdErrReader)
go func() {
for scanner.Scan() {
fmt.Println(scanner.Text())
}
}()
go func() {
for escanner.Scan() {
fmt.Println(escanner.Text())
}
}()
err = cmd.Start()
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
err = cmd.Wait()
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -1,43 +0,0 @@
{
"FixedFileInfo": {
"FileVersion": {
"Major": 1,
"Minor": 0,
"Patch": 0,
"Build": 0
},
"ProductVersion": {
"Major": 1,
"Minor": 0,
"Patch": 0,
"Build": 0
},
"FileFlagsMask": "3f",
"FileFlags ": "00",
"FileOS": "040004",
"FileType": "01",
"FileSubType": "00"
},
"StringFileInfo": {
"Comments": "",
"CompanyName": "Tactical Techs",
"FileDescription": "Tactical RMM Installer",
"FileVersion": "v1.0.0.0",
"InternalName": "rmm.exe",
"LegalCopyright": "Copyright (c) 2020 Tactical Techs",
"LegalTrademarks": "",
"OriginalFilename": "installer.go",
"PrivateBuild": "",
"ProductName": "Tactical RMM Installer",
"ProductVersion": "v1.0.0.0",
"SpecialBuild": ""
},
"VarFileInfo": {
"Translation": {
"LangID": "0409",
"CharsetID": "04B0"
}
},
"IconPath": "",
"ManifestPath": ""
}

View File

@@ -0,0 +1,27 @@
# Generated by Django 3.1.7 on 2021-03-17 14:45
import django.contrib.postgres.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0013_coresettings_alert_template'),
]
operations = [
migrations.CreateModel(
name='CustomField',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('order', models.PositiveIntegerField()),
('model', models.CharField(choices=[('client', 'Client'), ('site', 'Site'), ('agent', 'Agent')], max_length=25)),
('type', models.CharField(choices=[('text', 'Text'), ('number', 'Number'), ('single', 'Single'), ('multiple', 'Multiple'), ('checkbox', 'Checkbox'), ('datetime', 'DateTime')], default='text', max_length=25)),
('options', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=255, null=True), blank=True, default=list, null=True, size=None)),
('name', models.TextField(blank=True, null=True)),
('default_value', models.TextField(blank=True, null=True)),
('required', models.BooleanField(blank=True, default=False)),
],
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.7 on 2021-03-18 20:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0014_customfield'),
]
operations = [
migrations.AlterField(
model_name='customfield',
name='order',
field=models.PositiveIntegerField(default=0),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 3.1.7 on 2021-03-19 15:36
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('core', '0015_auto_20210318_2034'),
]
operations = [
migrations.AlterUniqueTogether(
name='customfield',
unique_together={('model', 'name')},
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 3.1.7 on 2021-03-29 10:50
import django.contrib.postgres.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0016_auto_20210319_1536'),
]
operations = [
migrations.AddField(
model_name='customfield',
name='checkbox_value',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='customfield',
name='default_values_multiple',
field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=255, null=True), blank=True, default=list, null=True, size=None),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.1.7 on 2021-03-29 17:09
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('core', '0017_auto_20210329_1050'),
]
operations = [
migrations.RenameField(
model_name='customfield',
old_name='checkbox_value',
new_name='default_value_bool',
),
migrations.RenameField(
model_name='customfield',
old_name='default_value',
new_name='default_value_string',
),
]

View File

@@ -216,3 +216,53 @@ class CoreSettings(BaseAuditModel):
from .serializers import CoreSerializer
return CoreSerializer(core).data
FIELD_TYPE_CHOICES = (
("text", "Text"),
("number", "Number"),
("single", "Single"),
("multiple", "Multiple"),
("checkbox", "Checkbox"),
("datetime", "DateTime"),
)
MODEL_CHOICES = (("client", "Client"), ("site", "Site"), ("agent", "Agent"))
class CustomField(models.Model):
order = models.PositiveIntegerField(default=0)
model = models.CharField(max_length=25, choices=MODEL_CHOICES)
type = models.CharField(max_length=25, choices=FIELD_TYPE_CHOICES, default="text")
options = ArrayField(
models.CharField(max_length=255, null=True, blank=True),
null=True,
blank=True,
default=list,
)
name = models.TextField(null=True, blank=True)
required = models.BooleanField(blank=True, default=False)
default_value_string = models.TextField(null=True, blank=True)
default_value_bool = models.BooleanField(default=False)
default_values_multiple = ArrayField(
models.CharField(max_length=255, null=True, blank=True),
null=True,
blank=True,
default=list,
)
class Meta:
unique_together = (("model", "name"),)
def __str__(self):
return self.name
@property
def default_value(self):
if self.type == "multiple":
return self.default_values_multiple
elif self.type == "checkbox":
return self.default_value_bool
else:
return self.default_value_string

View File

@@ -1,7 +1,7 @@
import pytz
from rest_framework import serializers
from .models import CoreSettings
from .models import CoreSettings, CustomField
class CoreSettingsSerializer(serializers.ModelSerializer):
@@ -21,3 +21,9 @@ class CoreSerializer(serializers.ModelSerializer):
class Meta:
model = CoreSettings
fields = "__all__"
class CustomFieldSerializer(serializers.ModelSerializer):
class Meta:
model = CustomField
fields = "__all__"

View File

@@ -1,11 +1,13 @@
from unittest.mock import patch
from model_bakery import baker, seq
from model_bakery import baker
from core.models import CoreSettings
from core.tasks import core_maintenance_tasks
from tacticalrmm.test import TacticalTestCase
from .models import CoreSettings, CustomField
from .serializers import CustomFieldSerializer
from .tasks import core_maintenance_tasks
class TestCoreTasks(TacticalTestCase):
def setUp(self):
@@ -42,7 +44,7 @@ class TestCoreTasks(TacticalTestCase):
url = "/core/editsettings/"
# setup
policies = baker.make("Policy", _quantity=2)
policies = baker.make("automation.Policy", _quantity=2)
# test normal request
data = {
"smtp_from_email": "newexample@example.com",
@@ -59,14 +61,14 @@ class TestCoreTasks(TacticalTestCase):
# test adding policy
data = {
"workstation_policy": policies[0].id,
"server_policy": policies[1].id,
"workstation_policy": policies[0].id, # type: ignore
"server_policy": policies[1].id, # type: ignore
}
r = self.client.patch(url, data)
self.assertEqual(r.status_code, 200)
self.assertEqual(CoreSettings.objects.first().server_policy.id, policies[1].id)
self.assertEqual(CoreSettings.objects.first().server_policy.id, policies[1].id) # type: ignore
self.assertEqual(
CoreSettings.objects.first().workstation_policy.id, policies[0].id
CoreSettings.objects.first().workstation_policy.id, policies[0].id # type: ignore
)
self.assertEqual(generate_all_agent_checks_task.call_count, 2)
@@ -128,3 +130,97 @@ class TestCoreTasks(TacticalTestCase):
remove_orphaned_win_tasks.assert_called()
self.check_not_authenticated("post", url)
def test_get_custom_fields(self):
url = "/core/customfields/"
# setup
custom_fields = baker.make("core.CustomField", _quantity=2)
r = self.client.get(url)
serializer = CustomFieldSerializer(custom_fields, many=True)
self.assertEqual(r.status_code, 200)
self.assertEqual(len(r.data), 2) # type: ignore
self.assertEqual(r.data, serializer.data) # type: ignore
self.check_not_authenticated("get", url)
def test_get_custom_fields_by_model(self):
url = "/core/customfields/"
# setup
custom_fields = baker.make("core.CustomField", model="agent", _quantity=5)
baker.make("core.CustomField", model="client", _quantity=5)
# will error if request invalid
r = self.client.patch(url, {"invalid": ""})
self.assertEqual(r.status_code, 400)
data = {"model": "agent"}
r = self.client.patch(url, data)
serializer = CustomFieldSerializer(custom_fields, many=True)
self.assertEqual(r.status_code, 200)
self.assertEqual(len(r.data), 5) # type: ignore
self.assertEqual(r.data, serializer.data) # type: ignore
self.check_not_authenticated("patch", url)
def test_add_custom_field(self):
url = "/core/customfields/"
data = {"model": "client", "type": "text", "name": "Field"}
r = self.client.patch(url, data)
self.assertEqual(r.status_code, 200)
self.check_not_authenticated("post", url)
def test_get_custom_field(self):
# setup
custom_field = baker.make("core.CustomField")
# test not found
r = self.client.get("/core/customfields/500/")
self.assertEqual(r.status_code, 404)
url = f"/core/customfields/{custom_field.id}/" # type: ignore
r = self.client.get(url)
serializer = CustomFieldSerializer(custom_field)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.data, serializer.data) # type: ignore
self.check_not_authenticated("get", url)
def test_update_custom_field(self):
# setup
custom_field = baker.make("core.CustomField")
# test not found
r = self.client.put("/core/customfields/500/")
self.assertEqual(r.status_code, 404)
url = f"/core/customfields/{custom_field.id}/" # type: ignore
data = {"type": "single", "options": ["ione", "two", "three"]}
r = self.client.put(url, data)
self.assertEqual(r.status_code, 200)
new_field = CustomField.objects.get(pk=custom_field.id) # type: ignore
self.assertEqual(new_field.type, data["type"])
self.assertEqual(new_field.options, data["options"])
self.check_not_authenticated("put", url)
def test_delete_custom_field(self):
# setup
custom_field = baker.make("core.CustomField")
# test not found
r = self.client.delete("/core/customfields/500/")
self.assertEqual(r.status_code, 404)
url = f"/core/customfields/{custom_field.id}/" # type: ignore
r = self.client.delete(url)
self.assertEqual(r.status_code, 200)
self.assertFalse(CustomField.objects.filter(pk=custom_field.id).exists()) # type: ignore
self.check_not_authenticated("delete", url)

View File

@@ -10,4 +10,6 @@ urlpatterns = [
path("emailtest/", views.email_test),
path("dashinfo/", views.dashboard_info),
path("servermaintenance/", views.server_maintenance),
path("customfields/", views.GetAddCustomFields.as_view()),
path("customfields/<int:pk>/", views.GetUpdateDeleteCustomFields.as_view()),
]

View File

@@ -1,6 +1,7 @@
import os
from django.conf import settings
from django.shortcuts import get_object_or_404
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.exceptions import ParseError
@@ -10,8 +11,8 @@ from rest_framework.views import APIView
from tacticalrmm.utils import notify_error
from .models import CoreSettings
from .serializers import CoreSettingsSerializer
from .models import CoreSettings, CustomField
from .serializers import CoreSettingsSerializer, CustomFieldSerializer
class UploadMeshAgent(APIView):
@@ -133,3 +134,46 @@ def server_maintenance(request):
return Response(f"{records_count} records were pruned from the database")
return notify_error("The data is incorrect")
class GetAddCustomFields(APIView):
def get(self, request):
fields = CustomField.objects.all()
return Response(CustomFieldSerializer(fields, many=True).data)
def patch(self, request):
if "model" in request.data.keys():
fields = CustomField.objects.filter(model=request.data["model"])
return Response(CustomFieldSerializer(fields, many=True).data)
else:
return notify_error("The request was invalid")
def post(self, request):
serializer = CustomFieldSerializer(data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("ok")
class GetUpdateDeleteCustomFields(APIView):
def get(self, request, pk):
custom_field = get_object_or_404(CustomField, pk=pk)
return Response(CustomFieldSerializer(custom_field).data)
def put(self, request, pk):
custom_field = get_object_or_404(CustomField, pk=pk)
serializer = CustomFieldSerializer(
instance=custom_field, data=request.data, partial=True
)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response("ok")
def delete(self, request, pk):
get_object_or_404(CustomField, pk=pk).delete()
return Response("ok")

View File

@@ -1,5 +0,0 @@
from django.apps import AppConfig
class NatsapiConfig(AppConfig):
name = "natsapi"

View File

@@ -1,36 +0,0 @@
from django.conf import settings
from model_bakery import baker
from tacticalrmm.test import TacticalTestCase
class TestNatsAPIViews(TacticalTestCase):
def setUp(self):
self.authenticate()
self.setup_coresettings()
def test_nats_agents(self):
baker.make_recipe(
"agents.online_agent", version=settings.LATEST_AGENT_VER, _quantity=14
)
baker.make_recipe(
"agents.offline_agent", version=settings.LATEST_AGENT_VER, _quantity=6
)
baker.make_recipe(
"agents.overdue_agent", version=settings.LATEST_AGENT_VER, _quantity=5
)
url = "/natsapi/online/agents/"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(len(r.json()["agent_ids"]), 14)
url = "/natsapi/offline/agents/"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(len(r.json()["agent_ids"]), 11)
url = "/natsapi/asdjaksdasd/agents/"
r = self.client.get(url)
self.assertEqual(r.status_code, 400)

View File

@@ -1,9 +0,0 @@
from django.urls import path
from . import views
urlpatterns = [
path("natsinfo/", views.nats_info),
path("<str:stat>/agents/", views.NatsAgents.as_view()),
path("logcrash/", views.LogCrash.as_view()),
]

View File

@@ -1,60 +0,0 @@
from django.conf import settings
from django.shortcuts import get_object_or_404
from django.utils import timezone as djangotime
from loguru import logger
from rest_framework.decorators import (
api_view,
authentication_classes,
permission_classes,
)
from rest_framework.response import Response
from rest_framework.views import APIView
from agents.models import Agent
from tacticalrmm.utils import notify_error
logger.configure(**settings.LOG_CONFIG)
@api_view()
@permission_classes([])
@authentication_classes([])
def nats_info(request):
return Response({"user": "tacticalrmm", "password": settings.SECRET_KEY})
class NatsAgents(APIView):
authentication_classes = [] # type: ignore
permission_classes = [] # type: ignore
def get(self, request, stat: str):
if stat not in ["online", "offline"]:
return notify_error("invalid request")
ret: list[str] = []
agents = Agent.objects.only(
"pk", "agent_id", "version", "last_seen", "overdue_time", "offline_time"
)
if stat == "online":
ret = [i.agent_id for i in agents if i.status == "online"]
else:
ret = [i.agent_id for i in agents if i.status != "online"]
return Response({"agent_ids": ret})
class LogCrash(APIView):
authentication_classes = [] # type: ignore
permission_classes = [] # type: ignore
def post(self, request):
agent = get_object_or_404(Agent, agent_id=request.data["agentid"])
agent.last_seen = djangotime.now()
agent.save(update_fields=["last_seen"])
if hasattr(settings, "DEBUGTEST") and settings.DEBUGTEST:
logger.info(
f"Detected crashed tacticalagent service on {agent.hostname} v{agent.version}, attempting recovery"
)
return Response("ok")

View File

@@ -6,12 +6,12 @@ celery==5.0.5
certifi==2020.12.5
cffi==1.14.5
chardet==4.0.0
cryptography==3.4.6
cryptography==3.4.7
decorator==4.4.2
Django==3.1.7
django-cors-headers==3.7.0
django-rest-knox==4.1.0
djangorestframework==3.12.2
djangorestframework==3.12.4
future==0.18.2
kombu==5.0.2
loguru==0.5.3
@@ -28,8 +28,8 @@ redis==3.5.3
requests==2.25.1
six==1.15.0
sqlparse==0.4.1
twilio==6.53.0
urllib3==1.26.3
twilio==6.55.0
urllib3==1.26.4
uWSGI==2.0.19.1
validators==0.18.2
vine==5.0.0

View File

@@ -21,11 +21,12 @@
"shell": "powershell"
},
{
"filename": "InstallDuplicati.ps1",
"filename": "Win_Install_Duplicati.ps1",
"submittedBy": "https://github.com/Omnicef",
"name": "Install Duplicati",
"description": "This script installs Duplicati 2.0.5.1 as a service.",
"shell": "powershell"
"shell": "powershell",
"category": "TRMM (Win):3rd Party Software"
},
{
"filename": "Reset-WindowsUpdate.ps1",
@@ -77,11 +78,12 @@
"shell": "powershell"
},
{
"filename": "bitlocker_create_status_report.ps1",
"filename": "Win_Bitlocker_Create_Status_Report.ps1",
"submittedBy": "https://github.com/ThatsNASt",
"name": "Create Bitlocker Status Report",
"description": "Creates a Bitlocker status report.",
"shell": "powershell"
"shell": "powershell",
"category": "TRMM (Win):Storage"
},
{
"filename": "bitlocker_retrieve_status_report.ps1",
@@ -91,11 +93,12 @@
"shell": "powershell"
},
{
"filename": "bios_check.ps1",
"filename": "Win_Bios_Check.ps1",
"submittedBy": "https://github.com/ThatsNASt",
"name": "Check BIOS Information",
"description": "Retreives and reports on BIOS make, version, and date .",
"shell": "powershell"
"description": "Retreives and reports on BIOS make, version, and date.",
"shell": "powershell",
"category": "TRMM (Win):Hardware"
},
{
"filename": "ResetHighPerformancePowerProfiletoDefaults.ps1",
@@ -217,31 +220,35 @@
"shell": "powershell"
},
{
"filename": "Chocolatey_Update_Installed.bat",
"filename": "Win_Chocolatey_Update_Installed.bat",
"submittedBy": "https://github.com/silversword411",
"name": "Chocolatey Update Installed Apps",
"description": "Update all apps that were installed using Chocolatey.",
"shell": "cmd"
"shell": "cmd",
"category": "TRMM (Win):3rd Party Software>Chocolatey"
},
{
"filename": "AD_Check_And_Enable_AD_Recycle_Bin.ps1",
"filename": "Win_AD_Check_And_Enable_AD_Recycle_Bin.ps1",
"submittedBy": "https://github.com/silversword411",
"name": "AD - Check and Enable AD Recycle Bin",
"description": "Only run on Domain Controllers, checks for Active Directory Recycle Bin and enables if not already enabled",
"shell": "powershell"
"shell": "powershell",
"category": "TRMM (Win):Active Directory"
},
{
"filename": "Check_Events_for_Bluescreens.ps1",
"submittedBy": "https://github.com/dinger1986",
"name": "Event Viewer - Check for Bluescreens",
"description": "This will check for Bluescreen events on your system",
"shell": "powershell"
"shell": "powershell",
"category": "TRMM (Win):Monitoring"
},
{
"filename": "Rename_Computer.ps1",
"filename": "Win_Rename_Computer.ps1",
"submittedBy": "https://github.com/silversword411",
"name": "Rename Computer",
"description": "Rename computer. First parameter will be new PC name. 2nd parameter if yes will auto-reboot machine",
"shell": "powershell"
"shell": "powershell",
"category": "TRMM (Win):Other"
}
]
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.7 on 2021-03-31 01:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('scripts', '0005_auto_20201207_1606'),
]
operations = [
migrations.AddField(
model_name='script',
name='default_timeout',
field=models.PositiveIntegerField(default=90),
),
]

View File

@@ -29,6 +29,7 @@ class Script(BaseAuditModel):
favorite = models.BooleanField(default=False)
category = models.CharField(max_length=100, null=True, blank=True)
code_base64 = models.TextField(null=True, blank=True)
default_timeout = models.PositiveIntegerField(default=90)
def __str__(self):
return self.name

View File

@@ -14,6 +14,7 @@ class ScriptTableSerializer(ModelSerializer):
"shell",
"category",
"favorite",
"default_timeout",
]
@@ -28,6 +29,7 @@ class ScriptSerializer(ModelSerializer):
"category",
"favorite",
"code_base64",
"default_timeout",
]

View File

@@ -36,6 +36,7 @@ class TestScriptViews(TacticalTestCase):
"shell": "powershell",
"category": "New",
"code": "Some Test Code\nnew Line",
"default_timeout": 99,
}
# test without file upload
@@ -55,6 +56,7 @@ class TestScriptViews(TacticalTestCase):
"shell": "cmd",
"category": "New",
"filename": file,
"default_timeout": 4455,
}
# test with file upload
@@ -79,6 +81,7 @@ class TestScriptViews(TacticalTestCase):
"description": "Description Change",
"shell": script.shell,
"code": "Test Code\nAnother Line",
"default_timeout": 13344556,
}
# test edit a userdefined script
@@ -104,6 +107,7 @@ class TestScriptViews(TacticalTestCase):
"shell": script.shell,
"favorite": True,
"code": "Test Code\nAnother Line",
"default_timeout": 54345,
}
# test marking a builtin script as favorite
resp = self.client.put(

View File

@@ -30,6 +30,7 @@ class GetAddScripts(APIView):
"category": request.data["category"],
"description": request.data["description"],
"shell": request.data["shell"],
"default_timeout": request.data["default_timeout"],
"script_type": "userdefined", # force all uploads to be userdefined. built in scripts cannot be edited by user
}

View File

@@ -35,6 +35,14 @@ app.conf.beat_schedule = {
"task": "agents.tasks.auto_self_agent_update_task",
"schedule": crontab(minute=35, hour="*"),
},
"monitor-agents": {
"task": "agents.tasks.monitor_agents_task",
"schedule": crontab(minute="*/7"),
},
"get-wmi": {
"task": "agents.tasks.get_wmi_task",
"schedule": crontab(minute="*/18"),
},
}

View File

@@ -15,7 +15,6 @@ def get_debug_info():
EXCLUDE_PATHS = (
"/natsapi",
"/api/v3",
"/logs/auditlogs",
f"/{settings.ADMIN_URL}",

View File

@@ -15,11 +15,11 @@ EXE_DIR = os.path.join(BASE_DIR, "tacticalrmm/private/exe")
AUTH_USER_MODEL = "accounts.User"
# latest release
TRMM_VERSION = "0.4.29"
TRMM_VERSION = "0.4.32"
# bump this version everytime vue code is changed
# to alert user they need to manually refresh their browser
APP_VER = "0.0.122"
APP_VER = "0.0.125"
# https://github.com/wh1te909/rmmagent
LATEST_AGENT_VER = "1.4.13"
@@ -27,12 +27,14 @@ LATEST_AGENT_VER = "1.4.13"
MESH_VER = "0.7.93"
# for the update script, bump when need to recreate venv or npm install
PIP_VER = "11"
NPM_VER = "10"
PIP_VER = "13"
NPM_VER = "12"
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"
EXE_GEN_URL = "https://exe.tacticalrmm.io/api/v1/exe"
try:
from .local_settings import *
except ImportError:
@@ -61,7 +63,6 @@ INSTALLED_APPS = [
"logs",
"scripts",
"alerts",
"natsapi",
]
if not "AZPIPELINE" in os.environ:

View File

@@ -23,7 +23,6 @@ urlpatterns = [
path("scripts/", include("scripts.urls")),
path("alerts/", include("alerts.urls")),
path("accounts/", include("accounts.urls")),
path("natsapi/", include("natsapi.urls")),
]
if hasattr(settings, "ADMIN_ENABLED") and settings.ADMIN_ENABLED:

View File

@@ -3,11 +3,9 @@ import os
import string
import subprocess
import time
from typing import Union
import pytz
from django.conf import settings
from django.http import HttpResponse
from loguru import logger
from rest_framework import status
from rest_framework.response import Response
@@ -31,130 +29,6 @@ WEEK_DAYS = {
}
def generate_installer_exe(
file_name: str,
goarch: str,
inno: str,
api: str,
client_id: int,
site_id: int,
atype: str,
rdp: int,
ping: int,
power: int,
download_url: str,
token: str,
) -> Union[Response, HttpResponse]:
go_bin = "/usr/local/rmmgo/go/bin/go"
if not os.path.exists(go_bin):
return Response("nogolang", status=status.HTTP_409_CONFLICT)
exe = os.path.join(settings.EXE_DIR, file_name)
if os.path.exists(exe):
try:
os.remove(exe)
except Exception as e:
logger.error(str(e))
cmd = [
"env",
"CGO_ENABLED=0",
"GOOS=windows",
f"GOARCH={goarch}",
go_bin,
"build",
f"-ldflags=\"-s -w -X 'main.Inno={inno}'",
f"-X 'main.Api={api}'",
f"-X 'main.Client={client_id}'",
f"-X 'main.Site={site_id}'",
f"-X 'main.Atype={atype}'",
f"-X 'main.Rdp={rdp}'",
f"-X 'main.Ping={ping}'",
f"-X 'main.Power={power}'",
f"-X 'main.DownloadUrl={download_url}'",
f"-X 'main.Token={token}'\"",
"-o",
exe,
]
build_error = False
gen_error = False
gen = [
"env",
"GOOS=windows",
"CGO_ENABLED=0",
f"GOARCH={goarch}",
go_bin,
"generate",
]
try:
r1 = subprocess.run(
" ".join(gen),
capture_output=True,
shell=True,
cwd=os.path.join(settings.BASE_DIR, "core/goinstaller"),
)
except Exception as e:
gen_error = True
logger.error(str(e))
return Response("genfailed", status=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE)
if r1.returncode != 0:
gen_error = True
if r1.stdout:
logger.error(r1.stdout.decode("utf-8", errors="ignore"))
if r1.stderr:
logger.error(r1.stderr.decode("utf-8", errors="ignore"))
logger.error(f"Go build failed with return code {r1.returncode}")
if gen_error:
return Response("genfailed", status=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE)
try:
r = subprocess.run(
" ".join(cmd),
capture_output=True,
shell=True,
cwd=os.path.join(settings.BASE_DIR, "core/goinstaller"),
)
except Exception as e:
build_error = True
logger.error(str(e))
return Response("buildfailed", status=status.HTTP_412_PRECONDITION_FAILED)
if r.returncode != 0:
build_error = True
if r.stdout:
logger.error(r.stdout.decode("utf-8", errors="ignore"))
if r.stderr:
logger.error(r.stderr.decode("utf-8", errors="ignore"))
logger.error(f"Go build failed with return code {r.returncode}")
if build_error:
return Response("buildfailed", status=status.HTTP_412_PRECONDITION_FAILED)
if settings.DEBUG:
with open(exe, "rb") as f:
response = HttpResponse(
f.read(),
content_type="application/vnd.microsoft.portable-executable",
)
response["Content-Disposition"] = f"inline; filename={file_name}"
return response
else:
response = HttpResponse()
response["Content-Disposition"] = f"attachment; filename={file_name}"
response["X-Accel-Redirect"] = f"/private/exe/{file_name}"
return response
def get_default_timezone():
from core.models import CoreSettings

View File

@@ -19,7 +19,7 @@ def auto_approve_updates_task():
# scheduled task that checks and approves updates daily
agents = Agent.objects.only(
"pk", "version", "last_seen", "overdue_time", "offline_time"
"pk", "agent_id", "version", "last_seen", "overdue_time", "offline_time"
)
for agent in agents:
agent.delete_superseded_updates()
@@ -46,7 +46,7 @@ def auto_approve_updates_task():
def check_agent_update_schedule_task():
# scheduled task that installs updates on agents if enabled
agents = Agent.objects.only(
"pk", "version", "last_seen", "overdue_time", "offline_time"
"pk", "agent_id", "version", "last_seen", "overdue_time", "offline_time"
)
online = [
i

View File

@@ -1,6 +1,6 @@
#!/bin/bash
SCRIPT_VERSION="10"
SCRIPT_VERSION="11"
SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/backup.sh'
GREEN='\033[0;32m'
@@ -8,17 +8,20 @@ YELLOW='\033[1;33m'
BLUE='\033[0;34m'
RED='\033[0;31m'
NC='\033[0m'
THIS_SCRIPT=$(readlink -f "$0")
TMP_FILE=$(mktemp -p "" "rmmbackup_XXXXXXXXXX")
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 backup script is available.${NC}\n"
printf >&2 "${YELLOW}Please download the latest version from ${GREEN}${SCRIPT_URL}${YELLOW} and re-run.${NC}\n"
rm -f $TMP_FILE
exit 1
printf >&2 "${YELLOW}Old backup script detected, downloading and replacing with the latest version...${NC}\n"
wget -q "${SCRIPT_URL}" -O backup.sh
exec ${THIS_SCRIPT}
fi
rm -f $TMP_FILE
if [ $EUID -eq 0 ]; then
echo -ne "\033[0;31mDo NOT run this script as root. Exiting.\e[0m\n"
exit 1
@@ -69,7 +72,7 @@ sudo tar -czvf ${tmp_dir}/nginx/etc-nginx.tar.gz -C /etc/nginx .
sudo tar -czvf ${tmp_dir}/confd/etc-confd.tar.gz -C /etc/conf.d .
sudo cp ${sysd}/rmm.service ${sysd}/celery.service ${sysd}/celerybeat.service ${sysd}/meshcentral.service ${sysd}/nats.service ${sysd}/natsapi.service ${tmp_dir}/systemd/
sudo cp ${sysd}/rmm.service ${sysd}/celery.service ${sysd}/celerybeat.service ${sysd}/meshcentral.service ${sysd}/nats.service ${tmp_dir}/systemd/
cat /rmm/api/tacticalrmm/tacticalrmm/private/log/debug.log | gzip -9 > ${tmp_dir}/rmm/debug.log.gz
cp /rmm/api/tacticalrmm/tacticalrmm/local_settings.py /rmm/api/tacticalrmm/app.ini ${tmp_dir}/rmm/

View File

@@ -1,4 +1,4 @@
FROM node:12-alpine AS builder
FROM node:14-alpine AS builder
WORKDIR /home/node/app

View File

@@ -1,4 +1,4 @@
FROM node:12-alpine
FROM node:14-alpine
WORKDIR /home/node/app

View File

@@ -7,9 +7,6 @@ RUN apk add --no-cache inotify-tools supervisor bash
SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"]
COPY natsapi/bin/nats-api /usr/local/bin/
RUN chmod +x /usr/local/bin/nats-api
COPY docker/containers/tactical-nats/entrypoint.sh /
RUN chmod +x /entrypoint.sh

View File

@@ -2,15 +2,6 @@
set -e
: "${DEV:=0}"
: "${API_CONTAINER:=tactical-backend}"
: "${API_PORT:=80}"
if [ "${DEV}" = 1 ]; then
NATS_CONFIG=/workspace/api/tacticalrmm/nats-rmm.conf
else
NATS_CONFIG="${TACTICAL_DIR}/api/nats-rmm.conf"
fi
sleep 15
until [ -f "${TACTICAL_READY_FILE}" ]; do
echo "waiting for init container to finish install or update..."
@@ -38,11 +29,6 @@ stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
redirect_stderr=true
[program:nats-api]
command=/bin/bash -c "/usr/local/bin/nats-api -debug -api-host http://${API_CONTAINER}:${API_PORT}/natsapi -nats-host tls://${API_HOST}:4222"
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
redirect_stderr=true
EOF
)"

View File

@@ -30,33 +30,30 @@ FROM python:3.9.2-slim
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"
ENV PATH "${VIRTUAL_ENV}/bin:$PATH"
# copy files from repo
COPY api/tacticalrmm ${TACTICAL_TMP_DIR}/api
COPY scripts ${TACTICAL_TMP_DIR}/scripts
# copy go install from build stage
COPY --from=golang:1.16 /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 rsync && \
apt-get install -y --no-install-recommends rsync && \
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
# copy nats-api file
COPY natsapi/bin/nats-api /usr/local/bin/
RUN chmod +x /usr/local/bin/nats-api
# docker init
COPY docker/containers/tactical/entrypoint.sh /

View File

@@ -35,11 +35,7 @@ if [ "$1" = 'tactical-init' ]; then
test -f "${TACTICAL_READY_FILE}" && rm "${TACTICAL_READY_FILE}"
# copy container data to volume
# bad
#cp -af ${TACTICAL_TMP_DIR}/. ${TACTICAL_DIR}/
# good
rsync -a --no-perms --no-owner --delete "${TACTICAL_TMP_DIR}/" "${TACTICAL_DIR}/"
rsync -a --no-perms --no-owner --delete --exclude "tmp/*" --exclude "certs/*" --exclude="api/tacticalrmm/private/*" "${TACTICAL_TMP_DIR}/" "${TACTICAL_DIR}/"
until (echo > /dev/tcp/"${POSTGRES_HOST}"/"${POSTGRES_PORT}") &> /dev/null; do
echo "waiting for postgresql container to be ready..."

View File

@@ -0,0 +1,19 @@
# Django Admin
!!!warning
Do not use the django admin unless you really know what you're doing.<br />You should never need to access it unless you are familiar with django or are instructed to do something here by one of the developers.
The django admin is basically a web interface for the postgres database.
As of Tactical RMM v0.4.19, the django admin is disabled by default.
To enable it, edit `/rmm/api/tacticalrmm/tacticalrmm/local_settings.py` and change `ADMIN_ENABLED` from `False` to `True` then `sudo systemctl restart rmm`
Login to the django admin using the same credentials as your normal web ui login.
If you did not save the django admin url (which was printed out at the end of the install script), check the `local_settings.py` file referenced above for the `ADMIN_URL` variable. Then simply append the value of this variable to your api domain (`https://api.yourdomain.com/`) to get the full url.
Example of a full django admin url:
```
https://api.example.com/JwboKNYb3v6K93Fvtcz0G3vUM17LMTSZggOUAxa97jQfAh0P5xosEk7u2PPkjEfdOtucUp/
```

View File

@@ -22,12 +22,20 @@ apt install -y wget curl sudo
apt -y upgrade
```
If a new kernel is installed, then reboot the server with the `reboot` command<br/><br/>
Create a linux user to run the rmm and add it to the sudoers group.<br/>For this example we'll be using a user named `tactical` but feel free to create whatever name you want.
Create a linux user named `tactical` to run the rmm and add it to the sudoers group.<br/>
**For Ubuntu**:
```bash
adduser tactical
usermod -a -G sudo tactical
```
**For Debian**:
```bash
useradd -m -s /bin/bash tactical
usermod -a -G sudo tactical
```
!!!tip
[Enable passwordless sudo to make your life easier](https://linuxconfig.org/configure-sudo-without-password-on-ubuntu-20-04-focal-fossa-linux)

View File

@@ -69,7 +69,6 @@ sudo systemctl status celery
sudo systemctl status celerybeat
sudo systemctl status nginx
sudo systemctl status nats
sudo systemctl status natsapi
sudo systemctl status meshcentral
sudo systemctl status mongod
sudo systemctl status postgresql

View File

@@ -15,6 +15,7 @@ nav:
- "Maintenance Mode": functions/maintenance_mode.md
- "Alerting": alerting.md
- "User Interface Preferences": functions/user_ui.md
- "Django Admin": functions/django_admin.md
- Backup: backup.md
- Restore: restore.md
- Troubleshooting: troubleshooting.md

2
go.mod
View File

@@ -3,9 +3,7 @@ module github.com/wh1te909/tacticalrmm
go 1.16
require (
github.com/go-resty/resty/v2 v2.5.0
github.com/nats-io/nats.go v1.10.1-0.20210107160453-a133396829fc
github.com/ugorji/go/codec v1.2.4
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect
golang.org/x/sys v0.0.0-20210122235752-a8b976e07c7b // indirect
)

9
go.sum
View File

@@ -1,5 +1,3 @@
github.com/go-resty/resty/v2 v2.5.0 h1:WFb5bD49/85PO7WgAjZ+/TJQ+Ty1XOcWEfD1zIFCM1c=
github.com/go-resty/resty/v2 v2.5.0/go.mod h1:B88+xCTEwvfD94NOuE6GS1wMlnoKNY8eEiNizfNwOwA=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
@@ -45,22 +43,15 @@ golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210122235752-a8b976e07c7b h1:HSSdksA3iHk8fuZz7C7+A6tDgtIRF+7FSXu5TgK09I8=
golang.org/x/sys v0.0.0-20210122235752-a8b976e07c7b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI=
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=

View File

@@ -1,6 +1,6 @@
#!/bin/bash
SCRIPT_VERSION="43"
SCRIPT_VERSION="44"
SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/install.sh'
sudo apt install -y curl wget dirmngr gnupg lsb-release
@@ -181,17 +181,6 @@ CERT_PUB_KEY=/etc/letsencrypt/live/${rootdomain}/fullchain.pem
sudo chown ${USER}:${USER} -R /etc/letsencrypt
sudo chmod 775 -R /etc/letsencrypt
print_green 'Installing golang'
sudo mkdir -p /usr/local/rmmgo
go_tmp=$(mktemp -d -t rmmgo-XXXXXXXXXX)
wget https://golang.org/dl/go1.16.2.linux-amd64.tar.gz -P ${go_tmp}
tar -xzf ${go_tmp}/go1.16.2.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)
@@ -212,7 +201,7 @@ sudo sed -i 's/worker_connections.*/worker_connections 2048;/g' /etc/nginx/nginx
print_green 'Installing NodeJS'
curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash -
sudo apt update
sudo apt install -y gcc g++ make
sudo apt install -y nodejs
@@ -376,11 +365,6 @@ EOF
)"
echo "${localvars}" > /rmm/api/tacticalrmm/tacticalrmm/local_settings.py
/usr/local/rmmgo/go/bin/go get github.com/josephspurrier/goversioninfo/cmd/goversioninfo
sudo cp /rmm/api/tacticalrmm/core/goinstaller/bin/goversioninfo /usr/local/bin/
sudo chown ${USER}:${USER} /usr/local/bin/goversioninfo
sudo chmod +x /usr/local/bin/goversioninfo
sudo cp /rmm/natsapi/bin/nats-api /usr/local/bin
sudo chown ${USER}:${USER} /usr/local/bin/nats-api
sudo chmod +x /usr/local/bin/nats-api
@@ -482,26 +466,6 @@ EOF
)"
echo "${natsservice}" | sudo tee /etc/systemd/system/nats.service > /dev/null
natsapi="$(cat << EOF
[Unit]
Description=Tactical NATS API
After=network.target rmm.service nginx.service nats.service
[Service]
Type=simple
ExecStart=/usr/local/bin/nats-api
User=${USER}
Group=${USER}
Restart=always
RestartSec=5s
[Install]
WantedBy=multi-user.target
EOF
)"
echo "${natsapi}" | sudo tee /etc/systemd/system/natsapi.service > /dev/null
nginxrmm="$(cat << EOF
server_tokens off;
@@ -808,7 +772,6 @@ 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
sudo systemctl enable natsapi.service
cd /rmm/api/tacticalrmm
source /rmm/api/env/bin/activate
python manage.py initial_db_setup
@@ -820,7 +783,7 @@ sudo systemctl start nats.service
sed -i 's/ADMIN_ENABLED = True/ADMIN_ENABLED = False/g' /rmm/api/tacticalrmm/tacticalrmm/local_settings.py
print_green 'Restarting services'
for i in rmm.service celery.service celerybeat.service natsapi.service
for i in rmm.service celery.service celerybeat.service
do
sudo systemctl stop ${i}
sudo systemctl start ${i}

17
main.go
View File

@@ -1,6 +1,6 @@
package main
// env CGO_ENABLED=0 go build -v -a -ldflags "-s -w" -o nats-api
// env CGO_ENABLED=0 go build -ldflags "-s -w" -o nats-api
import (
"flag"
@@ -9,13 +9,12 @@ import (
"github.com/wh1te909/tacticalrmm/natsapi"
)
var version = "1.1.0"
var version = "2.0.0"
func main() {
ver := flag.Bool("version", false, "Prints version")
apiHost := flag.String("api-host", "", "django full base url")
natsHost := flag.String("nats-host", "", "nats full connection string")
debug := flag.Bool("debug", false, "Debug")
mode := flag.String("m", "", "Mode")
config := flag.String("c", "", "config file")
flag.Parse()
if *ver {
@@ -23,5 +22,11 @@ func main() {
return
}
api.Listen(*apiHost, *natsHost, version, *debug)
switch *mode {
case "monitor":
api.MonitorAgents(*config)
case "wmi":
api.GetWMI(*config)
}
}

View File

@@ -1,82 +0,0 @@
package api
import (
"bufio"
"errors"
"fmt"
"log"
"os"
"strings"
"sync"
"time"
"github.com/go-resty/resty/v2"
nats "github.com/nats-io/nats.go"
)
var rClient = resty.New()
func getAPI(apihost, natshost string) (string, string, error) {
if apihost != "" && natshost != "" {
return apihost, natshost, nil
}
f, err := os.Open(`/etc/nginx/sites-available/rmm.conf`)
if err != nil {
return "", "", err
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
if strings.Contains(scanner.Text(), "server_name") && !strings.Contains(scanner.Text(), "301") {
r := strings.NewReplacer("server_name", "", ";", "")
s := strings.ReplaceAll(r.Replace(scanner.Text()), " ", "")
return fmt.Sprintf("https://%s/natsapi", s), fmt.Sprintf("tls://%s:4222", s), nil
}
}
return "", "", errors.New("unable to parse api from nginx conf")
}
func Listen(apihost, natshost, version string, debug bool) {
api, natsurl, err := getAPI(apihost, natshost)
if err != nil {
log.Fatalln(err)
}
log.Printf("Tactical Nats API Version %s\n", version)
log.Println("Api base url: ", api)
log.Println("Nats connection url: ", natsurl)
rClient.SetHostURL(api)
rClient.SetTimeout(10 * time.Second)
natsinfo, err := rClient.R().SetResult(&NatsInfo{}).Get("/natsinfo/")
if err != nil {
log.Fatalln(err)
}
if natsinfo.IsError() {
log.Fatalln(natsinfo.String())
}
opts := []nats.Option{
nats.Name("TacticalRMM"),
nats.UserInfo(natsinfo.Result().(*NatsInfo).User,
natsinfo.Result().(*NatsInfo).Password),
nats.ReconnectWait(time.Second * 5),
nats.RetryOnFailedConnect(true),
nats.MaxReconnects(-1),
nats.ReconnectBufSize(-1),
}
nc, err := nats.Connect(natsurl, opts...)
if err != nil {
log.Fatalln(err)
}
var wg sync.WaitGroup
wg.Add(1)
go getWMI(rClient, nc)
go monitorAgents(rClient, nc)
wg.Wait()
}

Binary file not shown.

View File

@@ -1,15 +1,42 @@
package api
import (
"encoding/json"
"io/ioutil"
"log"
"math/rand"
"sync"
"time"
"github.com/go-resty/resty/v2"
nats "github.com/nats-io/nats.go"
"github.com/ugorji/go/codec"
)
func monitorAgents(c *resty.Client, nc *nats.Conn) {
type JsonFile struct {
Agents []string `json:"agents"`
Key string `json:"key"`
NatsURL string `json:"natsurl"`
}
type Recovery struct {
Func string `json:"func"`
Data map[string]string `json:"payload"`
}
func setupNatsOptions(key string) []nats.Option {
opts := []nats.Option{
nats.Name("TacticalRMM"),
nats.UserInfo("tacticalrmm", key),
nats.ReconnectWait(time.Second * 2),
nats.RetryOnFailedConnect(true),
nats.MaxReconnects(3),
nats.ReconnectBufSize(-1),
}
return opts
}
func MonitorAgents(file string) {
var result JsonFile
var payload, recPayload []byte
var mh codec.MsgpackHandle
mh.RawToString = true
@@ -22,56 +49,80 @@ func monitorAgents(c *resty.Client, nc *nats.Conn) {
Data: map[string]string{"mode": "tacagent"},
})
tick := time.NewTicker(7 * time.Minute)
for range tick.C {
var wg sync.WaitGroup
agentids, _ := c.R().SetResult(&AgentIDS{}).Get("/offline/agents/")
ids := agentids.Result().(*AgentIDS).IDs
wg.Add(len(ids))
var resp string
for _, id := range ids {
go func(id string, nc *nats.Conn, wg *sync.WaitGroup, c *resty.Client) {
defer wg.Done()
out, err := nc.Request(id, payload, 1*time.Second)
if err != nil {
return
}
dec := codec.NewDecoderBytes(out.Data, &mh)
if err := dec.Decode(&resp); err == nil {
// if the agent is respoding to pong from the rpc service but is not showing as online (handled by tacticalagent service)
// then tacticalagent service is hung. forcefully restart it
if resp == "pong" {
nc.Publish(id, recPayload)
p := map[string]string{"agentid": id}
c.R().SetBody(p).Post("/logcrash/")
}
}
}(id, nc, &wg, c)
}
wg.Wait()
jret, _ := ioutil.ReadFile(file)
err := json.Unmarshal(jret, &result)
if err != nil {
log.Fatalln(err)
}
opts := setupNatsOptions(result.Key)
nc, err := nats.Connect(result.NatsURL, opts...)
if err != nil {
log.Fatalln(err)
}
defer nc.Close()
var wg sync.WaitGroup
var resp string
wg.Add(len(result.Agents))
for _, id := range result.Agents {
go func(id string, nc *nats.Conn, wg *sync.WaitGroup) {
defer wg.Done()
out, err := nc.Request(id, payload, 1*time.Second)
if err != nil {
return
}
dec := codec.NewDecoderBytes(out.Data, &mh)
if err := dec.Decode(&resp); err == nil {
// if the agent is respoding to pong from the rpc service but is not showing as online (handled by tacticalagent service)
// then tacticalagent service is hung. forcefully restart it
if resp == "pong" {
nc.Publish(id, recPayload)
}
}
}(id, nc, &wg)
}
wg.Wait()
}
func getWMI(c *resty.Client, nc *nats.Conn) {
func GetWMI(file string) {
var result JsonFile
var payload []byte
var mh codec.MsgpackHandle
mh.RawToString = true
ret := codec.NewEncoderBytes(&payload, new(codec.MsgpackHandle))
ret.Encode(map[string]string{"func": "wmi"})
tick := time.NewTicker(18 * time.Minute)
for range tick.C {
agentids, _ := c.R().SetResult(&AgentIDS{}).Get("/online/agents/")
ids := agentids.Result().(*AgentIDS).IDs
chunks := makeChunks(ids, 40)
for _, id := range chunks {
for _, chunk := range id {
nc.Publish(chunk, payload)
time.Sleep(time.Duration(randRange(50, 400)) * time.Millisecond)
}
time.Sleep(15 * time.Second)
}
jret, _ := ioutil.ReadFile(file)
err := json.Unmarshal(jret, &result)
if err != nil {
log.Fatalln(err)
}
opts := setupNatsOptions(result.Key)
nc, err := nats.Connect(result.NatsURL, opts...)
if err != nil {
log.Fatalln(err)
}
defer nc.Close()
var wg sync.WaitGroup
wg.Add(len(result.Agents))
for _, id := range result.Agents {
go func(id string, nc *nats.Conn, wg *sync.WaitGroup) {
defer wg.Done()
time.Sleep(time.Duration(randRange(0, 20)) * time.Second)
nc.Publish(id, payload)
}(id, nc, &wg)
}
wg.Wait()
}
func randRange(min, max int) int {
rand.Seed(time.Now().UnixNano())
return rand.Intn(max-min) + min
}

View File

@@ -1,15 +0,0 @@
package api
type NatsInfo struct {
User string `json:"user"`
Password string `json:"password"`
}
type AgentIDS struct {
IDs []string `json:"agent_ids"`
}
type Recovery struct {
Func string `json:"func"`
Data map[string]string `json:"payload"`
}

View File

@@ -1,23 +0,0 @@
package api
import (
"math/rand"
"time"
)
func makeChunks(ids []string, chunkSize int) [][]string {
var chunks [][]string
for i := 0; i < len(ids); i += chunkSize {
end := i + chunkSize
if end > len(ids) {
end = len(ids)
}
chunks = append(chunks, ids[i:end])
}
return chunks
}
func randRange(min, max int) int {
rand.Seed(time.Now().UnixNano())
return rand.Intn(max-min) + min
}

View File

@@ -1,6 +1,6 @@
#!/bin/bash
SCRIPT_VERSION="21"
SCRIPT_VERSION="22"
SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/master/restore.sh'
sudo apt update
@@ -103,17 +103,7 @@ fi
# prevents logging issues with some VPS providers like Vultr if this is a freshly provisioned instance that hasn't been rebooted yet
sudo systemctl restart systemd-journald.service
print_green 'Installing golang'
sudo apt update
sudo mkdir -p /usr/local/rmmgo
go_tmp=$(mktemp -d -t rmmgo-XXXXXXXXXX)
wget https://golang.org/dl/go1.16.2.linux-amd64.tar.gz -P ${go_tmp}
tar -xzf ${go_tmp}/go1.16.2.linux-amd64.tar.gz -C ${go_tmp}
sudo mv ${go_tmp}/go /usr/local/rmmgo/
rm -rf ${go_tmp}
print_green 'Downloading NATS'
@@ -129,7 +119,7 @@ rm -rf ${nats_tmp}
print_green 'Installing NodeJS'
curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash -
sudo apt update
sudo apt install -y gcc g++ make
sudo apt install -y nodejs
@@ -276,11 +266,6 @@ gzip -d $tmp_dir/rmm/debug.log.gz
cp $tmp_dir/rmm/debug.log /rmm/api/tacticalrmm/tacticalrmm/private/log/
cp $tmp_dir/rmm/mesh*.exe /rmm/api/tacticalrmm/tacticalrmm/private/exe/
/usr/local/rmmgo/go/bin/go get github.com/josephspurrier/goversioninfo/cmd/goversioninfo
sudo cp /rmm/api/tacticalrmm/core/goinstaller/bin/goversioninfo /usr/local/bin/
sudo chown ${USER}:${USER} /usr/local/bin/goversioninfo
sudo chmod +x /usr/local/bin/goversioninfo
sudo cp /rmm/natsapi/bin/nats-api /usr/local/bin
sudo chown ${USER}:${USER} /usr/local/bin/nats-api
sudo chmod +x /usr/local/bin/nats-api
@@ -350,10 +335,6 @@ print_green 'Starting meshcentral'
sudo systemctl enable meshcentral
sudo systemctl start meshcentral
print_green 'Starting natsapi'
sudo systemctl enable natsapi.service
sudo systemctl start natsapi.service
printf >&2 "${YELLOW}%0.s*${NC}" {1..80}
printf >&2 "\n\n"
printf >&2 "${YELLOW}Restore complete!${NC}\n\n"

View File

@@ -20,4 +20,4 @@ Try {
Catch {
Write-Host "Script Check Failed"
Exit 1001
}
}

View File

@@ -1,97 +1,97 @@
## Copied from https://github.com/ThatsNASt/tacticalrmm to add to new pull request for https://github.com/wh1te909/tacticalrmm
function Log-Message {
Param
(
[Parameter(Mandatory = $true, Position = 0)]
[string]$LogMessage,
[Parameter(Mandatory = $false, Position = 1)]
[string]$LogFile,
[Parameter(Mandatory = $false, Position = 2)]
$Echo
)
if ($LogFile) {
Write-Output ("{0} - {1}" -f (Get-Date), $LogMessage) | Out-File -Append $LogFile
if ($Echo) {
Write-Output ("{0} - {1}" -f (Get-Date), $LogMessage)
}
}
Else {
Write-Output ("{0} - {1}" -f (Get-Date), $LogMessage)
}
}
$log = "BitlockerReport.txt"
#Find BL info
$mbde = [string](manage-bde -status)
$mbdeProt = (manage-bde -protectors -get c: | Select-Object -Skip 6)
#Dig out the recovery password, check for PIN
ForEach ($line in $mbdeProt) {
if ($line -like "******-******-******-******-******-******-******-******") {
$RecoveryPassword = $line.Trim()
}
if ($line -like "*TPM And PIN:*") {
$PIN = $true
}
}
#Determine BL status
if ($mbde.Contains("Fully Decrypted")) {
$Encrypted = "No"
}
if ($mbde.Contains("Fully Encrypted")) {
$Encrypted = "Yes"
}
if ($mbde.Contains("Encryption in Progress")) {
$Encrypted = "InProgress"
}
if ($mbde.Contains("Decryption in Progress")) {
$Encrypted = "InProgressNo"
}
#Check for recovery password, report if found.
if ($RecoveryPassword) {
Try {
Log-Message "RP: $RecoveryPassword" $log e -ErrorAction Stop
}
#Catch for recovery password in place but encryption not active
Catch {
Log-Message "Could not retrieve recovery password, but it is enabled." $log e
}
}
if (!$RecoveryPassword) {
Log-Message "No Recovery Password found." $log e
}
#Try to make a summary for common situations
if ($Encrypted -eq "No" -and !$RecoveryPassword) {
Log-Message "WARNING: Decrypted, no password." $log e
exit 2001
}
if ($Encrypted -eq "No" -and $RecoveryPassword) {
Log-Message "WARNING: Decrypted, password set. Interrupted process?" $log e
exit 2002
}
if ($Encrypted -eq "Yes" -and !$RecoveryPassword) {
Log-Message "WARNING: Encrypted, no password." $log e
exit 2000
}
if ($Encrypted -eq "InProgress" -and $RecoveryPassword) {
Log-Message "WARNING: Encryption in progress, password set." $log e
exit 3000
}
if ($Encrypted -eq "InProgress" -and !$RecoveryPassword) {
Log-Message "WARNING: Encryption in progress, no password." $log e
exit 3001
}
if ($Encrypted -eq "InProgressNo") {
Log-Message "WARNING: Decryption in progress" $log e
exit 3002
}
if ($Encrypted -eq "Yes" -and $RecoveryPassword -and !$PIN) {
Log-Message "WARNING: Encrypted, PIN DISABLED, password is set." $log e
exit 3003
}
if ($Encrypted -eq "Yes" -and $RecoveryPassword -and $PIN -eq $true) {
Log-Message "SUCCESS: Encrypted, PIN enabled, password is set." $log e
Write-Host "Script check passed"
exit 0
## Copied from https://github.com/ThatsNASt/tacticalrmm to add to new pull request for https://github.com/wh1te909/tacticalrmm
function Log-Message {
Param
(
[Parameter(Mandatory = $true, Position = 0)]
[string]$LogMessage,
[Parameter(Mandatory = $false, Position = 1)]
[string]$LogFile,
[Parameter(Mandatory = $false, Position = 2)]
$Echo
)
if ($LogFile) {
Write-Output ("{0} - {1}" -f (Get-Date), $LogMessage) | Out-File -Append $LogFile
if ($Echo) {
Write-Output ("{0} - {1}" -f (Get-Date), $LogMessage)
}
}
Else {
Write-Output ("{0} - {1}" -f (Get-Date), $LogMessage)
}
}
$log = "BitlockerReport.txt"
#Find BL info
$mbde = [string](manage-bde -status)
$mbdeProt = (manage-bde -protectors -get c: | Select-Object -Skip 6)
#Dig out the recovery password, check for PIN
ForEach ($line in $mbdeProt) {
if ($line -like "******-******-******-******-******-******-******-******") {
$RecoveryPassword = $line.Trim()
}
if ($line -like "*TPM And PIN:*") {
$PIN = $true
}
}
#Determine BL status
if ($mbde.Contains("Fully Decrypted")) {
$Encrypted = "No"
}
if ($mbde.Contains("Fully Encrypted")) {
$Encrypted = "Yes"
}
if ($mbde.Contains("Encryption in Progress")) {
$Encrypted = "InProgress"
}
if ($mbde.Contains("Decryption in Progress")) {
$Encrypted = "InProgressNo"
}
#Check for recovery password, report if found.
if ($RecoveryPassword) {
Try {
Log-Message "RP: $RecoveryPassword" $log e -ErrorAction Stop
}
#Catch for recovery password in place but encryption not active
Catch {
Log-Message "Could not retrieve recovery password, but it is enabled." $log e
}
}
if (!$RecoveryPassword) {
Log-Message "No Recovery Password found." $log e
}
#Try to make a summary for common situations
if ($Encrypted -eq "No" -and !$RecoveryPassword) {
Log-Message "WARNING: Decrypted, no password." $log e
exit 2001
}
if ($Encrypted -eq "No" -and $RecoveryPassword) {
Log-Message "WARNING: Decrypted, password set. Interrupted process?" $log e
exit 2002
}
if ($Encrypted -eq "Yes" -and !$RecoveryPassword) {
Log-Message "WARNING: Encrypted, no password." $log e
exit 2000
}
if ($Encrypted -eq "InProgress" -and $RecoveryPassword) {
Log-Message "WARNING: Encryption in progress, password set." $log e
exit 3000
}
if ($Encrypted -eq "InProgress" -and !$RecoveryPassword) {
Log-Message "WARNING: Encryption in progress, no password." $log e
exit 3001
}
if ($Encrypted -eq "InProgressNo") {
Log-Message "WARNING: Decryption in progress" $log e
exit 3002
}
if ($Encrypted -eq "Yes" -and $RecoveryPassword -and !$PIN) {
Log-Message "WARNING: Encrypted, PIN DISABLED, password is set." $log e
exit 3003
}
if ($Encrypted -eq "Yes" -and $RecoveryPassword -and $PIN -eq $true) {
Log-Message "SUCCESS: Encrypted, PIN enabled, password is set." $log e
Write-Host "Script check passed"
exit 0
}

194
update.sh
View File

@@ -1,12 +1,13 @@
#!/bin/bash
SCRIPT_VERSION="114"
SCRIPT_VERSION="116"
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'
RED='\033[0;31m'
NC='\033[0m'
THIS_SCRIPT=$(readlink -f "$0")
TMP_FILE=$(mktemp -p "" "rmmupdate_XXXXXXXXXX")
curl -s -L "${SCRIPT_URL}" > ${TMP_FILE}
@@ -15,9 +16,7 @@ NEW_VER=$(grep "^SCRIPT_VERSION" "$TMP_FILE" | awk -F'[="]' '{print $3}')
if [ "${SCRIPT_VERSION}" -ne "${NEW_VER}" ]; then
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
exec ${THIS_SCRIPT}
fi
rm -f $TMP_FILE
@@ -27,13 +26,13 @@ if [[ $* == *--force* ]]; then
force=true
fi
sudo apt update
if [ $EUID -eq 0 ]; then
echo -ne "\033[0;31mDo NOT run this script as root. Exiting.\e[0m\n"
exit 1
fi
sudo apt update
strip="User="
ORIGUSER=$(grep ${strip} /etc/systemd/system/rmm.service | sed -e "s/^${strip}//")
@@ -42,78 +41,12 @@ if [ "$ORIGUSER" != "$USER" ]; then
exit 1
fi
CHECK_030_MIGRATION=$(grep natsapi /etc/nginx/sites-available/rmm.conf)
if ! [[ $CHECK_030_MIGRATION ]]; then
printf >&2 "${RED}Manual configuration changes required before continuing.${NC}\n"
printf >&2 "${RED}Please follow the steps here and then re-run this update script.${NC}\n"
printf >&2 "${GREEN}https://github.com/wh1te909/tacticalrmm/blob/develop/docs/migration-0.3.0.md${NC}\n"
CHECK_TOO_OLD=$(grep natsapi /etc/nginx/sites-available/rmm.conf)
if ! [[ $CHECK_TOO_OLD ]]; then
printf >&2 "${RED}Your version of TRMM is no longer supported. Refusing to update.${NC}\n"
exit 1
fi
CHECK_CELERY_V2=$(grep V2 /etc/systemd/system/celery.service)
if ! [[ $CHECK_CELERY_V2 ]]; then
printf >&2 "${GREEN}Updating celery.service${NC}\n"
sudo systemctl stop celery.service
sudo rm -f /etc/systemd/system/celery.service
celeryservice="$(cat << EOF
[Unit]
Description=Celery Service V2
After=network.target redis-server.service postgresql.service
[Service]
Type=forking
User=${USER}
Group=${USER}
EnvironmentFile=/etc/conf.d/celery.conf
WorkingDirectory=/rmm/api/tacticalrmm
ExecStart=/bin/sh -c '\${CELERY_BIN} -A \$CELERY_APP multi start \$CELERYD_NODES --pidfile=\${CELERYD_PID_FILE} --logfile=\${CELERYD_LOG_FILE} --loglevel="\${CELERYD_LOG_LEVEL}" \$CELERYD_OPTS'
ExecStop=/bin/sh -c '\${CELERY_BIN} multi stopwait \$CELERYD_NODES --pidfile=\${CELERYD_PID_FILE} --loglevel="\${CELERYD_LOG_LEVEL}"'
ExecReload=/bin/sh -c '\${CELERY_BIN} -A \$CELERY_APP multi restart \$CELERYD_NODES --pidfile=\${CELERYD_PID_FILE} --logfile=\${CELERYD_LOG_FILE} --loglevel="\${CELERYD_LOG_LEVEL}" \$CELERYD_OPTS'
Restart=always
RestartSec=10s
[Install]
WantedBy=multi-user.target
EOF
)"
echo "${celeryservice}" | sudo tee /etc/systemd/system/celery.service > /dev/null
sudo systemctl daemon-reload
sudo systemctl enable celery.service
fi
CHECK_CELERYBEAT_V2=$(grep V2 /etc/systemd/system/celerybeat.service)
if ! [[ $CHECK_CELERYBEAT_V2 ]]; then
printf >&2 "${GREEN}Updating celerybeat.service${NC}\n"
sudo systemctl stop celerybeat.service
sudo rm -f /etc/systemd/system/celerybeat.service
celerybeatservice="$(cat << EOF
[Unit]
Description=Celery Beat Service V2
After=network.target redis-server.service postgresql.service
[Service]
Type=simple
User=${USER}
Group=${USER}
EnvironmentFile=/etc/conf.d/celery.conf
WorkingDirectory=/rmm/api/tacticalrmm
ExecStart=/bin/sh -c '\${CELERY_BIN} -A \${CELERY_APP} beat --pidfile=\${CELERYBEAT_PID_FILE} --logfile=\${CELERYBEAT_LOG_FILE} --loglevel=\${CELERYD_LOG_LEVEL}'
Restart=always
RestartSec=10s
[Install]
WantedBy=multi-user.target
EOF
)"
echo "${celerybeatservice}" | sudo tee /etc/systemd/system/celerybeat.service > /dev/null
sudo systemctl daemon-reload
sudo systemctl enable celerybeat.service
fi
TMP_SETTINGS=$(mktemp -p "" "rmmsettings_XXXXXXXXXX")
curl -s -L "${LATEST_SETTINGS_URL}" > ${TMP_SETTINGS}
SETTINGS_FILE="/rmm/api/tacticalrmm/tacticalrmm/settings.py"
@@ -134,39 +67,15 @@ LATEST_NPM_VER=$(grep "^NPM_VER" "$TMP_SETTINGS" | awk -F'[= "]' '{print $5}')
CURRENT_PIP_VER=$(grep "^PIP_VER" "$SETTINGS_FILE" | awk -F'[= "]' '{print $5}')
CURRENT_NPM_VER=$(grep "^NPM_VER" "$SETTINGS_FILE" | awk -F'[= "]' '{print $5}')
if [ ! -f /etc/systemd/system/natsapi.service ]; then
natsapi="$(cat << EOF
[Unit]
Description=Tactical NATS API
After=network.target rmm.service nginx.service nats.service
[Service]
Type=simple
ExecStart=/usr/local/bin/nats-api
User=${USER}
Group=${USER}
Restart=always
RestartSec=5s
[Install]
WantedBy=multi-user.target
EOF
)"
echo "${natsapi}" | sudo tee /etc/systemd/system/natsapi.service > /dev/null
sudo systemctl daemon-reload
sudo systemctl enable natsapi.service
fi
if [ -f /etc/systemd/system/celery-winupdate.service ]; then
printf >&2 "${GREEN}Removing celery-winupdate.service${NC}\n"
sudo systemctl stop celery-winupdate.service
sudo systemctl disable celery-winupdate.service
sudo rm -f /etc/systemd/system/celery-winupdate.service
if [ -f /etc/systemd/system/natsapi.service ]; then
printf >&2 "${GREEN}Removing natsapi.service${NC}\n"
sudo systemctl stop natsapi.service
sudo systemctl disable natsapi.service
sudo rm -f /etc/systemd/system/natsapi.service
sudo systemctl daemon-reload
fi
for i in nginx nats natsapi rmm celery celerybeat
for i in nginx nats rmm celery celerybeat
do
printf >&2 "${GREEN}Stopping ${i} service...${NC}\n"
sudo systemctl stop ${i}
@@ -206,30 +115,12 @@ EOF
)"
echo "${uwsgini}" > /rmm/api/tacticalrmm/app.ini
# forgot to add this in install script. catch any installs that don't have it enabled and enable it
sudo systemctl enable natsapi.service
CHECK_NGINX_WORKER_CONN=$(grep "worker_connections 2048" /etc/nginx/nginx.conf)
if ! [[ $CHECK_NGINX_WORKER_CONN ]]; then
printf >&2 "${GREEN}Changing nginx worker connections to 2048${NC}\n"
sudo sed -i 's/worker_connections.*/worker_connections 2048;/g' /etc/nginx/nginx.conf
fi
CHECK_HAS_GO116=$(/usr/local/rmmgo/go/bin/go version | grep go1.16)
if ! [[ $CHECK_HAS_GO116 ]]; then
printf >&2 "${GREEN}Updating golang to version 1.16${NC}\n"
sudo rm -rf /home/${USER}/go/
sudo rm -rf /usr/local/rmmgo/
sudo mkdir -p /usr/local/rmmgo
go_tmp=$(mktemp -d -t rmmgo-XXXXXXXXXX)
wget https://golang.org/dl/go1.16.linux-amd64.tar.gz -P ${go_tmp}
tar -xzf ${go_tmp}/go1.16.linux-amd64.tar.gz -C ${go_tmp}
sudo mv ${go_tmp}/go /usr/local/rmmgo/
rm -rf ${go_tmp}
sudo chown -R $USER:$GROUP /home/${USER}/.cache
fi
HAS_PY39=$(which python3.9)
if ! [[ $HAS_PY39 ]]; then
printf >&2 "${GREEN}Updating to Python 3.9${NC}\n"
@@ -259,6 +150,25 @@ if ! [[ $HAS_NATS220 ]]; then
rm -rf ${nats_tmp}
fi
HAS_NODE14=$(/usr/bin/node --version | grep v14)
if ! [[ $HAS_NODE14 ]]; then
printf >&2 "${GREEN}Updating NodeJS to v14${NC}\n"
rm -rf /rmm/web/node_modules
sudo systemctl stop meshcentral
sudo apt remove -y nodejs
sudo rm -rf /usr/lib/node_modules
sudo rm -rf /home/${USER}/.npm/*
curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash -
sudo apt update
sudo apt install -y nodejs
sudo npm install -g npm
sudo chown ${USER}:${USER} -R /meshcentral
cd /meshcentral
rm -rf node_modules/
npm install meshcentral@${LATEST_MESH_VER}
sudo systemctl start meshcentral
fi
sudo npm install -g npm
cd /rmm
@@ -289,42 +199,6 @@ EOF
echo "${adminenabled}" | tee --append /rmm/api/tacticalrmm/tacticalrmm/local_settings.py > /dev/null
fi
CHECK_REMOVE_SALT=$(grep KEEP_SALT /rmm/api/tacticalrmm/tacticalrmm/local_settings.py)
if ! [[ $CHECK_REMOVE_SALT ]]; then
printf >&2 "${YELLOW}This update removes salt from the rmm${NC}\n"
printf >&2 "${YELLOW}You may continue to use salt on existing agents, but there will not be any more integration with tacticalrmm, and new agents will not install the salt-minion${NC}\n"
until [[ $rmsalt =~ (y|n) ]]; do
echo -ne "${YELLOW}Would you like to remove salt? (recommended) [y/n]${NC}: "
read rmsalt
done
if [[ $rmsalt == "y" ]]; then
keepsalt="$(cat << EOF
KEEP_SALT = False
EOF
)"
else
keepsalt="$(cat << EOF
KEEP_SALT = True
EOF
)"
fi
echo "${keepsalt}" | tee --append /rmm/api/tacticalrmm/tacticalrmm/local_settings.py > /dev/null
if [[ $rmsalt == "y" ]]; then
printf >&2 "${GREEN}Removing salt-master and salt-api${NC}\n"
for i in salt-api salt-master; do sudo systemctl stop $i; sudo systemctl disable $i; done
sudo apt remove -y --purge salt-master salt-api salt-common
else
sudo systemctl stop salt-api
sudo systemctl disable salt-api
fi
fi
/usr/local/rmmgo/go/bin/go get github.com/josephspurrier/goversioninfo/cmd/goversioninfo
sudo cp /rmm/api/tacticalrmm/core/goinstaller/bin/goversioninfo /usr/local/bin/
sudo chown ${USER}:${USER} /usr/local/bin/goversioninfo
sudo chmod +x /usr/local/bin/goversioninfo
sudo cp /rmm/natsapi/bin/nats-api /usr/local/bin
sudo chown ${USER}:${USER} /usr/local/bin/nats-api
sudo chmod +x /usr/local/bin/nats-api
@@ -366,7 +240,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 rmm celery celerybeat nginx nats natsapi
for i in rmm celery celerybeat nginx nats
do
printf >&2 "${GREEN}Starting ${i} service${NC}\n"
sudo systemctl start ${i}

115
web/package-lock.json generated
View File

@@ -7,23 +7,23 @@
"": {
"version": "0.1.8",
"dependencies": {
"@quasar/extras": "^1.9.19",
"@quasar/extras": "^1.10.0",
"apexcharts": "^3.23.1",
"axios": "^0.21.1",
"dotenv": "^8.2.0",
"qrcode.vue": "^1.7.0",
"quasar": "^1.15.5",
"quasar": "^1.15.9",
"vue-apexcharts": "^1.6.0"
},
"devDependencies": {
"@quasar/app": "^2.2.1",
"@quasar/app": "^2.2.3",
"@quasar/cli": "^1.1.3",
"@quasar/quasar-app-extension-testing": "^1.0.3",
"@quasar/quasar-app-extension-testing-e2e-cypress": "^3.0.1",
"core-js": "^3.8.1",
"eslint-plugin-cypress": "^2.11.1",
"core-js": "^3.9.1",
"eslint-plugin-cypress": "^2.11.2",
"flush-promises": "^1.0.2",
"fs-extra": "^9.0.1",
"fs-extra": "^9.1.0",
"prismjs": "^1.22.0",
"vue-prism-editor": "^1.2.2"
}
@@ -1779,9 +1779,9 @@
}
},
"node_modules/@quasar/app": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@quasar/app/-/app-2.2.1.tgz",
"integrity": "sha512-oFPxZnxsum7om7fjnE5HqRqyjCywdJmyj/jr1j0QHHY1lP+09p4DKqPN3ft/4P5gBZkGH85joZD8tfz6N2h8zg==",
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@quasar/app/-/app-2.2.3.tgz",
"integrity": "sha512-l6Jh5VqjWcMzLGeG5svLVpqRcRfw9I9415ck6OSBtlwKndKQMpb5PM5NmSRW0e2igIiVZhZYWOO8NtfOMcekeA==",
"dev": true,
"dependencies": {
"@quasar/babel-preset-app": "2.0.1",
@@ -1793,7 +1793,7 @@
"@types/terser-webpack-plugin": "3.0.0",
"@types/webpack": "4.41.26",
"@types/webpack-bundle-analyzer": "3.8.0",
"@types/webpack-dev-server": "3.11.0",
"@types/webpack-dev-server": "3.11.2",
"archiver": "5.0.2",
"autoprefixer": "9.8.6",
"browserslist": "^4.14.7",
@@ -2027,9 +2027,9 @@
}
},
"node_modules/@quasar/extras": {
"version": "1.9.19",
"resolved": "https://registry.npmjs.org/@quasar/extras/-/extras-1.9.19.tgz",
"integrity": "sha512-A1IO0dzfUtRkyKq3QC7ZQNvhLeJey2ET8MvRX7oV0Qe+g30jy/4pr1ZhO7GylkDAoLT3C9eY8t2/5Anrl0AHdA==",
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@quasar/extras/-/extras-1.10.0.tgz",
"integrity": "sha512-H71Y/6pxunwiEN+oo9OBGM96ncM2QreVdnb2t2iStVHduju3nnypw9euBCshhYxKE/ORHZoOBRDoiddUOyaUdA==",
"funding": {
"type": "github",
"url": "https://donate.quasar.dev"
@@ -2163,9 +2163,9 @@
}
},
"node_modules/@types/connect-history-api-fallback": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.3.tgz",
"integrity": "sha512-7SxFCd+FLlxCfwVwbyPxbR4khL9aNikJhrorw8nUIOqeuooc9gifBuDQOJw5kzN7i6i3vLn9G8Wde/4QDihpYw==",
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.4.tgz",
"integrity": "sha512-Kf8v0wljR5GSCOCF/VQWdV3ZhKOVA73drXtY3geMTQgHy9dgqQ0dLrf31M0hcuWkhFzK5sP0kkS3mJzcKVtZbw==",
"dev": true,
"dependencies": {
"@types/express-serve-static-core": "*",
@@ -2203,9 +2203,9 @@
}
},
"node_modules/@types/express-serve-static-core": {
"version": "4.17.18",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.18.tgz",
"integrity": "sha512-m4JTwx5RUBNZvky/JJ8swEJPKFd8si08pPF2PfizYjGZOKr/svUWPcoUmLow6MmPzhasphB7gSTINY67xn3JNA==",
"version": "4.17.19",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.19.tgz",
"integrity": "sha512-DJOSHzX7pCiSElWaGR8kCprwibCB/3yW6vcT8VG3P0SJjnv19gnWG/AZMfM60Xj/YJIp/YCaDHyvzsFVeniARA==",
"dev": true,
"dependencies": {
"@types/node": "*",
@@ -2238,16 +2238,6 @@
"@types/node": "*"
}
},
"node_modules/@types/http-proxy-middleware": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/http-proxy-middleware/-/http-proxy-middleware-1.0.0.tgz",
"integrity": "sha512-/s8lFX6rT43hSPqjjD8KNuu0SkPKY7uIdR6u9DCxVqCRhAvfKxGbVOixJsAT2mdpSnCyrGFAGoB39KFh6tmRxw==",
"deprecated": "This is a stub types definition. http-proxy-middleware provides its own type definitions, so you do not need this installed.",
"dev": true,
"dependencies": {
"http-proxy-middleware": "*"
}
},
"node_modules/@types/json-schema": {
"version": "7.0.7",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz",
@@ -2382,16 +2372,16 @@
}
},
"node_modules/@types/webpack-dev-server": {
"version": "3.11.0",
"resolved": "https://registry.npmjs.org/@types/webpack-dev-server/-/webpack-dev-server-3.11.0.tgz",
"integrity": "sha512-3+86AgSzl18n5P1iUP9/lz3G3GMztCp+wxdDvVuNhx1sr1jE79GpYfKHL8k+Vht3N74K2n98CuAEw4YPJCYtDA==",
"version": "3.11.2",
"resolved": "https://registry.npmjs.org/@types/webpack-dev-server/-/webpack-dev-server-3.11.2.tgz",
"integrity": "sha512-13w1VhaghN+G1rYjkBPgN/GFRoHd9uI2fwK9cSKvLutdmZ22L9iicFEvt69by40DP2I6uNcClaGTyPY6nYhIgQ==",
"dev": true,
"dependencies": {
"@types/connect-history-api-fallback": "*",
"@types/express": "*",
"@types/http-proxy-middleware": "*",
"@types/serve-static": "*",
"@types/webpack": "*"
"@types/webpack": "*",
"http-proxy-middleware": "^1.0.0"
}
},
"node_modules/@types/webpack-sources": {
@@ -14956,9 +14946,9 @@
}
},
"node_modules/quasar": {
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/quasar/-/quasar-1.15.5.tgz",
"integrity": "sha512-6A1o/kjFU72fJaQ+2flm7zK/NPDg/6z3oQ0twxZW/PxkMwZXANjq3fAXvzI3bpFXi4x8s4nQ6qiDFNAW8RTFjA==",
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/quasar/-/quasar-1.15.9.tgz",
"integrity": "sha512-Bx+EtUaN4fvU4EiQRkC28A7lt5WHeTE1rmP4a0BAUgg92iZkRjDZfm+OVlqjjCgOYQdcb8pitkvg/L9xxvNLdg==",
"engines": {
"node": ">= 10.0.0",
"npm": ">= 5.6.0",
@@ -22391,9 +22381,9 @@
"dev": true
},
"@quasar/app": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@quasar/app/-/app-2.2.1.tgz",
"integrity": "sha512-oFPxZnxsum7om7fjnE5HqRqyjCywdJmyj/jr1j0QHHY1lP+09p4DKqPN3ft/4P5gBZkGH85joZD8tfz6N2h8zg==",
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@quasar/app/-/app-2.2.3.tgz",
"integrity": "sha512-l6Jh5VqjWcMzLGeG5svLVpqRcRfw9I9415ck6OSBtlwKndKQMpb5PM5NmSRW0e2igIiVZhZYWOO8NtfOMcekeA==",
"dev": true,
"requires": {
"@quasar/babel-preset-app": "2.0.1",
@@ -22405,7 +22395,7 @@
"@types/terser-webpack-plugin": "3.0.0",
"@types/webpack": "4.41.26",
"@types/webpack-bundle-analyzer": "3.8.0",
"@types/webpack-dev-server": "3.11.0",
"@types/webpack-dev-server": "3.11.2",
"archiver": "5.0.2",
"autoprefixer": "9.8.6",
"browserslist": "^4.14.7",
@@ -22591,9 +22581,9 @@
}
},
"@quasar/extras": {
"version": "1.9.19",
"resolved": "https://registry.npmjs.org/@quasar/extras/-/extras-1.9.19.tgz",
"integrity": "sha512-A1IO0dzfUtRkyKq3QC7ZQNvhLeJey2ET8MvRX7oV0Qe+g30jy/4pr1ZhO7GylkDAoLT3C9eY8t2/5Anrl0AHdA=="
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@quasar/extras/-/extras-1.10.0.tgz",
"integrity": "sha512-H71Y/6pxunwiEN+oo9OBGM96ncM2QreVdnb2t2iStVHduju3nnypw9euBCshhYxKE/ORHZoOBRDoiddUOyaUdA=="
},
"@quasar/fastclick": {
"version": "1.1.4",
@@ -22695,9 +22685,9 @@
}
},
"@types/connect-history-api-fallback": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.3.tgz",
"integrity": "sha512-7SxFCd+FLlxCfwVwbyPxbR4khL9aNikJhrorw8nUIOqeuooc9gifBuDQOJw5kzN7i6i3vLn9G8Wde/4QDihpYw==",
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.4.tgz",
"integrity": "sha512-Kf8v0wljR5GSCOCF/VQWdV3ZhKOVA73drXtY3geMTQgHy9dgqQ0dLrf31M0hcuWkhFzK5sP0kkS3mJzcKVtZbw==",
"dev": true,
"requires": {
"@types/express-serve-static-core": "*",
@@ -22735,9 +22725,9 @@
}
},
"@types/express-serve-static-core": {
"version": "4.17.18",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.18.tgz",
"integrity": "sha512-m4JTwx5RUBNZvky/JJ8swEJPKFd8si08pPF2PfizYjGZOKr/svUWPcoUmLow6MmPzhasphB7gSTINY67xn3JNA==",
"version": "4.17.19",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.19.tgz",
"integrity": "sha512-DJOSHzX7pCiSElWaGR8kCprwibCB/3yW6vcT8VG3P0SJjnv19gnWG/AZMfM60Xj/YJIp/YCaDHyvzsFVeniARA==",
"dev": true,
"requires": {
"@types/node": "*",
@@ -22770,15 +22760,6 @@
"@types/node": "*"
}
},
"@types/http-proxy-middleware": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/http-proxy-middleware/-/http-proxy-middleware-1.0.0.tgz",
"integrity": "sha512-/s8lFX6rT43hSPqjjD8KNuu0SkPKY7uIdR6u9DCxVqCRhAvfKxGbVOixJsAT2mdpSnCyrGFAGoB39KFh6tmRxw==",
"dev": true,
"requires": {
"http-proxy-middleware": "*"
}
},
"@types/json-schema": {
"version": "7.0.7",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz",
@@ -22920,16 +22901,16 @@
}
},
"@types/webpack-dev-server": {
"version": "3.11.0",
"resolved": "https://registry.npmjs.org/@types/webpack-dev-server/-/webpack-dev-server-3.11.0.tgz",
"integrity": "sha512-3+86AgSzl18n5P1iUP9/lz3G3GMztCp+wxdDvVuNhx1sr1jE79GpYfKHL8k+Vht3N74K2n98CuAEw4YPJCYtDA==",
"version": "3.11.2",
"resolved": "https://registry.npmjs.org/@types/webpack-dev-server/-/webpack-dev-server-3.11.2.tgz",
"integrity": "sha512-13w1VhaghN+G1rYjkBPgN/GFRoHd9uI2fwK9cSKvLutdmZ22L9iicFEvt69by40DP2I6uNcClaGTyPY6nYhIgQ==",
"dev": true,
"requires": {
"@types/connect-history-api-fallback": "*",
"@types/express": "*",
"@types/http-proxy-middleware": "*",
"@types/serve-static": "*",
"@types/webpack": "*"
"@types/webpack": "*",
"http-proxy-middleware": "^1.0.0"
}
},
"@types/webpack-sources": {
@@ -33145,9 +33126,9 @@
"dev": true
},
"quasar": {
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/quasar/-/quasar-1.15.5.tgz",
"integrity": "sha512-6A1o/kjFU72fJaQ+2flm7zK/NPDg/6z3oQ0twxZW/PxkMwZXANjq3fAXvzI3bpFXi4x8s4nQ6qiDFNAW8RTFjA=="
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/quasar/-/quasar-1.15.9.tgz",
"integrity": "sha512-Bx+EtUaN4fvU4EiQRkC28A7lt5WHeTE1rmP4a0BAUgg92iZkRjDZfm+OVlqjjCgOYQdcb8pitkvg/L9xxvNLdg=="
},
"query-string": {
"version": "5.1.1",

View File

@@ -10,23 +10,23 @@
"test:e2e:ci": "cross-env E2E_TEST=true start-test \"quasar dev\" http-get://localhost:8080 \"cypress run\""
},
"dependencies": {
"@quasar/extras": "^1.9.19",
"@quasar/extras": "^1.10.0",
"apexcharts": "^3.23.1",
"axios": "^0.21.1",
"dotenv": "^8.2.0",
"qrcode.vue": "^1.7.0",
"quasar": "^1.15.5",
"quasar": "^1.15.9",
"vue-apexcharts": "^1.6.0"
},
"devDependencies": {
"@quasar/app": "^2.2.1",
"@quasar/app": "^2.2.3",
"@quasar/cli": "^1.1.3",
"@quasar/quasar-app-extension-testing": "^1.0.3",
"@quasar/quasar-app-extension-testing-e2e-cypress": "^3.0.1",
"core-js": "^3.8.1",
"eslint-plugin-cypress": "^2.11.1",
"core-js": "^3.9.1",
"eslint-plugin-cypress": "^2.11.2",
"flush-promises": "^1.0.2",
"fs-extra": "^9.0.1",
"fs-extra": "^9.1.0",
"prismjs": "^1.22.0",
"vue-prism-editor": "^1.2.2"
},

View File

@@ -511,9 +511,10 @@ export default {
}, 500);
},
runFavScript(scriptpk, agentpk) {
let default_timeout = this.favoriteScripts.find(i => i.value === scriptpk).timeout;
const data = {
pk: agentpk,
timeout: 900,
timeout: default_timeout,
scriptPK: scriptpk,
output: "forget",
args: [],
@@ -532,7 +533,7 @@ export default {
}
this.favoriteScripts = r.data
.filter(k => k.favorite === true)
.map(script => ({ label: script.name, value: script.id }))
.map(script => ({ label: script.name, value: script.id, timeout: script.default_timeout }))
.sort((a, b) => a.label.localeCompare(b.label));
});
},

View File

@@ -133,7 +133,6 @@ export default {
})
.catch(e => {
this.$q.loading.hide();
console.log({ e });
this.notifyError("There was an issue resolving alert");
});
},

View File

@@ -0,0 +1,189 @@
<template>
<q-dialog ref="dialog" @hide="onHide">
<div class="q-dialog-plugin" style="width: 90vw; max-width: 90vw">
<q-card>
<q-bar>
<q-btn @click="getClients" class="q-mr-sm" dense flat push icon="refresh" />Clients Manager
<q-space />
<q-btn dense flat icon="close" v-close-popup>
<q-tooltip content-class="bg-white text-primary">Close</q-tooltip>
</q-btn>
</q-bar>
<div class="q-pa-sm" style="min-height: 65vh; max-height: 65vh">
<div class="q-gutter-sm">
<q-btn label="New" dense flat push unelevated no-caps icon="add" @click="showAddClient" />
</div>
<q-table
dense
:data="clients"
:columns="columns"
:pagination.sync="pagination"
row-key="id"
binary-state-sort
hide-pagination
virtual-scroll
:rows-per-page-options="[0]"
no-data-label="No Clients"
:table-class="{ 'table-bgcolor': !$q.dark.isActive, 'table-bgcolor-dark': $q.dark.isActive }"
class="settings-tbl-sticky"
>
<!-- body slots -->
<template v-slot:body="props">
<q-tr :props="props" class="cursor-pointer" @dblclick="showEditClient(props.row)">
<!-- context menu -->
<q-menu context-menu>
<q-list dense style="min-width: 200px">
<q-item clickable v-close-popup @click="showEditClient(props.row)">
<q-item-section side>
<q-icon name="edit" />
</q-item-section>
<q-item-section>Edit</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="showClientDeleteModal(props.row)">
<q-item-section side>
<q-icon name="delete" />
</q-item-section>
<q-item-section>Delete</q-item-section>
</q-item>
<q-separator></q-separator>
<q-item clickable v-close-popup @click="showAddSite(props.row)">
<q-item-section side>
<q-icon name="add" />
</q-item-section>
<q-item-section>Add Site</q-item-section>
</q-item>
<q-separator></q-separator>
<q-item clickable v-close-popup>
<q-item-section>Close</q-item-section>
</q-item>
</q-list>
</q-menu>
<!-- name -->
<q-td>
{{ props.row.name }}
</q-td>
<q-td>
<span
style="cursor: pointer; text-decoration: underline"
class="text-primary"
@click="showSitesTable(props.row)"
>Show Sites</span
>
</q-td>
</q-tr>
</template>
</q-table>
</div>
</q-card>
</div>
</q-dialog>
</template>
<script>
import mixins from "@/mixins/mixins";
import ClientsForm from "@/components/modals/clients/ClientsForm";
import SitesForm from "@/components/modals/clients/SitesForm";
import DeleteClient from "@/components/modals/clients/DeleteClient";
import SitesTable from "@/components/modals/clients/SitesTable";
export default {
name: "ClientsManager",
mixins: [mixins],
data() {
return {
clients: [],
columns: [
{ name: "name", label: "Name", field: "name", align: "left" },
{ name: "sites", label: "Sites", field: "sites", align: "left" },
],
pagination: {
rowsPerPage: 0,
sortBy: "name",
descending: false,
},
};
},
methods: {
getClients() {
this.$q.loading.show();
this.$axios
.get("clients/clients/")
.then(r => {
this.clients = r.data;
this.$q.loading.hide();
})
.catch(e => {
this.$q.loading.hide();
this.notifyError("Unable to get Clients.");
});
},
showClientDeleteModal(client) {
this.$q
.dialog({
component: DeleteClient,
parent: this,
object: client,
type: "client",
})
.onOk(() => {
this.getClients();
});
},
showEditClient(client) {
this.$q
.dialog({
component: ClientsForm,
parent: this,
client: client,
})
.onOk(() => {
this.getClients();
});
},
showAddClient() {
this.$q
.dialog({
component: ClientsForm,
parent: this,
})
.onOk(() => {
this.getClients();
});
},
showAddSite(client) {
this.$q
.dialog({
component: SitesForm,
parent: this,
client: client.id,
})
.onOk(() => {
this.getClients();
});
},
showSitesTable(client) {
this.$q.dialog({
component: SitesTable,
parent: this,
client: client,
});
},
show() {
this.$refs.dialog.show();
},
hide() {
this.$refs.dialog.hide();
},
onHide() {
this.$emit("hide");
},
},
mounted() {
this.getClients();
},
};
</script>

View File

@@ -0,0 +1,104 @@
<template>
<q-input
v-if="field.type === 'text' || field.type === 'number'"
ref="input"
outlined
dense
:label="field.name"
:type="field.type === 'text' ? 'text' : 'number'"
:hint="hintText(field)"
:value="value"
@input="value => $emit('input', value)"
:rules="[...validationRules]"
reactive-rules
/>
<q-toggle
v-else-if="field.type === 'checkbox'"
ref="input"
:label="field.name"
:hint="hintText(field)"
:value="value"
@input="value => $emit('input', value)"
/>
<q-input
v-else-if="field.type === 'datetime'"
ref="input"
:label="field.name"
:hint="hintText(field)"
outlined
dense
:value="value"
@input="value => $emit('input', value)"
:rules="[...validationRules]"
reactive-rules
>
<template v-slot:append>
<q-icon name="event" class="cursor-pointer">
<q-popup-proxy transition-show="scale" transition-hide="scale">
<q-date :value="value" @input="value => $emit('input', value)" mask="YYYY-MM-DD HH:mm">
<div class="row items-center justify-end">
<q-btn v-close-popup label="Close" color="primary" flat />
</div>
</q-date>
</q-popup-proxy>
</q-icon>
<q-icon name="access_time" class="cursor-pointer">
<q-popup-proxy transition-show="scale" transition-hide="scale">
<q-time :value="value" @input="value => $emit('input', value)" mask="YYYY-MM-DD HH:mm">
<div class="row items-center justify-end">
<q-btn v-close-popup label="Close" color="primary" flat />
</div>
</q-time>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
<q-select
v-else-if="field.type === 'single' || field.type === 'multiple'"
ref="input"
:value="value"
@input="value => $emit('input', value)"
outlined
dense
:hint="hintText(field)"
:label="field.name"
:options="field.options"
:multiple="field.type === 'multiple'"
:rules="[...validationRules]"
reactive-rules
clearable
/>
</template>
<script>
export default {
name: "CustomField",
props: ["field", "value"],
methods: {
validate(...args) {
return this.$refs.input.validate(...args);
},
hintText(field) {
if (field.type === "multiple")
return field.default_values_multiple.length > 0 ? `Default value: ${field.default_values_multiple}` : "";
else if (field.type === "checkbox")
return field.default_value_bool ? `Default value: ${field.default_value_bool}` : "";
else return field.default_value_string ? `Default value: ${field.default_value_string}` : "";
},
},
computed: {
validationRules() {
const rules = [];
if (this.field.required) {
rules.push(val => !!val || `${this.field.name} is required`);
}
return rules;
},
},
};
</script>

View File

@@ -37,6 +37,7 @@
><span v-if="props.row.arch === '64'">64 bit</span><span v-else>32 bit</span></q-td
>
<q-td key="expiry" :props="props">{{ props.row.expiry }}</q-td>
<q-td key="created" :props="props">{{ props.row.created }}</q-td>
<q-td key="flags" :props="props"
><q-badge color="grey-8" label="View Flags" />
<q-tooltip content-style="font-size: 12px">{{ props.row.install_flags }}</q-tooltip>
@@ -58,7 +59,6 @@
</template>
<script>
import axios from "axios";
import mixins from "@/mixins/mixins";
import NewDeployment from "@/components/modals/clients/NewDeployment";
import { copyToClipboard } from "quasar";
@@ -82,11 +82,12 @@ export default {
{ name: "mon_type", label: "Type", field: "mon_type", align: "left", sortable: true },
{ name: "arch", label: "Arch", field: "arch", align: "left", sortable: true },
{ name: "expiry", label: "Expiry", field: "expiry", align: "left", sortable: true },
{ name: "created", label: "Created", field: "created", align: "left", sortable: true },
{ name: "flags", label: "Flags", field: "install_flags", align: "left" },
{ name: "link", label: "Download Link", align: "left" },
{ name: "delete", label: "Delete", align: "left" },
],
visibleColumns: ["client", "site", "mon_type", "arch", "expiry", "flags", "link", "delete"],
visibleColumns: ["client", "site", "mon_type", "arch", "expiry", "created", "flags", "link", "delete"],
pagination: {
rowsPerPage: 50,

View File

@@ -12,28 +12,11 @@
</q-item-section>
<q-menu anchor="top right" self="top left">
<q-list dense style="min-width: 100px">
<q-item clickable v-close-popup @click="showClientsFormModal('client', 'add')">
<q-item-section>Add Client</q-item-section>
<q-item clickable v-close-popup @click="showAddClientModal">
<q-item-section>Client</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="showClientsFormModal('site', 'add')">
<q-item-section>Add Site</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-item>
<q-item clickable>
<q-item-section>Delete</q-item-section>
<q-item-section side>
<q-icon name="keyboard_arrow_right" />
</q-item-section>
<q-menu anchor="top right" self="top left">
<q-list dense style="min-width: 100px">
<q-item clickable v-close-popup @click="showClientsFormModal('client', 'delete')">
<q-item-section>Delete Client</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="showClientsFormModal('site', 'delete')">
<q-item-section>Delete Site</q-item-section>
<q-item clickable v-close-popup @click="showAddSiteModal">
<q-item-section>Site</q-item-section>
</q-item>
</q-list>
</q-menu>
@@ -51,19 +34,6 @@
</q-list>
</q-menu>
</q-btn>
<!-- edit -->
<q-btn size="md" dense no-caps flat label="Edit">
<q-menu>
<q-list dense style="min-width: 100px">
<q-item clickable v-close-popup @click="showClientsFormModal('client', 'edit')">
<q-item-section>Edit Clients</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="showClientsFormModal('site', 'edit')">
<q-item-section>Edit Sites</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-btn>
<!-- view -->
<q-btn size="md" dense no-caps flat label="View">
<q-menu auto-close>
@@ -95,6 +65,10 @@
<q-btn size="md" dense no-caps flat label="Settings">
<q-menu auto-close>
<q-list dense style="min-width: 100px">
<!-- clients manager -->
<q-item clickable v-close-popup @click="showClientsManager">
<q-item-section>Clients Manager</q-item-section>
</q-item>
<!-- script manager -->
<q-item clickable v-close-popup @click="showScriptManager = true">
<q-item-section>Script Manager</q-item-section>
@@ -143,14 +117,6 @@
</q-btn>
</q-btn-group>
<q-space />
<!-- client form modal -->
<q-dialog v-model="showClientFormModal" @hide="closeClientsFormModal">
<ClientsForm @close="closeClientsFormModal" :op="clientOp" @edited="edited" />
</q-dialog>
<!-- site form modal -->
<q-dialog v-model="showSiteFormModal" @hide="closeClientsFormModal">
<SitesForm @close="closeClientsFormModal" :op="clientOp" @edited="edited" />
</q-dialog>
<!-- edit core settings modal -->
<q-dialog v-model="showEditCoreSettingsModal">
<EditCoreSettings @close="showEditCoreSettingsModal = false" />
@@ -220,6 +186,7 @@
<script>
import LogModal from "@/components/modals/logs/LogModal";
import PendingActions from "@/components/modals/logs/PendingActions";
import ClientsManager from "@/components/ClientsManager";
import ClientsForm from "@/components/modals/clients/ClientsForm";
import SitesForm from "@/components/modals/clients/SitesForm";
import UpdateAgents from "@/components/modals/agents/UpdateAgents";
@@ -240,8 +207,6 @@ export default {
components: {
LogModal,
PendingActions,
ClientsForm,
SitesForm,
UpdateAgents,
ScriptManager,
EditCoreSettings,
@@ -253,13 +218,9 @@ export default {
Deployment,
ServerMaintenance,
},
props: ["clients"],
data() {
return {
showServerMaintenance: false,
showClientFormModal: false,
showSiteFormModal: false,
clientOp: null,
showUpdateAgentsModal: false,
showEditCoreSettingsModal: false,
showAdminManager: false,
@@ -275,20 +236,6 @@ export default {
};
},
methods: {
showClientsFormModal(type, op) {
this.clientOp = op;
if (type === "client") {
this.showClientFormModal = true;
} else if (type === "site") {
this.showSiteFormModal = true;
}
},
closeClientsFormModal() {
this.clientOp = null;
this.showClientFormModal = null;
this.showSiteFormModal = null;
},
showBulkActionModal(mode) {
this.bulkMode = mode;
this.showBulkAction = true;
@@ -309,6 +256,24 @@ export default {
parent: this,
});
},
showClientsManager() {
this.$q.dialog({
component: ClientsManager,
parent: this,
});
},
showAddClientModal() {
this.$q.dialog({
component: ClientsForm,
parent: this,
});
},
showAddSiteModal() {
this.$q.dialog({
component: SitesForm,
parent: this,
});
},
edited() {
this.$emit("edited");
},

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