Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e43b55804 | ||
|
|
ba9bdaae0a | ||
|
|
7dfd7bde8e | ||
|
|
5e6c4161d0 | ||
|
|
d75d56dfc9 | ||
|
|
1d9d350091 | ||
|
|
5744053c6f | ||
|
|
65589b6ca2 | ||
|
|
e03a9d1137 | ||
|
|
29f80f2276 | ||
|
|
a9b74aa69b | ||
|
|
63ebfd3210 | ||
|
|
87fa5ff7a6 | ||
|
|
b686b53a9c | ||
|
|
258261dc64 | ||
|
|
9af5c9ead9 | ||
|
|
382654188c | ||
|
|
fa1df082b7 | ||
|
|
5c227d8f80 | ||
|
|
81dabdbfb7 | ||
|
|
91f89f5a33 | ||
|
|
9f92746aa0 | ||
|
|
5d6e6f9441 | ||
|
|
01395a2726 | ||
|
|
465d75c65d | ||
|
|
4634f8927e | ||
|
|
74a287f9fe | ||
|
|
7ff6c79835 | ||
|
|
3629982237 | ||
|
|
ddb610f1bc | ||
|
|
f899905d27 | ||
|
|
3e4531b5c5 | ||
|
|
a9e189e51d | ||
|
|
58ba08a8f3 | ||
|
|
9078ff27d8 | ||
|
|
6f43e61c24 | ||
|
|
4be0d3f212 | ||
|
|
00e47e5a27 | ||
|
|
152e145b32 | ||
|
|
54e55e8f57 | ||
|
|
05b8707f9e | ||
|
|
543e952023 | ||
|
|
6e5f40ea06 | ||
|
|
bbafb0be87 | ||
|
|
1c9c5232fe | ||
|
|
598d79a502 | ||
|
|
37d8360b77 | ||
|
|
82d9ca3317 | ||
|
|
4e4238d486 | ||
|
|
c77dbe44dc | ||
|
|
e03737f15f | ||
|
|
a02629bcd7 | ||
|
|
6c3fc23d78 | ||
|
|
0fe40f9ccb | ||
|
|
9bd7c8edd1 | ||
|
|
83ba480863 | ||
|
|
f158ea25e9 | ||
|
|
0227519eab | ||
|
|
616a9685fa | ||
|
|
fe61b01320 | ||
|
|
7b25144311 | ||
|
|
9d42fbbdd7 | ||
|
|
39ac5b088b | ||
|
|
c14ffd08a0 | ||
|
|
6e1239340b | ||
|
|
a297dc8b3b | ||
|
|
8d4ecc0898 |
@@ -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 /
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
24
api/tacticalrmm/agents/migrations/0032_agentcustomfield.py
Normal file
24
api/tacticalrmm/agents/migrations/0032_agentcustomfield.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 3.1.7 on 2021-03-17 14:45
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0014_customfield'),
|
||||
('agents', '0031_agent_alert_template'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='AgentCustomField',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('value', models.TextField(blank=True, null=True)),
|
||||
('agent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='custom_fields', to='agents.agent')),
|
||||
('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='agent_fields', to='core.customfield')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 3.1.7 on 2021-03-29 02:51
|
||||
|
||||
import django.contrib.postgres.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('agents', '0032_agentcustomfield'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='agentcustomfield',
|
||||
name='multiple_value',
|
||||
field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(blank=True, null=True), blank=True, default=list, null=True, size=None),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.7 on 2021-03-29 03:01
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('agents', '0033_agentcustomfield_multiple_value'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='agentcustomfield',
|
||||
name='checkbox_value',
|
||||
field=models.BooleanField(blank=True, default=False),
|
||||
),
|
||||
]
|
||||
23
api/tacticalrmm/agents/migrations/0035_auto_20210329_1709.py
Normal file
23
api/tacticalrmm/agents/migrations/0035_auto_20210329_1709.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.1.7 on 2021-03-29 17:09
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('agents', '0034_agentcustomfield_checkbox_value'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='agentcustomfield',
|
||||
old_name='checkbox_value',
|
||||
new_name='bool_value',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='agentcustomfield',
|
||||
old_name='value',
|
||||
new_name='string_value',
|
||||
),
|
||||
]
|
||||
@@ -4,7 +4,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
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
@@ -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 = [
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import Client, Deployment, Site
|
||||
from .models import Client, ClientCustomField, Deployment, Site, SiteCustomField
|
||||
|
||||
admin.site.register(Client)
|
||||
admin.site.register(Site)
|
||||
admin.site.register(Deployment)
|
||||
admin.site.register(ClientCustomField)
|
||||
admin.site.register(SiteCustomField)
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# Generated by Django 3.1.7 on 2021-03-17 14:45
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0014_customfield'),
|
||||
('clients', '0009_auto_20210212_1408'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SiteCustomField',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('value', models.TextField(blank=True, null=True)),
|
||||
('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='site_fields', to='core.customfield')),
|
||||
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='custom_fields', to='clients.site')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ClientCustomField',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('value', models.TextField(blank=True, null=True)),
|
||||
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='custom_fields', to='clients.client')),
|
||||
('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='client_fields', to='core.customfield')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 3.1.7 on 2021-03-21 15:11
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('clients', '0010_clientcustomfield_sitecustomfield'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name='site',
|
||||
unique_together={('client', 'name')},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.7 on 2021-03-26 06:52
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('clients', '0011_auto_20210321_1511'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='deployment',
|
||||
name='created',
|
||||
field=models.DateTimeField(auto_now_add=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 3.1.7 on 2021-03-29 02:51
|
||||
|
||||
import django.contrib.postgres.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('clients', '0012_deployment_created'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='clientcustomfield',
|
||||
name='multiple_value',
|
||||
field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(blank=True, null=True), blank=True, default=list, null=True, size=None),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitecustomfield',
|
||||
name='multiple_value',
|
||||
field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(blank=True, null=True), blank=True, default=list, null=True, size=None),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.1.7 on 2021-03-29 03:01
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('clients', '0013_auto_20210329_0251'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='clientcustomfield',
|
||||
name='checkbox_value',
|
||||
field=models.BooleanField(blank=True, default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitecustomfield',
|
||||
name='checkbox_value',
|
||||
field=models.BooleanField(blank=True, default=False),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,27 @@
|
||||
# Generated by Django 3.1.7 on 2021-03-29 17:09
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('clients', '0014_auto_20210329_0301'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='clientcustomfield',
|
||||
old_name='checkbox_value',
|
||||
new_name='bool_value',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='clientcustomfield',
|
||||
old_name='value',
|
||||
new_name='string_value',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='sitecustomfield',
|
||||
name='value',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.1.7 on 2021-03-29 18:27
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('clients', '0015_auto_20210329_1709'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='sitecustomfield',
|
||||
old_name='checkbox_value',
|
||||
new_name='bool_value',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitecustomfield',
|
||||
name='string_value',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,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
|
||||
|
||||
@@ -1,42 +1,85 @@
|
||||
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",
|
||||
"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",
|
||||
"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 +126,5 @@ class DeploymentSerializer(ModelSerializer):
|
||||
"arch",
|
||||
"expiry",
|
||||
"install_flags",
|
||||
"created",
|
||||
]
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import uuid
|
||||
from unittest.mock import patch
|
||||
|
||||
from model_bakery import baker
|
||||
from rest_framework.serializers import ValidationError
|
||||
|
||||
from tacticalrmm.test import TacticalTestCase
|
||||
|
||||
from .models import Client, Deployment, Site
|
||||
from .models import Client, ClientCustomField, Deployment, Site, SiteCustomField
|
||||
from .serializers import (
|
||||
ClientSerializer,
|
||||
ClientTreeSerializer,
|
||||
@@ -28,18 +29,29 @@ class TestClientViews(TacticalTestCase):
|
||||
r = self.client.get(url, format="json")
|
||||
serializer = ClientSerializer(clients, many=True)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(r.data, serializer.data)
|
||||
self.assertEqual(r.data, serializer.data) # type: ignore
|
||||
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
def test_add_client(self):
|
||||
url = "/clients/clients/"
|
||||
payload = {"client": "Company 1", "site": "Site 1"}
|
||||
|
||||
# test successfull add client
|
||||
payload = {
|
||||
"client": {"name": "Client1"},
|
||||
"site": {"name": "Site1"},
|
||||
"custom_fields": [],
|
||||
}
|
||||
r = self.client.post(url, payload, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
payload["client"] = "Company1|askd"
|
||||
serializer = ClientSerializer(data={"name": payload["client"]}, context=payload)
|
||||
# test add client with | in name
|
||||
payload = {
|
||||
"client": {"name": "Client2|d"},
|
||||
"site": {"name": "Site1"},
|
||||
"custom_fields": [],
|
||||
}
|
||||
serializer = ClientSerializer(data=payload["client"])
|
||||
with self.assertRaisesMessage(
|
||||
ValidationError, "Client name cannot contain the | character"
|
||||
):
|
||||
@@ -48,19 +60,22 @@ class TestClientViews(TacticalTestCase):
|
||||
r = self.client.post(url, payload, format="json")
|
||||
self.assertEqual(r.status_code, 400)
|
||||
|
||||
payload = {"client": "Company 156", "site": "Site2|a34"}
|
||||
serializer = ClientSerializer(data={"name": payload["client"]}, context=payload)
|
||||
with self.assertRaisesMessage(
|
||||
ValidationError, "Site name cannot contain the | character"
|
||||
):
|
||||
self.assertFalse(serializer.is_valid(raise_exception=True))
|
||||
|
||||
# test add client with | in Site name
|
||||
payload = {
|
||||
"client": {"name": "Client2"},
|
||||
"site": {"name": "Site1|fds"},
|
||||
"custom_fields": [],
|
||||
}
|
||||
r = self.client.post(url, payload, format="json")
|
||||
self.assertEqual(r.status_code, 400)
|
||||
|
||||
# test unique
|
||||
payload = {"client": "Company 1", "site": "Site 1"}
|
||||
serializer = ClientSerializer(data={"name": payload["client"]}, context=payload)
|
||||
payload = {
|
||||
"client": {"name": "Client1"},
|
||||
"site": {"name": "Site1"},
|
||||
"custom_fields": [],
|
||||
}
|
||||
serializer = ClientSerializer(data=payload["client"])
|
||||
with self.assertRaisesMessage(
|
||||
ValidationError, "client with this name already exists."
|
||||
):
|
||||
@@ -69,67 +84,129 @@ class TestClientViews(TacticalTestCase):
|
||||
r = self.client.post(url, payload, format="json")
|
||||
self.assertEqual(r.status_code, 400)
|
||||
|
||||
# test long site name
|
||||
payload = {"client": "Company 2394", "site": "Site123" * 100}
|
||||
serializer = ClientSerializer(data={"name": payload["client"]}, context=payload)
|
||||
with self.assertRaisesMessage(ValidationError, "Site name too long"):
|
||||
self.assertFalse(serializer.is_valid(raise_exception=True))
|
||||
|
||||
r = self.client.post(url, payload, format="json")
|
||||
self.assertEqual(r.status_code, 400)
|
||||
|
||||
# test initial setup
|
||||
payload = {
|
||||
"client": {"client": "Company 4", "site": "HQ"},
|
||||
"initialsetup": True,
|
||||
"client": {"name": "Setup Client"},
|
||||
"site": {"name": "Setup Site"},
|
||||
"timezone": "America/Los_Angeles",
|
||||
"initialsetup": True,
|
||||
}
|
||||
r = self.client.post(url, payload, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
# test add with custom fields
|
||||
field = baker.make("core.CustomField", model="client", type="text")
|
||||
payload = {
|
||||
"client": {"name": "Custom Field Client"},
|
||||
"site": {"name": "Setup Site"},
|
||||
"custom_fields": [{"field": field.id, "string_value": "new Value"}], # type: ignore
|
||||
}
|
||||
r = self.client.post(url, payload, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
client = Client.objects.get(name="Custom Field Client")
|
||||
self.assertTrue(
|
||||
ClientCustomField.objects.filter(client=client, field=field).exists()
|
||||
)
|
||||
|
||||
self.check_not_authenticated("post", url)
|
||||
|
||||
def test_get_client(self):
|
||||
# setup data
|
||||
client = baker.make("clients.Client")
|
||||
|
||||
url = f"/clients/{client.id}/client/" # type: ignore
|
||||
r = self.client.get(url, format="json")
|
||||
serializer = ClientSerializer(client)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(r.data, serializer.data) # type: ignore
|
||||
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
def test_edit_client(self):
|
||||
# setup data
|
||||
client = baker.make("clients.Client")
|
||||
client = baker.make("clients.Client", name="OldClientName")
|
||||
|
||||
# test invalid id
|
||||
r = self.client.put("/clients/500/client/", format="json")
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
data = {"id": client.id, "name": "New Name"}
|
||||
|
||||
url = f"/clients/{client.id}/client/"
|
||||
# test successfull edit client
|
||||
data = {"client": {"name": "NewClientName"}, "custom_fields": []}
|
||||
url = f"/clients/{client.id}/client/" # type: ignore
|
||||
r = self.client.put(url, data, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertTrue(Client.objects.filter(name="New Name").exists())
|
||||
self.assertTrue(Client.objects.filter(name="NewClientName").exists())
|
||||
self.assertFalse(Client.objects.filter(name="OldClientName").exists())
|
||||
|
||||
self.check_not_authenticated("put", url)
|
||||
|
||||
def test_delete_client(self):
|
||||
# setup data
|
||||
client = baker.make("clients.Client")
|
||||
site = baker.make("clients.Site", client=client)
|
||||
agent = baker.make_recipe("agents.agent", site=site)
|
||||
|
||||
# test invalid id
|
||||
r = self.client.delete("/clients/500/client/", format="json")
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
url = f"/clients/{client.id}/client/"
|
||||
|
||||
# test deleting with agents under client
|
||||
r = self.client.delete(url, format="json")
|
||||
# test edit client with | in name
|
||||
data = {"client": {"name": "NewClie|ntName"}, "custom_fields": []}
|
||||
url = f"/clients/{client.id}/client/" # type: ignore
|
||||
r = self.client.put(url, data, format="json")
|
||||
self.assertEqual(r.status_code, 400)
|
||||
|
||||
# test successful deletion
|
||||
agent.delete()
|
||||
r = self.client.delete(url, format="json")
|
||||
# test add with custom fields new value
|
||||
field = baker.make("core.CustomField", model="client", type="checkbox")
|
||||
payload = {
|
||||
"client": {
|
||||
"id": client.id, # type: ignore
|
||||
"name": "Custom Field Client",
|
||||
},
|
||||
"custom_fields": [{"field": field.id, "bool_value": True}], # type: ignore
|
||||
}
|
||||
r = self.client.put(url, payload, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertFalse(Client.objects.filter(pk=client.id).exists())
|
||||
self.assertFalse(Site.objects.filter(pk=site.id).exists())
|
||||
|
||||
client = Client.objects.get(name="Custom Field Client")
|
||||
self.assertTrue(
|
||||
ClientCustomField.objects.filter(client=client, field=field).exists()
|
||||
)
|
||||
|
||||
# edit custom field value
|
||||
payload = {
|
||||
"client": {
|
||||
"id": client.id, # type: ignore
|
||||
"name": "Custom Field Client",
|
||||
},
|
||||
"custom_fields": [{"field": field.id, "bool_value": False}], # type: ignore
|
||||
}
|
||||
r = self.client.put(url, payload, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
self.assertFalse(
|
||||
ClientCustomField.objects.get(client=client, field=field).value
|
||||
)
|
||||
|
||||
self.check_not_authenticated("put", url)
|
||||
|
||||
@patch("automation.tasks.generate_all_agent_checks_task.delay")
|
||||
@patch("automation.tasks.generate_all_agent_checks_task.delay")
|
||||
def test_delete_client(self, task1, task2):
|
||||
from agents.models import Agent
|
||||
|
||||
task1.return_value = "ok"
|
||||
task2.return_value = "ok"
|
||||
# setup data
|
||||
client_to_delete = baker.make("clients.Client")
|
||||
client_to_move = baker.make("clients.Client")
|
||||
site_to_move = baker.make("clients.Site", client=client_to_move)
|
||||
agent = baker.make_recipe("agents.agent", site=site_to_move)
|
||||
|
||||
# test invalid id
|
||||
r = self.client.delete("/clients/334/953/", format="json")
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
url = f"/clients/{client_to_delete.id}/{site_to_move.id}/" # type: ignore
|
||||
|
||||
# test successful deletion
|
||||
r = self.client.delete(url, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
agent_moved = Agent.objects.get(pk=agent.pk)
|
||||
self.assertEqual(agent_moved.site.id, site_to_move.id) # type: ignore
|
||||
self.assertFalse(Client.objects.filter(pk=client_to_delete.id).exists()) # type: ignore
|
||||
|
||||
self.check_not_authenticated("delete", url)
|
||||
|
||||
def test_get_sites(self):
|
||||
# setup data
|
||||
baker.make("clients.Site", _quantity=5)
|
||||
@@ -139,29 +216,31 @@ class TestClientViews(TacticalTestCase):
|
||||
r = self.client.get(url, format="json")
|
||||
serializer = SiteSerializer(sites, many=True)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(r.data, serializer.data)
|
||||
self.assertEqual(r.data, serializer.data) # type: ignore
|
||||
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
def test_add_site(self):
|
||||
# setup data
|
||||
site = baker.make("clients.Site")
|
||||
client = baker.make("clients.Client")
|
||||
site = baker.make("clients.Site", client=client)
|
||||
|
||||
url = "/clients/sites/"
|
||||
|
||||
# test success add
|
||||
payload = {"client": site.client.id, "name": "LA Office"}
|
||||
payload = {
|
||||
"site": {"client": client.id, "name": "LA Office"}, # type: ignore
|
||||
"custom_fields": [],
|
||||
}
|
||||
r = self.client.post(url, payload, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertTrue(
|
||||
Site.objects.filter(
|
||||
name="LA Office", client__name=site.client.name
|
||||
).exists()
|
||||
)
|
||||
|
||||
# test with | symbol
|
||||
payload = {"client": site.client.id, "name": "LA Off|ice |*&@#$"}
|
||||
serializer = SiteSerializer(data=payload, context={"clientpk": site.client.id})
|
||||
payload = {
|
||||
"site": {"client": client.id, "name": "LA Office |*&@#$"}, # type: ignore
|
||||
"custom_fields": [],
|
||||
}
|
||||
serializer = SiteSerializer(data=payload["site"])
|
||||
with self.assertRaisesMessage(
|
||||
ValidationError, "Site name cannot contain the | character"
|
||||
):
|
||||
@@ -171,55 +250,139 @@ class TestClientViews(TacticalTestCase):
|
||||
self.assertEqual(r.status_code, 400)
|
||||
|
||||
# test site already exists
|
||||
payload = {"client": site.client.id, "name": "LA Office"}
|
||||
serializer = SiteSerializer(data=payload, context={"clientpk": site.client.id})
|
||||
with self.assertRaisesMessage(ValidationError, "Site LA Office already exists"):
|
||||
payload = {
|
||||
"site": {"client": site.client.id, "name": "LA Office"}, # type: ignore
|
||||
"custom_fields": [],
|
||||
}
|
||||
serializer = SiteSerializer(data=payload["site"])
|
||||
with self.assertRaisesMessage(
|
||||
ValidationError, "The fields client, name must make a unique set."
|
||||
):
|
||||
self.assertFalse(serializer.is_valid(raise_exception=True))
|
||||
|
||||
# test add with custom fields
|
||||
field = baker.make(
|
||||
"core.CustomField",
|
||||
model="site",
|
||||
type="single",
|
||||
options=["one", "two", "three"],
|
||||
)
|
||||
payload = {
|
||||
"site": {"client": client.id, "name": "Custom Field Site"}, # type: ignore
|
||||
"custom_fields": [{"field": field.id, "string_value": "one"}], # type: ignore
|
||||
}
|
||||
r = self.client.post(url, payload, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
site = Site.objects.get(name="Custom Field Site")
|
||||
self.assertTrue(SiteCustomField.objects.filter(site=site, field=field).exists())
|
||||
|
||||
self.check_not_authenticated("post", url)
|
||||
|
||||
def test_edit_site(self):
|
||||
def test_get_site(self):
|
||||
# setup data
|
||||
site = baker.make("clients.Site")
|
||||
|
||||
url = f"/clients/sites/{site.id}/" # type: ignore
|
||||
r = self.client.get(url, format="json")
|
||||
serializer = SiteSerializer(site)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(r.data, serializer.data) # type: ignore
|
||||
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
def test_edit_site(self):
|
||||
# setup data
|
||||
client = baker.make("clients.Client")
|
||||
site = baker.make("clients.Site", client=client)
|
||||
|
||||
# test invalid id
|
||||
r = self.client.put("/clients/500/site/", format="json")
|
||||
r = self.client.put("/clients/sites/688/", format="json")
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
data = {"id": site.id, "name": "New Name", "client": site.client.id}
|
||||
data = {
|
||||
"site": {"client": client.id, "name": "New Site Name"}, # type: ignore
|
||||
"custom_fields": [],
|
||||
}
|
||||
|
||||
url = f"/clients/{site.id}/site/"
|
||||
url = f"/clients/sites/{site.id}/" # type: ignore
|
||||
r = self.client.put(url, data, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertTrue(Site.objects.filter(name="New Name").exists())
|
||||
self.assertTrue(
|
||||
Site.objects.filter(client=client, name="New Site Name").exists()
|
||||
)
|
||||
|
||||
# test add with custom fields new value
|
||||
field = baker.make(
|
||||
"core.CustomField",
|
||||
model="site",
|
||||
type="multiple",
|
||||
options=["one", "two", "three"],
|
||||
)
|
||||
payload = {
|
||||
"site": {
|
||||
"id": site.id, # type: ignore
|
||||
"client": site.client.id, # type: ignore
|
||||
"name": "Custom Field Site",
|
||||
},
|
||||
"custom_fields": [{"field": field.id, "multiple_value": ["two", "three"]}], # type: ignore
|
||||
}
|
||||
r = self.client.put(url, payload, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
site = Site.objects.get(name="Custom Field Site")
|
||||
self.assertTrue(SiteCustomField.objects.filter(site=site, field=field).exists())
|
||||
|
||||
# edit custom field value
|
||||
payload = {
|
||||
"site": {
|
||||
"id": site.id, # type: ignore
|
||||
"client": client.id, # type: ignore
|
||||
"name": "Custom Field Site",
|
||||
},
|
||||
"custom_fields": [{"field": field.id, "multiple_value": ["one"]}], # type: ignore
|
||||
}
|
||||
r = self.client.put(url, payload, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
self.assertTrue(
|
||||
SiteCustomField.objects.get(site=site, field=field).value,
|
||||
["one"],
|
||||
)
|
||||
|
||||
self.check_not_authenticated("put", url)
|
||||
|
||||
def test_delete_site(self):
|
||||
@patch("automation.tasks.generate_all_agent_checks_task.delay")
|
||||
@patch("automation.tasks.generate_all_agent_checks_task.delay")
|
||||
def test_delete_site(self, task1, task2):
|
||||
from agents.models import Agent
|
||||
|
||||
task1.return_value = "ok"
|
||||
task2.return_value = "ok"
|
||||
# setup data
|
||||
site = baker.make("clients.Site")
|
||||
agent = baker.make_recipe("agents.agent", site=site)
|
||||
client = baker.make("clients.Client")
|
||||
site_to_delete = baker.make("clients.Site", client=client)
|
||||
site_to_move = baker.make("clients.Site")
|
||||
agent = baker.make_recipe("agents.agent", site=site_to_delete)
|
||||
|
||||
# test invalid id
|
||||
r = self.client.delete("/clients/500/site/", format="json")
|
||||
r = self.client.delete("/clients/500/445/", format="json")
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
url = f"/clients/{site.id}/site/"
|
||||
url = f"/clients/sites/{site_to_delete.id}/{site_to_move.id}/" # type: ignore
|
||||
|
||||
# test deleting with last site under client
|
||||
r = self.client.delete(url, format="json")
|
||||
self.assertEqual(r.status_code, 400)
|
||||
|
||||
# test deletion when agents exist under site
|
||||
baker.make("clients.Site", client=site.client)
|
||||
r = self.client.delete(url, format="json")
|
||||
self.assertEqual(r.status_code, 400)
|
||||
self.assertEqual(r.json(), "A client must have at least 1 site.")
|
||||
|
||||
# test successful deletion
|
||||
agent.delete()
|
||||
site_to_move.client = client # type: ignore
|
||||
site_to_move.save(update_fields=["client"]) # type: ignore
|
||||
r = self.client.delete(url, format="json")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertFalse(Site.objects.filter(pk=site.id).exists())
|
||||
agent_moved = Agent.objects.get(pk=agent.pk)
|
||||
self.assertEqual(agent_moved.site.id, site_to_move.id) # type: ignore
|
||||
|
||||
self.check_not_authenticated("delete", url)
|
||||
|
||||
@@ -233,7 +396,7 @@ class TestClientViews(TacticalTestCase):
|
||||
r = self.client.get(url, format="json")
|
||||
serializer = ClientTreeSerializer(clients, many=True)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(r.data, serializer.data)
|
||||
self.assertEqual(r.data, serializer.data) # type: ignore
|
||||
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
@@ -245,7 +408,7 @@ class TestClientViews(TacticalTestCase):
|
||||
r = self.client.get(url)
|
||||
serializer = DeploymentSerializer(deployments, many=True)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(r.data, serializer.data)
|
||||
self.assertEqual(r.data, serializer.data) # type: ignore
|
||||
|
||||
self.check_not_authenticated("get", url)
|
||||
|
||||
@@ -255,8 +418,8 @@ class TestClientViews(TacticalTestCase):
|
||||
|
||||
url = "/clients/deployments/"
|
||||
payload = {
|
||||
"client": site.client.id,
|
||||
"site": site.id,
|
||||
"client": site.client.id, # type: ignore
|
||||
"site": site.id, # type: ignore
|
||||
"expires": "2037-11-23 18:53",
|
||||
"power": 1,
|
||||
"ping": 0,
|
||||
@@ -284,10 +447,10 @@ class TestClientViews(TacticalTestCase):
|
||||
|
||||
url = "/clients/deployments/"
|
||||
|
||||
url = f"/clients/{deployment.id}/deployment/"
|
||||
url = f"/clients/{deployment.id}/deployment/" # type: ignore
|
||||
r = self.client.delete(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertFalse(Deployment.objects.filter(pk=deployment.id).exists())
|
||||
self.assertFalse(Deployment.objects.filter(pk=deployment.id).exists()) # type: ignore
|
||||
|
||||
url = "/clients/32348/deployment/"
|
||||
r = self.client.delete(url)
|
||||
@@ -301,7 +464,7 @@ class TestClientViews(TacticalTestCase):
|
||||
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 400)
|
||||
self.assertEqual(r.data, "invalid")
|
||||
self.assertEqual(r.data, "invalid") # type: ignore
|
||||
|
||||
uid = uuid.uuid4()
|
||||
url = f"/clients/{uid}/deploy/"
|
||||
|
||||
@@ -4,10 +4,12 @@ from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path("clients/", views.GetAddClients.as_view()),
|
||||
path("<int:pk>/client/", views.GetUpdateDeleteClient.as_view()),
|
||||
path("<int:pk>/client/", views.GetUpdateClient.as_view()),
|
||||
path("<int:pk>/<int:sitepk>/", views.DeleteClient.as_view()),
|
||||
path("tree/", views.GetClientTree.as_view()),
|
||||
path("sites/", views.GetAddSites.as_view()),
|
||||
path("<int:pk>/site/", views.GetUpdateDeleteSite.as_view()),
|
||||
path("sites/<int:pk>/", views.GetUpdateSite.as_view()),
|
||||
path("sites/<int:pk>/<int:sitepk>/", views.DeleteSite.as_view()),
|
||||
path("deployments/", views.AgentDeployment.as_view()),
|
||||
path("<int:pk>/deployment/", views.AgentDeployment.as_view()),
|
||||
path("<str:uid>/deploy/", views.GenerateAgent.as_view()),
|
||||
|
||||
@@ -12,13 +12,15 @@ 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,
|
||||
)
|
||||
|
||||
@@ -29,45 +31,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 +140,88 @@ class GetAddSites(APIView):
|
||||
return Response(SiteSerializer(sites, many=True).data)
|
||||
|
||||
def post(self, request):
|
||||
name = request.data["name"].strip()
|
||||
serializer = SiteSerializer(
|
||||
data={"name": name, "client": request.data["client"]},
|
||||
context={"clientpk": request.data["client"]},
|
||||
)
|
||||
serializer = SiteSerializer(data=request.data["site"])
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
site = serializer.save()
|
||||
|
||||
return Response("ok")
|
||||
# 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 GetUpdateDeleteSite(APIView):
|
||||
def put(self, request, pk):
|
||||
|
||||
class GetUpdateSite(APIView):
|
||||
def get(self, request, pk):
|
||||
site = get_object_or_404(Site, pk=pk)
|
||||
serializer = SiteSerializer(instance=site, data=request.data, partial=True)
|
||||
return Response(SiteSerializer(site).data)
|
||||
|
||||
def put(self, request, pk):
|
||||
site = get_object_or_404(Site, pk=pk)
|
||||
|
||||
if (
|
||||
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(instance=site, data=request.data["site"])
|
||||
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 DeleteSite(APIView):
|
||||
def delete(self, request, pk, sitepk):
|
||||
from automation.tasks import generate_all_agent_checks_task
|
||||
|
||||
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 +278,10 @@ class GenerateAgent(APIView):
|
||||
permission_classes = (AllowAny,)
|
||||
|
||||
def get(self, request, uid):
|
||||
import requests
|
||||
import tempfile
|
||||
from django.http import FileResponse
|
||||
|
||||
try:
|
||||
_ = uuid.UUID(uid, version=4)
|
||||
except ValueError:
|
||||
@@ -190,18 +299,36 @@ 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:
|
||||
r = requests.post(
|
||||
settings.EXE_GEN_URL,
|
||||
json=data,
|
||||
headers=headers,
|
||||
stream=True,
|
||||
)
|
||||
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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Binary file not shown.
@@ -1,5 +0,0 @@
|
||||
module github.com/wh1te909/goinstaller
|
||||
|
||||
go 1.16
|
||||
|
||||
require github.com/josephspurrier/goversioninfo v1.2.0 // indirect
|
||||
@@ -1,10 +0,0 @@
|
||||
github.com/akavel/rsrc v0.8.0 h1:zjWn7ukO9Kc5Q62DOJCcxGpXC18RawVtYAGdz2aLlfw=
|
||||
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/josephspurrier/goversioninfo v1.2.0 h1:tpLHXAxLHKHg/dCU2AAYx08A4m+v9/CWg6+WUvTF4uQ=
|
||||
github.com/josephspurrier/goversioninfo v1.2.0/go.mod h1:AGP2a+Y/OVJZ+s6XM4IwFUpkETwvn0orYurY8qpw1+0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -1,17 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<assemblyIdentity
|
||||
type="win32"
|
||||
name="TacticalRMMInstaller"
|
||||
version="1.0.0.0"
|
||||
processorArchitecture="*"/>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel
|
||||
level="requireAdministrator"
|
||||
uiAccess="false"/>
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
</assembly>
|
||||
@@ -1,186 +0,0 @@
|
||||
//go:generate goversioninfo -icon=onit.ico -manifest=goversioninfo.exe.manifest -gofile=versioninfo.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
Inno string
|
||||
Api string
|
||||
Client string
|
||||
Site string
|
||||
Atype string
|
||||
Power string
|
||||
Rdp string
|
||||
Ping string
|
||||
Token string
|
||||
DownloadUrl string
|
||||
)
|
||||
|
||||
var netTransport = &http.Transport{
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: 5 * time.Second,
|
||||
}).Dial,
|
||||
TLSHandshakeTimeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
var netClient = &http.Client{
|
||||
Timeout: time.Second * 900,
|
||||
Transport: netTransport,
|
||||
}
|
||||
|
||||
func downloadAgent(filepath string) (err error) {
|
||||
|
||||
out, err := os.Create(filepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
resp, err := netClient.Get(DownloadUrl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("Bad response: %s", resp.Status)
|
||||
}
|
||||
|
||||
_, err = io.Copy(out, resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
debugLog := flag.String("log", "", "Verbose output")
|
||||
localMesh := flag.String("local-mesh", "", "Use local mesh agent")
|
||||
silent := flag.Bool("silent", false, "Do not popup any message boxes during installation")
|
||||
cert := flag.String("cert", "", "Path to ca.pem")
|
||||
flag.Parse()
|
||||
|
||||
var debug bool = false
|
||||
|
||||
if strings.TrimSpace(strings.ToLower(*debugLog)) == "debug" {
|
||||
debug = true
|
||||
}
|
||||
|
||||
agentBinary := filepath.Join(os.Getenv("windir"), "Temp", Inno)
|
||||
tacrmm := filepath.Join(os.Getenv("PROGRAMFILES"), "TacticalAgent", "tacticalrmm.exe")
|
||||
|
||||
cmdArgs := []string{
|
||||
"-m", "install", "--api", Api, "--client-id",
|
||||
Client, "--site-id", Site, "--agent-type", Atype,
|
||||
"--auth", Token,
|
||||
}
|
||||
|
||||
if debug {
|
||||
cmdArgs = append(cmdArgs, "-log", "debug")
|
||||
}
|
||||
|
||||
if *silent {
|
||||
cmdArgs = append(cmdArgs, "-silent")
|
||||
}
|
||||
|
||||
if len(strings.TrimSpace(*localMesh)) != 0 {
|
||||
cmdArgs = append(cmdArgs, "-local-mesh", *localMesh)
|
||||
}
|
||||
|
||||
if len(strings.TrimSpace(*cert)) != 0 {
|
||||
cmdArgs = append(cmdArgs, "-cert", *cert)
|
||||
}
|
||||
|
||||
if Rdp == "1" {
|
||||
cmdArgs = append(cmdArgs, "-rdp")
|
||||
}
|
||||
|
||||
if Ping == "1" {
|
||||
cmdArgs = append(cmdArgs, "-ping")
|
||||
}
|
||||
|
||||
if Power == "1" {
|
||||
cmdArgs = append(cmdArgs, "-power")
|
||||
}
|
||||
|
||||
if debug {
|
||||
fmt.Println("Installer:", agentBinary)
|
||||
fmt.Println("Tactical Agent:", tacrmm)
|
||||
fmt.Println("Download URL:", DownloadUrl)
|
||||
fmt.Println("Install command:", tacrmm, strings.Join(cmdArgs, " "))
|
||||
}
|
||||
|
||||
fmt.Println("Downloading agent...")
|
||||
dl := downloadAgent(agentBinary)
|
||||
if dl != nil {
|
||||
fmt.Println("ERROR: unable to download agent from", DownloadUrl)
|
||||
fmt.Println(dl)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer os.Remove(agentBinary)
|
||||
|
||||
fmt.Println("Extracting files...")
|
||||
winagentCmd := exec.Command(agentBinary, "/VERYSILENT", "/SUPPRESSMSGBOXES")
|
||||
err := winagentCmd.Run()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
fmt.Println("Installation starting.")
|
||||
cmd := exec.Command(tacrmm, cmdArgs...)
|
||||
|
||||
cmdReader, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
return
|
||||
}
|
||||
|
||||
cmdErrReader, oerr := cmd.StderrPipe()
|
||||
if oerr != nil {
|
||||
fmt.Fprintln(os.Stderr, oerr)
|
||||
return
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(cmdReader)
|
||||
escanner := bufio.NewScanner(cmdErrReader)
|
||||
go func() {
|
||||
for scanner.Scan() {
|
||||
fmt.Println(scanner.Text())
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
for escanner.Scan() {
|
||||
fmt.Println(escanner.Text())
|
||||
}
|
||||
}()
|
||||
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 48 KiB |
@@ -1,43 +0,0 @@
|
||||
{
|
||||
"FixedFileInfo": {
|
||||
"FileVersion": {
|
||||
"Major": 1,
|
||||
"Minor": 0,
|
||||
"Patch": 0,
|
||||
"Build": 0
|
||||
},
|
||||
"ProductVersion": {
|
||||
"Major": 1,
|
||||
"Minor": 0,
|
||||
"Patch": 0,
|
||||
"Build": 0
|
||||
},
|
||||
"FileFlagsMask": "3f",
|
||||
"FileFlags ": "00",
|
||||
"FileOS": "040004",
|
||||
"FileType": "01",
|
||||
"FileSubType": "00"
|
||||
},
|
||||
"StringFileInfo": {
|
||||
"Comments": "",
|
||||
"CompanyName": "Tactical Techs",
|
||||
"FileDescription": "Tactical RMM Installer",
|
||||
"FileVersion": "v1.0.0.0",
|
||||
"InternalName": "rmm.exe",
|
||||
"LegalCopyright": "Copyright (c) 2020 Tactical Techs",
|
||||
"LegalTrademarks": "",
|
||||
"OriginalFilename": "installer.go",
|
||||
"PrivateBuild": "",
|
||||
"ProductName": "Tactical RMM Installer",
|
||||
"ProductVersion": "v1.0.0.0",
|
||||
"SpecialBuild": ""
|
||||
},
|
||||
"VarFileInfo": {
|
||||
"Translation": {
|
||||
"LangID": "0409",
|
||||
"CharsetID": "04B0"
|
||||
}
|
||||
},
|
||||
"IconPath": "",
|
||||
"ManifestPath": ""
|
||||
}
|
||||
27
api/tacticalrmm/core/migrations/0014_customfield.py
Normal file
27
api/tacticalrmm/core/migrations/0014_customfield.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# Generated by Django 3.1.7 on 2021-03-17 14:45
|
||||
|
||||
import django.contrib.postgres.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0013_coresettings_alert_template'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CustomField',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('order', models.PositiveIntegerField()),
|
||||
('model', models.CharField(choices=[('client', 'Client'), ('site', 'Site'), ('agent', 'Agent')], max_length=25)),
|
||||
('type', models.CharField(choices=[('text', 'Text'), ('number', 'Number'), ('single', 'Single'), ('multiple', 'Multiple'), ('checkbox', 'Checkbox'), ('datetime', 'DateTime')], default='text', max_length=25)),
|
||||
('options', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=255, null=True), blank=True, default=list, null=True, size=None)),
|
||||
('name', models.TextField(blank=True, null=True)),
|
||||
('default_value', models.TextField(blank=True, null=True)),
|
||||
('required', models.BooleanField(blank=True, default=False)),
|
||||
],
|
||||
),
|
||||
]
|
||||
18
api/tacticalrmm/core/migrations/0015_auto_20210318_2034.py
Normal file
18
api/tacticalrmm/core/migrations/0015_auto_20210318_2034.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.7 on 2021-03-18 20:34
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0014_customfield'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='customfield',
|
||||
name='order',
|
||||
field=models.PositiveIntegerField(default=0),
|
||||
),
|
||||
]
|
||||
17
api/tacticalrmm/core/migrations/0016_auto_20210319_1536.py
Normal file
17
api/tacticalrmm/core/migrations/0016_auto_20210319_1536.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 3.1.7 on 2021-03-19 15:36
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0015_auto_20210318_2034'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name='customfield',
|
||||
unique_together={('model', 'name')},
|
||||
),
|
||||
]
|
||||
24
api/tacticalrmm/core/migrations/0017_auto_20210329_1050.py
Normal file
24
api/tacticalrmm/core/migrations/0017_auto_20210329_1050.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 3.1.7 on 2021-03-29 10:50
|
||||
|
||||
import django.contrib.postgres.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0016_auto_20210319_1536'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='customfield',
|
||||
name='checkbox_value',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='customfield',
|
||||
name='default_values_multiple',
|
||||
field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=255, null=True), blank=True, default=list, null=True, size=None),
|
||||
),
|
||||
]
|
||||
23
api/tacticalrmm/core/migrations/0018_auto_20210329_1709.py
Normal file
23
api/tacticalrmm/core/migrations/0018_auto_20210329_1709.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.1.7 on 2021-03-29 17:09
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0017_auto_20210329_1050'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='customfield',
|
||||
old_name='checkbox_value',
|
||||
new_name='default_value_bool',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='customfield',
|
||||
old_name='default_value',
|
||||
new_name='default_value_string',
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import pytz
|
||||
from rest_framework import serializers
|
||||
|
||||
from .models import CoreSettings
|
||||
from .models import CoreSettings, CustomField
|
||||
|
||||
|
||||
class CoreSettingsSerializer(serializers.ModelSerializer):
|
||||
@@ -21,3 +21,9 @@ class CoreSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = CoreSettings
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class CustomFieldSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = CustomField
|
||||
fields = "__all__"
|
||||
|
||||
@@ -1,11 +1,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)
|
||||
|
||||
@@ -10,4 +10,6 @@ urlpatterns = [
|
||||
path("emailtest/", views.email_test),
|
||||
path("dashinfo/", views.dashboard_info),
|
||||
path("servermaintenance/", views.server_maintenance),
|
||||
path("customfields/", views.GetAddCustomFields.as_view()),
|
||||
path("customfields/<int:pk>/", views.GetUpdateDeleteCustomFields.as_view()),
|
||||
]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import os
|
||||
|
||||
from django.conf import settings
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.exceptions import ParseError
|
||||
@@ -10,8 +11,8 @@ from rest_framework.views import APIView
|
||||
|
||||
from tacticalrmm.utils import notify_error
|
||||
|
||||
from .models import CoreSettings
|
||||
from .serializers import CoreSettingsSerializer
|
||||
from .models import CoreSettings, CustomField
|
||||
from .serializers import CoreSettingsSerializer, CustomFieldSerializer
|
||||
|
||||
|
||||
class UploadMeshAgent(APIView):
|
||||
@@ -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")
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class NatsapiConfig(AppConfig):
|
||||
name = "natsapi"
|
||||
@@ -1,36 +0,0 @@
|
||||
from django.conf import settings
|
||||
from model_bakery import baker
|
||||
|
||||
from tacticalrmm.test import TacticalTestCase
|
||||
|
||||
|
||||
class TestNatsAPIViews(TacticalTestCase):
|
||||
def setUp(self):
|
||||
self.authenticate()
|
||||
self.setup_coresettings()
|
||||
|
||||
def test_nats_agents(self):
|
||||
baker.make_recipe(
|
||||
"agents.online_agent", version=settings.LATEST_AGENT_VER, _quantity=14
|
||||
)
|
||||
|
||||
baker.make_recipe(
|
||||
"agents.offline_agent", version=settings.LATEST_AGENT_VER, _quantity=6
|
||||
)
|
||||
baker.make_recipe(
|
||||
"agents.overdue_agent", version=settings.LATEST_AGENT_VER, _quantity=5
|
||||
)
|
||||
|
||||
url = "/natsapi/online/agents/"
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(len(r.json()["agent_ids"]), 14)
|
||||
|
||||
url = "/natsapi/offline/agents/"
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(len(r.json()["agent_ids"]), 11)
|
||||
|
||||
url = "/natsapi/asdjaksdasd/agents/"
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 400)
|
||||
@@ -1,9 +0,0 @@
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path("natsinfo/", views.nats_info),
|
||||
path("<str:stat>/agents/", views.NatsAgents.as_view()),
|
||||
path("logcrash/", views.LogCrash.as_view()),
|
||||
]
|
||||
@@ -1,60 +0,0 @@
|
||||
from django.conf import settings
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils import timezone as djangotime
|
||||
from loguru import logger
|
||||
from rest_framework.decorators import (
|
||||
api_view,
|
||||
authentication_classes,
|
||||
permission_classes,
|
||||
)
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from agents.models import Agent
|
||||
from tacticalrmm.utils import notify_error
|
||||
|
||||
logger.configure(**settings.LOG_CONFIG)
|
||||
|
||||
|
||||
@api_view()
|
||||
@permission_classes([])
|
||||
@authentication_classes([])
|
||||
def nats_info(request):
|
||||
return Response({"user": "tacticalrmm", "password": settings.SECRET_KEY})
|
||||
|
||||
|
||||
class NatsAgents(APIView):
|
||||
authentication_classes = [] # type: ignore
|
||||
permission_classes = [] # type: ignore
|
||||
|
||||
def get(self, request, stat: str):
|
||||
if stat not in ["online", "offline"]:
|
||||
return notify_error("invalid request")
|
||||
|
||||
ret: list[str] = []
|
||||
agents = Agent.objects.only(
|
||||
"pk", "agent_id", "version", "last_seen", "overdue_time", "offline_time"
|
||||
)
|
||||
if stat == "online":
|
||||
ret = [i.agent_id for i in agents if i.status == "online"]
|
||||
else:
|
||||
ret = [i.agent_id for i in agents if i.status != "online"]
|
||||
|
||||
return Response({"agent_ids": ret})
|
||||
|
||||
|
||||
class LogCrash(APIView):
|
||||
authentication_classes = [] # type: ignore
|
||||
permission_classes = [] # type: ignore
|
||||
|
||||
def post(self, request):
|
||||
agent = get_object_or_404(Agent, agent_id=request.data["agentid"])
|
||||
agent.last_seen = djangotime.now()
|
||||
agent.save(update_fields=["last_seen"])
|
||||
|
||||
if hasattr(settings, "DEBUGTEST") and settings.DEBUGTEST:
|
||||
logger.info(
|
||||
f"Detected crashed tacticalagent service on {agent.hostname} v{agent.version}, attempting recovery"
|
||||
)
|
||||
|
||||
return Response("ok")
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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"),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ def get_debug_info():
|
||||
|
||||
|
||||
EXCLUDE_PATHS = (
|
||||
"/natsapi",
|
||||
"/api/v3",
|
||||
"/logs/auditlogs",
|
||||
f"/{settings.ADMIN_URL}",
|
||||
|
||||
@@ -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.31"
|
||||
|
||||
# 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.124"
|
||||
|
||||
# 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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
15
backup.sh
15
backup.sh
@@ -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/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:12-alpine AS builder
|
||||
FROM node:14-alpine AS builder
|
||||
|
||||
WORKDIR /home/node/app
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:12-alpine
|
||||
FROM node:14-alpine
|
||||
|
||||
WORKDIR /home/node/app
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
)"
|
||||
|
||||
|
||||
@@ -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 /
|
||||
|
||||
@@ -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..."
|
||||
|
||||
19
docs/docs/functions/django_admin.md
Normal file
19
docs/docs/functions/django_admin.md
Normal 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/
|
||||
```
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
2
go.mod
@@ -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
9
go.sum
@@ -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=
|
||||
|
||||
43
install.sh
43
install.sh
@@ -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
17
main.go
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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.
139
natsapi/tasks.go
139
natsapi/tasks.go
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
23
restore.sh
23
restore.sh
@@ -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"
|
||||
|
||||
@@ -20,4 +20,4 @@ Try {
|
||||
Catch {
|
||||
Write-Host "Script Check Failed"
|
||||
Exit 1001
|
||||
}
|
||||
}
|
||||
@@ -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
194
update.sh
@@ -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
115
web/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
},
|
||||
|
||||
@@ -133,7 +133,6 @@ export default {
|
||||
})
|
||||
.catch(e => {
|
||||
this.$q.loading.hide();
|
||||
console.log({ e });
|
||||
this.notifyError("There was an issue resolving alert");
|
||||
});
|
||||
},
|
||||
|
||||
189
web/src/components/ClientsManager.vue
Normal file
189
web/src/components/ClientsManager.vue
Normal 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>
|
||||
104
web/src/components/CustomField.vue
Normal file
104
web/src/components/CustomField.vue
Normal 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>
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
},
|
||||
|
||||
@@ -291,6 +291,7 @@
|
||||
props.row.description
|
||||
}}</q-tooltip>
|
||||
</q-td>
|
||||
<q-td>{{ props.row.default_timeout }}</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
@@ -368,8 +369,15 @@ export default {
|
||||
align: "left",
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
name: "default_timeout",
|
||||
label: "Default Timeout (seconds)",
|
||||
field: "default_timeout",
|
||||
align: "left",
|
||||
sortable: true,
|
||||
},
|
||||
],
|
||||
visibleColumns: ["favorite", "name", "category", "desc", "shell"],
|
||||
visibleColumns: ["favorite", "name", "category", "desc", "shell", "default_timeout"],
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user