From 0cb8ccfddd707da14e9bc0d65843a69a3d646e93 Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Sun, 13 Nov 2022 01:53:05 +0000 Subject: [PATCH 01/22] back to dev [skip ci] --- api/tacticalrmm/tacticalrmm/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/tacticalrmm/tacticalrmm/settings.py b/api/tacticalrmm/tacticalrmm/settings.py index f49f25007e..aea0819baf 100644 --- a/api/tacticalrmm/tacticalrmm/settings.py +++ b/api/tacticalrmm/tacticalrmm/settings.py @@ -20,7 +20,7 @@ AUTH_USER_MODEL = "accounts.User" # latest release -TRMM_VERSION = "0.15.3" +TRMM_VERSION = "0.15.4-dev" # https://github.com/amidaware/tacticalrmm-web WEB_VERSION = "0.101.7" From 2c37d2233a543c2377ede210a02bcea1516b4056 Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Sun, 13 Nov 2022 07:44:23 +0000 Subject: [PATCH 02/22] add flake8 --- .github/workflows/ci-tests.yml | 8 ++++++++ api/tacticalrmm/.flake8 | 12 ++++++++++++ api/tacticalrmm/accounts/tests.py | 2 +- api/tacticalrmm/accounts/views.py | 3 ++- api/tacticalrmm/agents/tests/test_agent_update.py | 2 +- api/tacticalrmm/agents/tests/test_agent_utils.py | 3 +-- api/tacticalrmm/agents/tests/test_agents.py | 4 ++-- api/tacticalrmm/agents/views.py | 6 +++--- api/tacticalrmm/alerts/tests.py | 2 -- api/tacticalrmm/apiv3/tests/tests.py | 6 ++---- api/tacticalrmm/apiv3/views.py | 2 +- api/tacticalrmm/automation/tests.py | 2 +- api/tacticalrmm/automation/views.py | 1 + api/tacticalrmm/autotasks/models.py | 6 ++---- api/tacticalrmm/autotasks/serializers.py | 14 +++++++------- api/tacticalrmm/autotasks/tests.py | 14 +++++++++----- api/tacticalrmm/checks/models.py | 4 +--- api/tacticalrmm/checks/serializers.py | 12 ++++++------ api/tacticalrmm/checks/tests.py | 8 ++++---- api/tacticalrmm/clients/tests.py | 6 +++--- api/tacticalrmm/core/models.py | 4 ++-- api/tacticalrmm/core/tests.py | 9 +++------ api/tacticalrmm/core/utils.py | 2 +- api/tacticalrmm/logs/apps.py | 2 +- api/tacticalrmm/logs/tests.py | 6 +++--- api/tacticalrmm/requirements-test.txt | 3 ++- api/tacticalrmm/scripts/tests.py | 4 ++-- api/tacticalrmm/software/tests.py | 8 +++++--- api/tacticalrmm/tacticalrmm/__init__.py | 2 +- api/tacticalrmm/tacticalrmm/asgi.py | 4 ++-- api/tacticalrmm/tacticalrmm/settings.py | 8 ++++---- api/tacticalrmm/tacticalrmm/test.py | 2 -- api/tacticalrmm/tacticalrmm/utils.py | 8 ++++---- api/tacticalrmm/winupdate/views.py | 1 + 34 files changed, 98 insertions(+), 82 deletions(-) create mode 100644 api/tacticalrmm/.flake8 diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 90fac09f48..e495f09eb4 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -56,6 +56,14 @@ jobs: if [ $? -ne 0 ]; then exit 1 fi + + - name: Lint with flake8 + working-directory: api/tacticalrmm + run: | + flake8 --config .flake8 . + if [ $? -ne 0 ]; then + exit 1 + fi - name: Run django tests env: diff --git a/api/tacticalrmm/.flake8 b/api/tacticalrmm/.flake8 new file mode 100644 index 0000000000..3f1049b11f --- /dev/null +++ b/api/tacticalrmm/.flake8 @@ -0,0 +1,12 @@ +[flake8] +ignore = E501,W503,E722,E203 +exclude = + .mypy* + .pytest* + .git + demo_data.py + manage.py + */__pycache__/* + */env/* + /usr/local/lib/* + **/migrations/* diff --git a/api/tacticalrmm/accounts/tests.py b/api/tacticalrmm/accounts/tests.py index adee565d90..f8b1d270d2 100644 --- a/api/tacticalrmm/accounts/tests.py +++ b/api/tacticalrmm/accounts/tests.py @@ -197,7 +197,7 @@ def test_delete(self): r = self.client.delete(url) self.assertEqual(r.status_code, 200) - url = f"/accounts/893452/users/" + url = "/accounts/893452/users/" r = self.client.delete(url) self.assertEqual(r.status_code, 404) diff --git a/api/tacticalrmm/accounts/views.py b/api/tacticalrmm/accounts/views.py index 5d9394e5d0..8b194d5a28 100644 --- a/api/tacticalrmm/accounts/views.py +++ b/api/tacticalrmm/accounts/views.py @@ -169,6 +169,7 @@ def delete(self, request, pk): class UserActions(APIView): permission_classes = [IsAuthenticated, AccountsPerms] + # reset password def post(self, request): user = get_object_or_404(User, pk=request.data["id"]) @@ -267,7 +268,7 @@ def post(self, request): request.data["key"] = get_random_string(length=32).upper() serializer = APIKeySerializer(data=request.data) serializer.is_valid(raise_exception=True) - obj = serializer.save() + serializer.save() return Response("The API Key was added") diff --git a/api/tacticalrmm/agents/tests/test_agent_update.py b/api/tacticalrmm/agents/tests/test_agent_update.py index 7a792a6362..d73e00a694 100644 --- a/api/tacticalrmm/agents/tests/test_agent_update.py +++ b/api/tacticalrmm/agents/tests/test_agent_update.py @@ -264,7 +264,7 @@ def test_agent_update_permissions(self, update_task, mock_token): agents = baker.make_recipe("agents.agent", _quantity=5) other_agents = baker.make_recipe("agents.agent", _quantity=7) - url = f"/agents/update/" + url = "/agents/update/" data = { "agent_ids": [agent.agent_id for agent in agents] diff --git a/api/tacticalrmm/agents/tests/test_agent_utils.py b/api/tacticalrmm/agents/tests/test_agent_utils.py index 604beb3c0b..a6bdc7cc41 100644 --- a/api/tacticalrmm/agents/tests/test_agent_utils.py +++ b/api/tacticalrmm/agents/tests/test_agent_utils.py @@ -1,7 +1,6 @@ -from unittest.mock import patch, AsyncMock +from unittest.mock import patch from django.conf import settings -from rest_framework.response import Response from agents.utils import generate_linux_install, get_agent_url from tacticalrmm.test import TacticalTestCase diff --git a/api/tacticalrmm/agents/tests/test_agents.py b/api/tacticalrmm/agents/tests/test_agents.py index 4acc66f47a..01d9543b7b 100644 --- a/api/tacticalrmm/agents/tests/test_agents.py +++ b/api/tacticalrmm/agents/tests/test_agents.py @@ -1272,9 +1272,9 @@ def test_get_agent_history_permissions(self): sites = baker.make("clients.Site", _quantity=2) agent = baker.make_recipe("agents.agent", site=sites[0]) - history = baker.make("agents.AgentHistory", agent=agent, _quantity=5) + history = baker.make("agents.AgentHistory", agent=agent, _quantity=5) # noqa unauthorized_agent = baker.make_recipe("agents.agent", site=sites[1]) - unauthorized_history = baker.make( + unauthorized_history = baker.make( # noqa "agents.AgentHistory", agent=unauthorized_agent, _quantity=6 ) diff --git a/api/tacticalrmm/agents/views.py b/api/tacticalrmm/agents/views.py index 1a076fe35c..f54753be23 100644 --- a/api/tacticalrmm/agents/views.py +++ b/api/tacticalrmm/agents/views.py @@ -7,7 +7,7 @@ from pathlib import Path from django.conf import settings -from django.db.models import Count, Exists, OuterRef, Prefetch, Q +from django.db.models import Exists, OuterRef, Prefetch, Q from django.http import HttpResponse from django.shortcuts import get_object_or_404 from django.utils import timezone as djangotime @@ -41,7 +41,6 @@ DebugLogType, EvtLogNames, PAAction, - PAStatus, ) from tacticalrmm.helpers import date_is_in_past, notify_error from tacticalrmm.permissions import ( @@ -461,6 +460,7 @@ def send_raw_cmd(request, agent_id): class Reboot(APIView): permission_classes = [IsAuthenticated, RebootAgentPerms] + # reboot now def post(self, request, agent_id): agent = get_object_or_404(Agent, agent_id=agent_id) @@ -1005,7 +1005,7 @@ def agent_maintenance(request): return Response(f"Maintenance mode has been {action} on {count} agents") return Response( - f"No agents have been put in maintenance mode. You might not have permissions to the resources." + "No agents have been put in maintenance mode. You might not have permissions to the resources." ) diff --git a/api/tacticalrmm/alerts/tests.py b/api/tacticalrmm/alerts/tests.py index 5896496644..058b0ed74f 100644 --- a/api/tacticalrmm/alerts/tests.py +++ b/api/tacticalrmm/alerts/tests.py @@ -677,8 +677,6 @@ def test_handle_agent_alerts( agent_template_email = Agent.objects.get(pk=agent_template_email.pk) # have the two agents checkin - url = "/api/v3/checkin/" - agent_template_text.version = settings.LATEST_AGENT_VER agent_template_text.last_seen = djangotime.now() agent_template_text.save() diff --git a/api/tacticalrmm/apiv3/tests/tests.py b/api/tacticalrmm/apiv3/tests/tests.py index e7b49537d6..b6260b0bb4 100644 --- a/api/tacticalrmm/apiv3/tests/tests.py +++ b/api/tacticalrmm/apiv3/tests/tests.py @@ -77,9 +77,7 @@ def test_checkrunner_interval(self): ) # add check to agent with check interval set - check = baker.make_recipe( - "checks.ping_check", agent=self.agent, run_interval=30 - ) + baker.make_recipe("checks.ping_check", agent=self.agent, run_interval=30) r = self.client.get(url, format="json") self.assertEqual(r.status_code, 200) @@ -89,7 +87,7 @@ def test_checkrunner_interval(self): ) # minimum check run interval is 15 seconds - check = baker.make_recipe("checks.ping_check", agent=self.agent, run_interval=5) + baker.make_recipe("checks.ping_check", agent=self.agent, run_interval=5) r = self.client.get(url, format="json") self.assertEqual(r.status_code, 200) diff --git a/api/tacticalrmm/apiv3/views.py b/api/tacticalrmm/apiv3/views.py index 872ca8f633..34f250eb17 100644 --- a/api/tacticalrmm/apiv3/views.py +++ b/api/tacticalrmm/apiv3/views.py @@ -516,7 +516,7 @@ def post(self, request): ver = request.data["version"] if ( pyver.parse(ver) < pyver.parse(settings.LATEST_AGENT_VER) - and not "-dev" in settings.LATEST_AGENT_VER + and "-dev" not in settings.LATEST_AGENT_VER ): return notify_error( f"Old installer detected (version {ver} ). Latest version is {settings.LATEST_AGENT_VER} Please generate a new installer from the RMM" diff --git a/api/tacticalrmm/automation/tests.py b/api/tacticalrmm/automation/tests.py index 2bdaf0c868..2b83cfdfdd 100644 --- a/api/tacticalrmm/automation/tests.py +++ b/api/tacticalrmm/automation/tests.py @@ -87,7 +87,7 @@ def test_add_policy(self, create_task): "copyId": policy.pk, } - resp = self.client.post(f"/automation/policies/", data, format="json") + resp = self.client.post("/automation/policies/", data, format="json") self.assertEqual(resp.status_code, 200) copied_policy = Policy.objects.get(name=data["name"]) diff --git a/api/tacticalrmm/automation/views.py b/api/tacticalrmm/automation/views.py index 5b1d50df45..213347ecd4 100644 --- a/api/tacticalrmm/automation/views.py +++ b/api/tacticalrmm/automation/views.py @@ -146,6 +146,7 @@ def get(self, request, pk): class UpdatePatchPolicy(APIView): permission_classes = [IsAuthenticated, AutomationPolicyPerms] + # create new patch policy def post(self, request): policy = get_object_or_404(Policy, pk=request.data["policy"]) diff --git a/api/tacticalrmm/autotasks/models.py b/api/tacticalrmm/autotasks/models.py index e439c4258b..7105af7b9f 100644 --- a/api/tacticalrmm/autotasks/models.py +++ b/api/tacticalrmm/autotasks/models.py @@ -226,15 +226,13 @@ def serialize(task): def create_policy_task( self, policy: "Policy", assigned_check: "Optional[Check]" = None ) -> None: - ### Copies certain properties on this task (self) to a new task and sets it to the supplied Policy - fields_to_copy = POLICY_TASK_FIELDS_TO_COPY - + # Copies certain properties on this task (self) to a new task and sets it to the supplied Policy task = AutomatedTask.objects.create( policy=policy, assigned_check=assigned_check, ) - for field in fields_to_copy: + for field in POLICY_TASK_FIELDS_TO_COPY: setattr(task, field, getattr(self, field)) task.save() diff --git a/api/tacticalrmm/autotasks/serializers.py b/api/tacticalrmm/autotasks/serializers.py index 209ba6f6a4..18a4e2cc4b 100644 --- a/api/tacticalrmm/autotasks/serializers.py +++ b/api/tacticalrmm/autotasks/serializers.py @@ -33,40 +33,40 @@ def validate_actions(self, value): if not value: raise serializers.ValidationError( - f"There must be at least one action configured" + "There must be at least one action configured" ) for action in value: if "type" not in action: raise serializers.ValidationError( - f"Each action must have a type field of either 'script' or 'cmd'" + "Each action must have a type field of either 'script' or 'cmd'" ) if action["type"] == "script": if "script" not in action: raise serializers.ValidationError( - f"A script action type must have a 'script' field with primary key of script" + "A script action type must have a 'script' field with primary key of script" ) if "script_args" not in action: raise serializers.ValidationError( - f"A script action type must have a 'script_args' field with an array of arguments" + "A script action type must have a 'script_args' field with an array of arguments" ) if "timeout" not in action: raise serializers.ValidationError( - f"A script action type must have a 'timeout' field" + "A script action type must have a 'timeout' field" ) if action["type"] == "cmd": if "command" not in action: raise serializers.ValidationError( - f"A command action type must have a 'command' field" + "A command action type must have a 'command' field" ) if "timeout" not in action: raise serializers.ValidationError( - f"A command action type must have a 'timeout' field" + "A command action type must have a 'timeout' field" ) return value diff --git a/api/tacticalrmm/autotasks/tests.py b/api/tacticalrmm/autotasks/tests.py index f4ca0ee6d6..f88bc1a85c 100644 --- a/api/tacticalrmm/autotasks/tests.py +++ b/api/tacticalrmm/autotasks/tests.py @@ -51,7 +51,7 @@ def test_add_autotask(self, create_win_task_schedule): # setup data script = baker.make_recipe("scripts.script") agent = baker.make_recipe("agents.agent") - policy = baker.make("automation.Policy") + policy = baker.make("automation.Policy") # noqa check = baker.make_recipe("checks.diskspace_check", agent=agent) custom_field = baker.make("core.CustomField") @@ -258,7 +258,9 @@ def test_update_autotask(self): agent = baker.make_recipe("agents.agent") agent_task = baker.make("autotasks.AutomatedTask", agent=agent) policy = baker.make("automation.Policy") - policy_task = baker.make("autotasks.AutomatedTask", enabled=True, policy=policy) + policy_task = baker.make( # noqa + "autotasks.AutomatedTask", enabled=True, policy=policy + ) custom_field = baker.make("core.CustomField") script = baker.make("scripts.Script") @@ -766,12 +768,14 @@ def test_get_tasks_permissions(self): agent = baker.make_recipe("agents.agent") policy = baker.make("automation.Policy") unauthorized_agent = baker.make_recipe("agents.agent") - task = baker.make("autotasks.AutomatedTask", agent=agent, _quantity=5) - unauthorized_task = baker.make( + task = baker.make("autotasks.AutomatedTask", agent=agent, _quantity=5) # noqa + unauthorized_task = baker.make( # noqa "autotasks.AutomatedTask", agent=unauthorized_agent, _quantity=7 ) - policy_tasks = baker.make("autotasks.AutomatedTask", policy=policy, _quantity=2) + policy_tasks = baker.make( # noqa + "autotasks.AutomatedTask", policy=policy, _quantity=2 + ) # test super user access self.check_authorized_superuser("get", f"{base_url}/") diff --git a/api/tacticalrmm/checks/models.py b/api/tacticalrmm/checks/models.py index c0d32e4fa8..5d6536ca8f 100644 --- a/api/tacticalrmm/checks/models.py +++ b/api/tacticalrmm/checks/models.py @@ -221,8 +221,6 @@ def non_editable_fields() -> list[str]: def create_policy_check(self, policy: "Policy") -> None: - fields_to_copy = POLICY_CHECK_FIELDS_TO_COPY - check = Check.objects.create( policy=policy, ) @@ -230,7 +228,7 @@ def create_policy_check(self, policy: "Policy") -> None: for task in self.assignedtasks.all(): # type: ignore task.create_policy_task(policy=policy, assigned_check=check) - for field in fields_to_copy: + for field in POLICY_CHECK_FIELDS_TO_COPY: setattr(check, field, getattr(self, field)) check.save() diff --git a/api/tacticalrmm/checks/serializers.py b/api/tacticalrmm/checks/serializers.py index 2c89a96d98..fb93a45174 100644 --- a/api/tacticalrmm/checks/serializers.py +++ b/api/tacticalrmm/checks/serializers.py @@ -82,7 +82,7 @@ def validate(self, val): if not val["warning_threshold"] and not val["error_threshold"]: raise serializers.ValidationError( - f"Warning threshold or Error Threshold must be set" + "Warning threshold or Error Threshold must be set" ) if ( @@ -91,7 +91,7 @@ def validate(self, val): and val["error_threshold"] > 0 ): raise serializers.ValidationError( - f"Warning threshold must be greater than Error Threshold" + "Warning threshold must be greater than Error Threshold" ) # ping checks @@ -113,7 +113,7 @@ def validate(self, val): if not val["warning_threshold"] and not val["error_threshold"]: raise serializers.ValidationError( - f"Warning threshold or Error Threshold must be set" + "Warning threshold or Error Threshold must be set" ) if ( @@ -122,7 +122,7 @@ def validate(self, val): and val["error_threshold"] > 0 ): raise serializers.ValidationError( - f"Warning threshold must be less than Error Threshold" + "Warning threshold must be less than Error Threshold" ) if check_type == CheckType.MEMORY and not self.instance: @@ -133,7 +133,7 @@ def validate(self, val): if not val["warning_threshold"] and not val["error_threshold"]: raise serializers.ValidationError( - f"Warning threshold or Error Threshold must be set" + "Warning threshold or Error Threshold must be set" ) if ( @@ -142,7 +142,7 @@ def validate(self, val): and val["error_threshold"] > 0 ): raise serializers.ValidationError( - f"Warning threshold must be less than Error Threshold" + "Warning threshold must be less than Error Threshold" ) return val diff --git a/api/tacticalrmm/checks/tests.py b/api/tacticalrmm/checks/tests.py index dafd4f2273..613a4dc995 100644 --- a/api/tacticalrmm/checks/tests.py +++ b/api/tacticalrmm/checks/tests.py @@ -41,7 +41,7 @@ def test_get_checks(self): self.assertEqual(len(resp.data), 4) # test agent doesn't exist - url = f"/agents/jh3498uf8fkh4ro8hfd8df98/checks/" + url = "/agents/jh3498uf8fkh4ro8hfd8df98/checks/" resp = self.client.get(url, format="json") self.assertEqual(resp.status_code, 404) @@ -884,12 +884,12 @@ def test_get_checks_permissions(self): agent = baker.make_recipe("agents.agent") policy = baker.make("automation.Policy") unauthorized_agent = baker.make_recipe("agents.agent") - check = baker.make("checks.Check", agent=agent, _quantity=5) - unauthorized_check = baker.make( + check = baker.make("checks.Check", agent=agent, _quantity=5) # noqa + unauthorized_check = baker.make( # noqa "checks.Check", agent=unauthorized_agent, _quantity=7 ) - policy_checks = baker.make("checks.Check", policy=policy, _quantity=2) + policy_checks = baker.make("checks.Check", policy=policy, _quantity=2) # noqa # test super user access self.check_authorized_superuser("get", f"{base_url}/") diff --git a/api/tacticalrmm/clients/tests.py b/api/tacticalrmm/clients/tests.py index 3d4432c2f3..92f66860f9 100644 --- a/api/tacticalrmm/clients/tests.py +++ b/api/tacticalrmm/clients/tests.py @@ -23,7 +23,7 @@ def setUp(self): def test_get_clients(self): # setup data baker.make("clients.Client", _quantity=5) - clients = Client.objects.all() + clients = Client.objects.all() # noqa url = f"{base_url}/" r = self.client.get(url, format="json") @@ -710,8 +710,8 @@ def test_get_pendingactions_permissions(self): site = baker.make("clients.Site") other_site = baker.make("clients.Site") - deployments = baker.make("clients.Deployment", site=site, _quantity=5) - other_deployments = baker.make( + deployments = baker.make("clients.Deployment", site=site, _quantity=5) # noqa + other_deployments = baker.make( # noqa "clients.Deployment", site=other_site, _quantity=7 ) diff --git a/api/tacticalrmm/core/models.py b/api/tacticalrmm/core/models.py index 0e87ce7248..3c5a1d808a 100644 --- a/api/tacticalrmm/core/models.py +++ b/api/tacticalrmm/core/models.py @@ -126,10 +126,10 @@ def save(self, *args, **kwargs) -> None: cache_agents_alert_template.delay() if old_settings.workstation_policy != self.workstation_policy: - cache.delete_many_pattern(f"site_workstation_*") + cache.delete_many_pattern("site_workstation_*") if old_settings.server_policy != self.server_policy: - cache.delete_many_pattern(f"site_server_*") + cache.delete_many_pattern("site_server_*") if ( old_settings.server_policy != self.server_policy diff --git a/api/tacticalrmm/core/tests.py b/api/tacticalrmm/core/tests.py index 85ea3b72f1..32bd7ae881 100644 --- a/api/tacticalrmm/core/tests.py +++ b/api/tacticalrmm/core/tests.py @@ -111,7 +111,7 @@ def test_edit_coresettings(self): url = "/core/settings/" # setup - policies = baker.make("automation.Policy", _quantity=2) + baker.make("automation.Policy", _quantity=2) # test normal request data = { "smtp_from_email": "newexample@example.com", @@ -129,7 +129,7 @@ def test_edit_coresettings(self): def test_ui_maintenance_actions(self, remove_orphaned_win_tasks, reload_nats): url = "/core/servermaintenance/" - agents = baker.make_recipe("agents.online_agent", _quantity=3) + baker.make_recipe("agents.online_agent", _quantity=3) # test with empty data r = self.client.post(url, {}) @@ -186,9 +186,7 @@ def test_get_custom_fields_by_model(self): url = "/core/customfields/" # setup - custom_fields = baker.make( - "core.CustomField", model=CustomFieldModel.AGENT, _quantity=5 - ) + baker.make("core.CustomField", model=CustomFieldModel.AGENT, _quantity=5) baker.make("core.CustomField", model="client", _quantity=5) # will error if request invalid @@ -197,7 +195,6 @@ def test_get_custom_fields_by_model(self): 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) diff --git a/api/tacticalrmm/core/utils.py b/api/tacticalrmm/core/utils.py index 1dd6c1fe65..9f715f9cd4 100644 --- a/api/tacticalrmm/core/utils.py +++ b/api/tacticalrmm/core/utils.py @@ -21,7 +21,7 @@ ) if TYPE_CHECKING: - from core.models import CodeSignToken, CoreSettings + from core.models import CoreSettings class CoreSettingsNotFound(Exception): diff --git a/api/tacticalrmm/logs/apps.py b/api/tacticalrmm/logs/apps.py index ac75a42914..9e148fad27 100644 --- a/api/tacticalrmm/logs/apps.py +++ b/api/tacticalrmm/logs/apps.py @@ -5,4 +5,4 @@ class LogsConfig(AppConfig): name = "logs" def ready(self): - from . import signals + from . import signals # noqa diff --git a/api/tacticalrmm/logs/tests.py b/api/tacticalrmm/logs/tests.py index 136ce135e9..01ef4787bd 100644 --- a/api/tacticalrmm/logs/tests.py +++ b/api/tacticalrmm/logs/tests.py @@ -250,7 +250,7 @@ def test_get_debug_log(self): _quantity=4, ) - logs = baker.make( + logs = baker.make( # noqa "logs.DebugLog", log_type=DebugLogType.SYSTEM_ISSUES, log_level=cycle([i.value for i in DebugLogLevel]), @@ -391,8 +391,8 @@ def test_debuglog_permissions(self): def test_get_pendingaction_permissions(self): agent = baker.make_recipe("agents.agent") unauthorized_agent = baker.make_recipe("agents.agent") - actions = baker.make("logs.PendingAction", agent=agent, _quantity=5) - unauthorized_actions = baker.make( + actions = baker.make("logs.PendingAction", agent=agent, _quantity=5) # noqa + unauthorized_actions = baker.make( # noqa "logs.PendingAction", agent=unauthorized_agent, _quantity=7 ) diff --git a/api/tacticalrmm/requirements-test.txt b/api/tacticalrmm/requirements-test.txt index 1a1be81b10..68d719408e 100644 --- a/api/tacticalrmm/requirements-test.txt +++ b/api/tacticalrmm/requirements-test.txt @@ -6,4 +6,5 @@ pytest-django pytest-xdist pytest-cov codecov -refurb \ No newline at end of file +refurb +flake8 \ No newline at end of file diff --git a/api/tacticalrmm/scripts/tests.py b/api/tacticalrmm/scripts/tests.py index a2b4c97d14..514e7b399e 100644 --- a/api/tacticalrmm/scripts/tests.py +++ b/api/tacticalrmm/scripts/tests.py @@ -37,7 +37,7 @@ def test_get_scripts(self): @override_settings(SECRET_KEY="Test Secret Key") def test_add_script(self): - url = f"/scripts/" + url = "/scripts/" data = { "name": "Name", @@ -453,7 +453,7 @@ def test_get_script_snippets(self): self.check_not_authenticated("get", url) def test_add_script_snippet(self): - url = f"/scripts/snippets/" + url = "/scripts/snippets/" data = { "name": "Name", diff --git a/api/tacticalrmm/software/tests.py b/api/tacticalrmm/software/tests.py index 5033f23c74..bfe99bc4df 100644 --- a/api/tacticalrmm/software/tests.py +++ b/api/tacticalrmm/software/tests.py @@ -145,7 +145,7 @@ def test_list_software_permissions(self): agent = baker.make_recipe("agents.agent") unauthorized_agent = baker.make_recipe("agents.agent") software = baker.make("software.InstalledSoftware", software={}, agent=agent) - unauthorized_software = baker.make( + unauthorized_software = baker.make( # noqa "software.InstalledSoftware", software={}, agent=unauthorized_agent ) @@ -180,8 +180,10 @@ def test_list_software_permissions(self): def test_install_refresh_software_permissions(self, nats_cmd): agent = baker.make_recipe("agents.agent") unauthorized_agent = baker.make_recipe("agents.agent") - software = baker.make("software.InstalledSoftware", software={}, agent=agent) - unauthorized_software = baker.make( + software = baker.make( # noqa + "software.InstalledSoftware", software={}, agent=agent + ) + unauthorized_software = baker.make( # noqa "software.InstalledSoftware", software={}, agent=unauthorized_agent ) diff --git a/api/tacticalrmm/tacticalrmm/__init__.py b/api/tacticalrmm/tacticalrmm/__init__.py index ee5ee8b206..0da1386dff 100644 --- a/api/tacticalrmm/tacticalrmm/__init__.py +++ b/api/tacticalrmm/tacticalrmm/__init__.py @@ -3,6 +3,6 @@ from .celery import app as celery_app # drf auto-registers this as an authentication method when imported -from .schema import APIAuthenticationScheme +from .schema import APIAuthenticationScheme # noqa __all__ = ("celery_app",) diff --git a/api/tacticalrmm/tacticalrmm/asgi.py b/api/tacticalrmm/tacticalrmm/asgi.py index fca76baa62..7fd597e8e7 100644 --- a/api/tacticalrmm/tacticalrmm/asgi.py +++ b/api/tacticalrmm/tacticalrmm/asgi.py @@ -6,8 +6,8 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tacticalrmm.settings") # isort:skip django_asgi_app = get_asgi_application() # isort:skip -from tacticalrmm.utils import KnoxAuthMiddlewareStack # isort:skip -from .urls import ws_urlpatterns # isort:skip +from tacticalrmm.utils import KnoxAuthMiddlewareStack # isort:skip # noqa +from .urls import ws_urlpatterns # isort:skip # noqa application = ProtocolTypeRouter( diff --git a/api/tacticalrmm/tacticalrmm/settings.py b/api/tacticalrmm/tacticalrmm/settings.py index aea0819baf..653bb7ec3f 100644 --- a/api/tacticalrmm/tacticalrmm/settings.py +++ b/api/tacticalrmm/tacticalrmm/settings.py @@ -75,7 +75,7 @@ REDIS_HOST = "127.0.0.1" with suppress(ImportError): - from .local_settings import * + from .local_settings import * # noqa if "GHACTIONS" in os.environ: DEBUG = False @@ -141,9 +141,9 @@ } # silence cache key length warnings -import warnings +import warnings # noqa -from django.core.cache import CacheKeyWarning +from django.core.cache import CacheKeyWarning # noqa warnings.simplefilter("ignore", CacheKeyWarning) @@ -161,7 +161,7 @@ MIDDLEWARE = [ "django.contrib.sessions.middleware.SessionMiddleware", - "corsheaders.middleware.CorsMiddleware", ## + "corsheaders.middleware.CorsMiddleware", "tacticalrmm.middleware.LogIPMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", diff --git a/api/tacticalrmm/tacticalrmm/test.py b/api/tacticalrmm/tacticalrmm/test.py index 4540b98235..6c54621ee6 100644 --- a/api/tacticalrmm/tacticalrmm/test.py +++ b/api/tacticalrmm/tacticalrmm/test.py @@ -13,8 +13,6 @@ from tacticalrmm.constants import CustomFieldModel, CustomFieldType if TYPE_CHECKING: - from agents.models import Agent - from automation.models import Policy from checks.models import Check from clients.models import Client, Site from core.models import CustomField diff --git a/api/tacticalrmm/tacticalrmm/utils.py b/api/tacticalrmm/tacticalrmm/utils.py index 6628370736..cd4a76559a 100644 --- a/api/tacticalrmm/tacticalrmm/utils.py +++ b/api/tacticalrmm/tacticalrmm/utils.py @@ -247,7 +247,7 @@ async def __call__(self, scope, receive, send): return await self.app(scope, receive, send) -KnoxAuthMiddlewareStack = lambda inner: KnoxAuthMiddlewareInstance( +KnoxAuthMiddlewareStack = lambda inner: KnoxAuthMiddlewareInstance( # noqa AuthMiddlewareStack(inner) ) @@ -352,7 +352,7 @@ def replace_db_values( value = model_fields.get(**{model: obj}).value # need explicit None check since a false boolean value will pass default value - if value == None and field.default_value != None: + if value is None and field.default_value is not None: value = field.default_value # check if value exists and if not use default @@ -362,7 +362,7 @@ def replace_db_values( if quotes else format_shell_array(value) ) - elif value != None and field.type == CustomFieldType.CHECKBOX: + elif value is not None and field.type == CustomFieldType.CHECKBOX: value = format_shell_bool(value, shell) else: value = f"'{value}'" if quotes else value @@ -376,7 +376,7 @@ def replace_db_values( return "" # log any unhashable type errors - if value != None: + if value is not None: return value else: DebugLog.error( diff --git a/api/tacticalrmm/winupdate/views.py b/api/tacticalrmm/winupdate/views.py index 922be28f17..2305e34671 100644 --- a/api/tacticalrmm/winupdate/views.py +++ b/api/tacticalrmm/winupdate/views.py @@ -30,6 +30,7 @@ def get(self, request, agent_id): class ScanWindowsUpdates(APIView): permission_classes = [IsAuthenticated, AgentWinUpdatePerms] + # scan for windows updates on agent def post(self, request, agent_id): agent = get_object_or_404(Agent, agent_id=agent_id) From 0343ee4f6b1dc7ae7d1b81a5f72fcf9ec9807e45 Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Sun, 13 Nov 2022 08:05:42 +0000 Subject: [PATCH 03/22] fix mypy --- api/tacticalrmm/requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/tacticalrmm/requirements-dev.txt b/api/tacticalrmm/requirements-dev.txt index 6dd1b04267..4f8814e734 100644 --- a/api/tacticalrmm/requirements-dev.txt +++ b/api/tacticalrmm/requirements-dev.txt @@ -4,7 +4,7 @@ django-extensions isort types-pytz django-silk -mypy +mypy==0.982 django-stubs djangorestframework-stubs django-types From 1db6733e664bbd355cc1c742a36bb3716391a953 Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Tue, 22 Nov 2022 19:06:18 +0000 Subject: [PATCH 04/22] add checkin config --- api/tacticalrmm/apiv3/tests/tests.py | 6 ++++++ api/tacticalrmm/apiv3/urls.py | 1 + api/tacticalrmm/apiv3/utils.py | 25 +++++++++++++++++++++++++ api/tacticalrmm/apiv3/views.py | 11 +++++++++++ api/tacticalrmm/tacticalrmm/structs.py | 20 ++++++++++++++++++++ 5 files changed, 63 insertions(+) create mode 100644 api/tacticalrmm/apiv3/utils.py create mode 100644 api/tacticalrmm/tacticalrmm/structs.py diff --git a/api/tacticalrmm/apiv3/tests/tests.py b/api/tacticalrmm/apiv3/tests/tests.py index b6260b0bb4..03b3c0f4f2 100644 --- a/api/tacticalrmm/apiv3/tests/tests.py +++ b/api/tacticalrmm/apiv3/tests/tests.py @@ -294,3 +294,9 @@ def test_task_runner_results(self): AgentCustomField.objects.get(field=multiple, agent=task.agent).value, ["this"], ) + + def test_get_agent_config(self): + agent = baker.make_recipe("agents.online_agent") + url = f"/api/v3/{agent.agent_id}/config/" + r = self.client.get(url) + self.assertEqual(r.status_code, 200) diff --git a/api/tacticalrmm/apiv3/urls.py b/api/tacticalrmm/apiv3/urls.py index 2ce1036132..6506b85036 100644 --- a/api/tacticalrmm/apiv3/urls.py +++ b/api/tacticalrmm/apiv3/urls.py @@ -19,4 +19,5 @@ path("superseded/", views.SupersededWinUpdate.as_view()), path("/chocoresult/", views.ChocoResult.as_view()), path("//histresult/", views.AgentHistoryResult.as_view()), + path("/config/", views.AgentConfig.as_view()), ] diff --git a/api/tacticalrmm/apiv3/utils.py b/api/tacticalrmm/apiv3/utils.py new file mode 100644 index 0000000000..4464f633d8 --- /dev/null +++ b/api/tacticalrmm/apiv3/utils.py @@ -0,0 +1,25 @@ +import random + +from django.conf import settings + +from tacticalrmm.structs import AgentCheckInConfig + + +def get_agent_config() -> AgentCheckInConfig: + return AgentCheckInConfig( + checkin_hello=random.randint(*getattr(settings, "CHECKIN_HELLO", (30, 60))), + checkin_agentinfo=random.randint( + *getattr(settings, "CHECKIN_AGENTINFO", (200, 400)) + ), + checkin_winsvc=random.randint( + *getattr(settings, "CHECKIN_WINSVC", (2400, 3000)) + ), + checkin_pubip=random.randint(*getattr(settings, "CHECKIN_PUBIP", (300, 500))), + checkin_disks=random.randint(*getattr(settings, "CHECKIN_DISKS", (1000, 2000))), + checkin_sw=random.randint(*getattr(settings, "CHECKIN_SW", (2800, 3500))), + checkin_wmi=random.randint(*getattr(settings, "CHECKIN_WMI", (3000, 4000))), + checkin_syncmesh=random.randint( + *getattr(settings, "CHECKIN_SYNCMESH", (800, 1200)) + ), + limit_data=getattr(settings, "LIMIT_DATA", False), + ) diff --git a/api/tacticalrmm/apiv3/views.py b/api/tacticalrmm/apiv3/views.py index 34f250eb17..4d41c62041 100644 --- a/api/tacticalrmm/apiv3/views.py +++ b/api/tacticalrmm/apiv3/views.py @@ -14,6 +14,7 @@ from accounts.models import User from agents.models import Agent, AgentHistory from agents.serializers import AgentHistorySerializer +from apiv3.utils import get_agent_config from autotasks.models import AutomatedTask, TaskResult from autotasks.serializers import TaskGOGetSerializer, TaskResultSerializer from checks.constants import CHECK_DEFER, CHECK_RESULT_DEFER @@ -569,3 +570,13 @@ def patch(self, request, agentid, pk): s.is_valid(raise_exception=True) s.save() return Response("ok") + + +class AgentConfig(APIView): + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] + + def get(self, request, agentid): + get_object_or_404(Agent.objects.only("pk", "agent_id"), agent_id=agentid) + ret = get_agent_config() + return Response(ret._to_dict()) diff --git a/api/tacticalrmm/tacticalrmm/structs.py b/api/tacticalrmm/tacticalrmm/structs.py new file mode 100644 index 0000000000..481f180317 --- /dev/null +++ b/api/tacticalrmm/tacticalrmm/structs.py @@ -0,0 +1,20 @@ +import dataclasses +from typing import Any + + +class TRMMStruct: + def _to_dict(self) -> dict[str, Any]: + return dataclasses.asdict(self) + + +@dataclasses.dataclass +class AgentCheckInConfig(TRMMStruct): + checkin_hello: int + checkin_agentinfo: int + checkin_winsvc: int + checkin_pubip: int + checkin_disks: int + checkin_sw: int + checkin_wmi: int + checkin_syncmesh: int + limit_data: bool From 0f497257892d10bdaf5903b5fb414944e2b8aae3 Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Tue, 22 Nov 2022 19:13:44 +0000 Subject: [PATCH 05/22] update reqs --- api/tacticalrmm/requirements.txt | 4 ++-- api/tacticalrmm/tacticalrmm/settings.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/tacticalrmm/requirements.txt b/api/tacticalrmm/requirements.txt index 7a9b580253..e453f6029d 100644 --- a/api/tacticalrmm/requirements.txt +++ b/api/tacticalrmm/requirements.txt @@ -24,12 +24,12 @@ pyotp==2.7.0 pyparsing==3.0.9 pytz==2022.5 qrcode==7.3.1 -redis==4.3.4 +redis==4.3.5 hiredis==2.0.0 requests==2.28.1 six==1.16.0 sqlparse==0.4.3 -twilio==7.15.2 +twilio==7.15.3 urllib3==1.26.12 uWSGI==2.0.21 validators==0.20.0 diff --git a/api/tacticalrmm/tacticalrmm/settings.py b/api/tacticalrmm/tacticalrmm/settings.py index 653bb7ec3f..ce852d2db8 100644 --- a/api/tacticalrmm/tacticalrmm/settings.py +++ b/api/tacticalrmm/tacticalrmm/settings.py @@ -34,7 +34,7 @@ MESH_VER = "1.0.97" -NATS_SERVER_VER = "2.9.6" +NATS_SERVER_VER = "2.9.7" # for the update script, bump when need to recreate venv PIP_VER = "34" From 15a41d532e7a2c655f94b2f588eabac18f3090d2 Mon Sep 17 00:00:00 2001 From: Spam Me Date: Wed, 30 Nov 2022 15:01:17 +0100 Subject: [PATCH 06/22] Fix wrong service name --- api/tacticalrmm/core/installer.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/tacticalrmm/core/installer.ps1 b/api/tacticalrmm/core/installer.ps1 index 6071386f69..4d01675b10 100644 --- a/api/tacticalrmm/core/installer.ps1 +++ b/api/tacticalrmm/core/installer.ps1 @@ -13,7 +13,7 @@ $apilink = $downloadlink.split('/') [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -$serviceName = 'tacticalagent' +$serviceName = 'tacticalrmm' If (Get-Service $serviceName -ErrorAction SilentlyContinue) { write-host ('Tactical RMM Is Already Installed') } Else { From 02b98a242986515225cce9b1837731278799cfa5 Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Fri, 25 Nov 2022 07:56:26 +0000 Subject: [PATCH 07/22] don't call now twice --- api/tacticalrmm/agents/models.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/api/tacticalrmm/agents/models.py b/api/tacticalrmm/agents/models.py index 0e681e45cb..d4cc1d44ef 100644 --- a/api/tacticalrmm/agents/models.py +++ b/api/tacticalrmm/agents/models.py @@ -201,8 +201,9 @@ def do_update(self, *, token: str = "", force: bool = False) -> str: @property def status(self) -> str: - offline = djangotime.now() - djangotime.timedelta(minutes=self.offline_time) - overdue = djangotime.now() - djangotime.timedelta(minutes=self.overdue_time) + now = djangotime.now() + offline = now - djangotime.timedelta(minutes=self.offline_time) + overdue = now - djangotime.timedelta(minutes=self.overdue_time) if self.last_seen is not None: if (self.last_seen < offline) and (self.last_seen > overdue): From c18bc5fe67dfc562cd97496890f6f28be60d96ee Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Fri, 25 Nov 2022 08:05:01 +0000 Subject: [PATCH 08/22] switch to stringio --- api/tacticalrmm/agents/utils.py | 11 ++++------- api/tacticalrmm/agents/views.py | 25 ++++--------------------- 2 files changed, 8 insertions(+), 28 deletions(-) diff --git a/api/tacticalrmm/agents/utils.py b/api/tacticalrmm/agents/utils.py index c9fdc2943b..8930108a8b 100644 --- a/api/tacticalrmm/agents/utils.py +++ b/api/tacticalrmm/agents/utils.py @@ -1,6 +1,6 @@ import asyncio -import tempfile import urllib.parse +from io import StringIO from pathlib import Path from django.conf import settings @@ -70,11 +70,8 @@ def generate_linux_install( for i, j in replace.items(): text = text.replace(i, j) - with tempfile.NamedTemporaryFile() as fp: - with open(fp.name, "w") as f: - f.write(text) - f.write("\n") - + text += "\n" + with StringIO(text) as fp: return FileResponse( - open(fp.name, "rb"), as_attachment=True, filename="linux_agent_install.sh" + fp.read(), as_attachment=True, filename="linux_agent_install.sh" ) diff --git a/api/tacticalrmm/agents/views.py b/api/tacticalrmm/agents/views.py index f54753be23..96cd25dbd8 100644 --- a/api/tacticalrmm/agents/views.py +++ b/api/tacticalrmm/agents/views.py @@ -1,9 +1,9 @@ import asyncio import datetime as dt -import os import random import string import time +from io import StringIO from pathlib import Path from django.conf import settings @@ -665,26 +665,9 @@ def install_agent(request): for i, j in replace_dict.items(): text = text.replace(i, j) - file_name = "rmm-installer.ps1" - ps1 = os.path.join(settings.EXE_DIR, file_name) - - if os.path.exists(ps1): - try: - os.remove(ps1) - except Exception as e: - DebugLog.error(message=str(e)) - - Path(ps1).write_text(text) - - if settings.DEBUG: - with open(ps1, "r") as f: - response = HttpResponse(f.read(), content_type="text/plain") - 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}" + with StringIO(text) as fp: + response = HttpResponse(fp.read(), content_type="text/plain") + response["Content-Disposition"] = "attachment; filename=rmm-installer.ps1" return response From 80a94f97c425baa6640e009cee15fe616d631197 Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Wed, 30 Nov 2022 22:41:44 +0000 Subject: [PATCH 09/22] add ws compression option --- api/tacticalrmm/tacticalrmm/utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api/tacticalrmm/tacticalrmm/utils.py b/api/tacticalrmm/tacticalrmm/utils.py index cd4a76559a..a9149ff806 100644 --- a/api/tacticalrmm/tacticalrmm/utils.py +++ b/api/tacticalrmm/tacticalrmm/utils.py @@ -214,6 +214,9 @@ def reload_nats() -> None: elif hasattr(settings, "NATS_HTTP_PORT"): config["http_port"] = settings.NATS_HTTP_PORT # type: ignore + if "NATS_WS_COMPRESSION" in os.environ or hasattr(settings, "NATS_WS_COMPRESSION"): + config["websocket"]["compression"] = True + conf = os.path.join(settings.BASE_DIR, "nats-rmm.conf") with open(conf, "w") as f: json.dump(config, f) From 0e60d062e9bd39fa830b461a1976afafa6e73ce6 Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Wed, 30 Nov 2022 23:25:37 +0000 Subject: [PATCH 10/22] feat: env vars --- api/tacticalrmm/agents/models.py | 2 ++ api/tacticalrmm/agents/tasks.py | 2 ++ api/tacticalrmm/agents/tests/test_agents.py | 15 ++++++++ api/tacticalrmm/agents/views.py | 7 ++++ ..._alerttemplate_action_env_vars_and_more.py | 36 +++++++++++++++++++ api/tacticalrmm/alerts/models.py | 14 ++++++++ api/tacticalrmm/alerts/tests.py | 7 +++- api/tacticalrmm/apiv3/tests/tests.py | 9 ++++- api/tacticalrmm/autotasks/serializers.py | 1 + .../migrations/0019_script_env_vars.py | 25 +++++++++++++ api/tacticalrmm/scripts/models.py | 6 ++++ api/tacticalrmm/scripts/serializers.py | 4 ++- api/tacticalrmm/scripts/tasks.py | 2 ++ api/tacticalrmm/scripts/tests.py | 1 + api/tacticalrmm/scripts/views.py | 1 + 15 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 api/tacticalrmm/alerts/migrations/0013_alerttemplate_action_env_vars_and_more.py create mode 100644 api/tacticalrmm/scripts/migrations/0019_script_env_vars.py diff --git a/api/tacticalrmm/agents/models.py b/api/tacticalrmm/agents/models.py index d4cc1d44ef..518292da89 100644 --- a/api/tacticalrmm/agents/models.py +++ b/api/tacticalrmm/agents/models.py @@ -540,6 +540,7 @@ def run_script( run_on_any: bool = False, history_pk: int = 0, run_as_user: bool = False, + env_vars: list[str] = [], ) -> Any: from scripts.models import Script @@ -561,6 +562,7 @@ def run_script( "shell": script.shell, }, "run_as_user": run_as_user, + "env_vars": env_vars, } if history_pk != 0: diff --git a/api/tacticalrmm/agents/tasks.py b/api/tacticalrmm/agents/tasks.py index 8a0e05ea78..388deb4145 100644 --- a/api/tacticalrmm/agents/tasks.py +++ b/api/tacticalrmm/agents/tasks.py @@ -154,6 +154,7 @@ def run_script_email_results_task( args: list[str] = [], history_pk: int = 0, run_as_user: bool = False, + env_vars: list[str] = [], ): agent = Agent.objects.get(pk=agentpk) script = Script.objects.get(pk=scriptpk) @@ -165,6 +166,7 @@ def run_script_email_results_task( wait=True, history_pk=history_pk, run_as_user=run_as_user, + env_vars=env_vars, ) if r == "timeout": DebugLog.error( diff --git a/api/tacticalrmm/agents/tests/test_agents.py b/api/tacticalrmm/agents/tests/test_agents.py index 01d9543b7b..2c062299b0 100644 --- a/api/tacticalrmm/agents/tests/test_agents.py +++ b/api/tacticalrmm/agents/tests/test_agents.py @@ -540,6 +540,7 @@ def test_run_script(self, run_script, email_task): "args": [], "timeout": 15, "run_as_user": False, + "env_vars": ["hello=world", "foo=bar"], } r = self.client.post(url, data, format="json") @@ -555,6 +556,7 @@ def test_run_script(self, run_script, email_task): wait=True, history_pk=hist.pk, run_as_user=False, + env_vars=["hello=world", "foo=bar"], ) run_script.reset_mock() @@ -567,6 +569,7 @@ def test_run_script(self, run_script, email_task): "emailMode": "default", "emails": ["admin@example.com", "bob@example.com"], "run_as_user": False, + "env_vars": ["hello=world", "foo=bar"], } r = self.client.post(url, data, format="json") self.assertEqual(r.status_code, 200) @@ -577,6 +580,7 @@ def test_run_script(self, run_script, email_task): emails=[], args=["abc", "123"], run_as_user=False, + env_vars=["hello=world", "foo=bar"], ) email_task.reset_mock() @@ -591,6 +595,7 @@ def test_run_script(self, run_script, email_task): emails=["admin@example.com", "bob@example.com"], args=["abc", "123"], run_as_user=False, + env_vars=["hello=world", "foo=bar"], ) # test fire and forget @@ -600,6 +605,7 @@ def test_run_script(self, run_script, email_task): "args": ["hello", "world"], "timeout": 22, "run_as_user": True, + "env_vars": ["hello=world", "foo=bar"], } r = self.client.post(url, data, format="json") @@ -614,6 +620,7 @@ def test_run_script(self, run_script, email_task): timeout=25, history_pk=hist.pk, run_as_user=True, + env_vars=["hello=world", "foo=bar"], ) run_script.reset_mock() @@ -629,6 +636,7 @@ def test_run_script(self, run_script, email_task): "custom_field": custom_field.pk, "save_all_output": True, "run_as_user": False, + "env_vars": ["hello=world", "foo=bar"], } r = self.client.post(url, data, format="json") @@ -644,6 +652,7 @@ def test_run_script(self, run_script, email_task): wait=True, history_pk=hist.pk, run_as_user=False, + env_vars=["hello=world", "foo=bar"], ) run_script.reset_mock() @@ -662,6 +671,7 @@ def test_run_script(self, run_script, email_task): "custom_field": custom_field.pk, "save_all_output": False, "run_as_user": False, + "env_vars": ["hello=world", "foo=bar"], } r = self.client.post(url, data, format="json") @@ -677,6 +687,7 @@ def test_run_script(self, run_script, email_task): wait=True, history_pk=hist.pk, run_as_user=False, + env_vars=["hello=world", "foo=bar"], ) run_script.reset_mock() @@ -697,6 +708,7 @@ def test_run_script(self, run_script, email_task): "custom_field": custom_field.pk, "save_all_output": False, "run_as_user": False, + "env_vars": ["hello=world", "foo=bar"], } r = self.client.post(url, data, format="json") @@ -712,6 +724,7 @@ def test_run_script(self, run_script, email_task): wait=True, history_pk=hist.pk, run_as_user=False, + env_vars=["hello=world", "foo=bar"], ) run_script.reset_mock() @@ -729,6 +742,7 @@ def test_run_script(self, run_script, email_task): "args": ["hello", "world"], "timeout": 22, "run_as_user": False, + "env_vars": ["hello=world", "foo=bar"], } r = self.client.post(url, data, format="json") @@ -744,6 +758,7 @@ def test_run_script(self, run_script, email_task): wait=True, history_pk=hist.pk, run_as_user=False, + env_vars=["hello=world", "foo=bar"], ) run_script.reset_mock() diff --git a/api/tacticalrmm/agents/views.py b/api/tacticalrmm/agents/views.py index 96cd25dbd8..5443326713 100644 --- a/api/tacticalrmm/agents/views.py +++ b/api/tacticalrmm/agents/views.py @@ -700,6 +700,7 @@ def run_script(request, agent_id): output = request.data["output"] args = request.data["args"] run_as_user: bool = request.data["run_as_user"] + env_vars: list[str] = request.data["env_vars"] req_timeout = int(request.data["timeout"]) + 3 AuditLog.audit_script_run( @@ -725,6 +726,7 @@ def run_script(request, agent_id): wait=True, history_pk=history_pk, run_as_user=run_as_user, + env_vars=env_vars, ) return Response(r) @@ -739,6 +741,7 @@ def run_script(request, agent_id): emails=emails, args=args, run_as_user=run_as_user, + env_vars=env_vars, ) elif output == "collector": from core.models import CustomField @@ -750,6 +753,7 @@ def run_script(request, agent_id): wait=True, history_pk=history_pk, run_as_user=run_as_user, + env_vars=env_vars, ) custom_field = CustomField.objects.get(pk=request.data["custom_field"]) @@ -779,6 +783,7 @@ def run_script(request, agent_id): wait=True, history_pk=history_pk, run_as_user=run_as_user, + env_vars=env_vars, ) Note.objects.create(agent=agent, user=request.user, note=r) @@ -790,6 +795,7 @@ def run_script(request, agent_id): timeout=req_timeout, history_pk=history_pk, run_as_user=run_as_user, + env_vars=env_vars, ) return Response(f"{script.name} will now be run on {agent.hostname}") @@ -939,6 +945,7 @@ def bulk(request): request.data["timeout"], request.user.username[:50], request.data["run_as_user"], + request.data["env_vars"], ) return Response(f"{script.name} will now be run on {len(agents)} agents") diff --git a/api/tacticalrmm/alerts/migrations/0013_alerttemplate_action_env_vars_and_more.py b/api/tacticalrmm/alerts/migrations/0013_alerttemplate_action_env_vars_and_more.py new file mode 100644 index 0000000000..f066481052 --- /dev/null +++ b/api/tacticalrmm/alerts/migrations/0013_alerttemplate_action_env_vars_and_more.py @@ -0,0 +1,36 @@ +# Generated by Django 4.1.3 on 2022-11-26 20:22 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("alerts", "0012_alter_alert_action_retcode_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="alerttemplate", + name="action_env_vars", + 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="alerttemplate", + name="resolved_action_env_vars", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.TextField(blank=True, null=True), + blank=True, + default=list, + null=True, + size=None, + ), + ), + ] diff --git a/api/tacticalrmm/alerts/models.py b/api/tacticalrmm/alerts/models.py index 0a94e47bdb..393929429b 100644 --- a/api/tacticalrmm/alerts/models.py +++ b/api/tacticalrmm/alerts/models.py @@ -470,6 +470,7 @@ def handle_alert_failure( full=True, run_on_any=True, run_as_user=False, + env_vars=alert_template.action_env_vars, ) # command was successful @@ -593,6 +594,7 @@ def handle_alert_resolve( full=True, run_on_any=True, run_as_user=False, + env_vars=alert_template.resolved_action_env_vars, ) # command was successful @@ -661,6 +663,12 @@ class AlertTemplate(BaseAuditModel): blank=True, default=list, ) + action_env_vars = ArrayField( + models.TextField(null=True, blank=True), + null=True, + blank=True, + default=list, + ) action_timeout = models.PositiveIntegerField(default=15) resolved_action = models.ForeignKey( "scripts.Script", @@ -675,6 +683,12 @@ class AlertTemplate(BaseAuditModel): blank=True, default=list, ) + resolved_action_env_vars = ArrayField( + models.TextField(null=True, blank=True), + null=True, + blank=True, + default=list, + ) resolved_action_timeout = models.PositiveIntegerField(default=15) # overrides the global recipients diff --git a/api/tacticalrmm/alerts/tests.py b/api/tacticalrmm/alerts/tests.py index 058b0ed74f..c8220c04d1 100644 --- a/api/tacticalrmm/alerts/tests.py +++ b/api/tacticalrmm/alerts/tests.py @@ -1397,9 +1397,12 @@ def test_alert_actions( agent_script_actions=False, action=failure_action, action_timeout=30, + action_args=["hello", "world"], + action_env_vars=["hello=world", "foo=bar"], resolved_action=resolved_action, resolved_action_timeout=35, resolved_action_args=["nice_arg"], + resolved_action_env_vars=["resolved=action", "env=vars"], ) agent.client.alert_template = alert_template agent.client.save() @@ -1420,9 +1423,10 @@ def test_alert_actions( data = { "func": "runscriptfull", "timeout": 30, - "script_args": [], + "script_args": ["hello", "world"], "payload": {"code": failure_action.code, "shell": failure_action.shell}, "run_as_user": False, + "env_vars": ["hello=world", "foo=bar"], } nats_cmd.assert_called_with(data, timeout=30, wait=True) @@ -1452,6 +1456,7 @@ def test_alert_actions( "script_args": ["nice_arg"], "payload": {"code": resolved_action.code, "shell": resolved_action.shell}, "run_as_user": False, + "env_vars": ["resolved=action", "env=vars"], } nats_cmd.assert_called_with(data, timeout=35, wait=True) diff --git a/api/tacticalrmm/apiv3/tests/tests.py b/api/tacticalrmm/apiv3/tests/tests.py index 03b3c0f4f2..110979bf9f 100644 --- a/api/tacticalrmm/apiv3/tests/tests.py +++ b/api/tacticalrmm/apiv3/tests/tests.py @@ -127,8 +127,15 @@ def test_task_runner_get(self): "script": script.id, "script_args": ["test"], "timeout": 30, + "env_vars": ["hello=world", "foo=bar"], + }, + { + "type": "script", + "script": 3, + "script_args": [], + "timeout": 30, + "env_vars": ["hello=world", "foo=bar"], }, - {"type": "script", "script": 3, "script_args": [], "timeout": 30}, ] agent = baker.make_recipe("agents.agent") diff --git a/api/tacticalrmm/autotasks/serializers.py b/api/tacticalrmm/autotasks/serializers.py index 18a4e2cc4b..edead8ca10 100644 --- a/api/tacticalrmm/autotasks/serializers.py +++ b/api/tacticalrmm/autotasks/serializers.py @@ -241,6 +241,7 @@ def get_task_actions(self, obj): "shell": script.shell, "timeout": action["timeout"], "run_as_user": script.run_as_user, + "env_vars": action["env_vars"], } ) if actions_to_remove: diff --git a/api/tacticalrmm/scripts/migrations/0019_script_env_vars.py b/api/tacticalrmm/scripts/migrations/0019_script_env_vars.py new file mode 100644 index 0000000000..1ea9edc956 --- /dev/null +++ b/api/tacticalrmm/scripts/migrations/0019_script_env_vars.py @@ -0,0 +1,25 @@ +# Generated by Django 4.1.3 on 2022-11-26 01:38 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("scripts", "0018_script_run_as_user"), + ] + + operations = [ + migrations.AddField( + model_name="script", + name="env_vars", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.TextField(blank=True, null=True), + blank=True, + default=list, + null=True, + size=None, + ), + ), + ] diff --git a/api/tacticalrmm/scripts/models.py b/api/tacticalrmm/scripts/models.py index 5427ece876..64249eb9b5 100644 --- a/api/tacticalrmm/scripts/models.py +++ b/api/tacticalrmm/scripts/models.py @@ -29,6 +29,12 @@ class Script(BaseAuditModel): blank=True, default=list, ) + env_vars = ArrayField( + models.TextField(null=True, blank=True), + null=True, + blank=True, + default=list, + ) syntax = TextField(null=True, blank=True) favorite = models.BooleanField(default=False) category = models.CharField(max_length=100, null=True, blank=True) diff --git a/api/tacticalrmm/scripts/serializers.py b/api/tacticalrmm/scripts/serializers.py index 0cdb27ae5f..6c7d80e026 100644 --- a/api/tacticalrmm/scripts/serializers.py +++ b/api/tacticalrmm/scripts/serializers.py @@ -21,6 +21,7 @@ class Meta: "hidden", "supported_platforms", "run_as_user", + "env_vars", ] @@ -45,6 +46,7 @@ class Meta: "hidden", "supported_platforms", "run_as_user", + "env_vars", ] @@ -54,7 +56,7 @@ class ScriptCheckSerializer(ModelSerializer): class Meta: model = Script - fields = ["code", "shell", "run_as_user", "script_hash"] + fields = ["code", "shell", "run_as_user", "env_vars", "script_hash"] class ScriptSnippetSerializer(ModelSerializer): diff --git a/api/tacticalrmm/scripts/tasks.py b/api/tacticalrmm/scripts/tasks.py index 64226301fe..c3707b5b16 100644 --- a/api/tacticalrmm/scripts/tasks.py +++ b/api/tacticalrmm/scripts/tasks.py @@ -46,6 +46,7 @@ def handle_bulk_script_task( timeout: int, username: str, run_as_user: bool = False, + env_vars: list[str] = [], ) -> None: script = Script.objects.get(pk=scriptpk) agent: "Agent" @@ -62,4 +63,5 @@ def handle_bulk_script_task( timeout=timeout, history_pk=hist.pk, run_as_user=run_as_user, + env_vars=env_vars, ) diff --git a/api/tacticalrmm/scripts/tests.py b/api/tacticalrmm/scripts/tests.py index 514e7b399e..027a5911a8 100644 --- a/api/tacticalrmm/scripts/tests.py +++ b/api/tacticalrmm/scripts/tests.py @@ -146,6 +146,7 @@ def test_test_script(self, run_script): "args": [], "shell": ScriptShell.POWERSHELL, "run_as_user": False, + "env_vars": ["hello=world", "foo=bar"], } resp = self.client.post(url, data, format="json") diff --git a/api/tacticalrmm/scripts/views.py b/api/tacticalrmm/scripts/views.py index 2361a52544..dd6ae92d1c 100644 --- a/api/tacticalrmm/scripts/views.py +++ b/api/tacticalrmm/scripts/views.py @@ -161,6 +161,7 @@ def post(self, request, agent_id): "shell": request.data["shell"], }, "run_as_user": request.data["run_as_user"], + "env_vars": request.data["env_vars"], } r = asyncio.run( From 6ef02004ff54aae6832ef6e5a4f39c9d6906eb76 Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Wed, 30 Nov 2022 23:56:18 +0000 Subject: [PATCH 11/22] update reqs --- api/tacticalrmm/requirements.txt | 14 +++++++------- api/tacticalrmm/tacticalrmm/settings.py | 10 +++++----- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/api/tacticalrmm/requirements.txt b/api/tacticalrmm/requirements.txt index e453f6029d..849411e3c6 100644 --- a/api/tacticalrmm/requirements.txt +++ b/api/tacticalrmm/requirements.txt @@ -5,7 +5,7 @@ cffi==1.15.1 channels==4.0.0 channels_redis==4.0.0 chardet==4.0.0 -cryptography==38.0.3 +cryptography==38.0.4 daphne==4.0.0 Django==4.1.3 django-cors-headers==3.13.0 @@ -14,26 +14,26 @@ django-rest-knox==4.2.0 djangorestframework==3.14.0 drf-spectacular==0.24.2 future==0.18.2 +hiredis==2.0.0 +meshctrl==0.1.15 msgpack==1.0.4 nats-py==2.2.0 psutil==5.9.4 psycopg2-binary==2.9.5 pycparser==2.21 -pycryptodome==3.15.0 +pycryptodome==3.16.0 pyotp==2.7.0 pyparsing==3.0.9 pytz==2022.5 qrcode==7.3.1 redis==4.3.5 -hiredis==2.0.0 requests==2.28.1 six==1.16.0 sqlparse==0.4.3 -twilio==7.15.3 -urllib3==1.26.12 +twilio==7.15.4 +urllib3==1.26.13 uWSGI==2.0.21 validators==0.20.0 vine==5.0.0 websockets==10.4 -zipp==3.10.0 -meshctrl==0.1.15 +zipp==3.11.0 diff --git a/api/tacticalrmm/tacticalrmm/settings.py b/api/tacticalrmm/tacticalrmm/settings.py index ce852d2db8..bf9a8ab80c 100644 --- a/api/tacticalrmm/tacticalrmm/settings.py +++ b/api/tacticalrmm/tacticalrmm/settings.py @@ -23,18 +23,18 @@ TRMM_VERSION = "0.15.4-dev" # https://github.com/amidaware/tacticalrmm-web -WEB_VERSION = "0.101.7" +WEB_VERSION = "0.101.8-dev" # bump this version everytime vue code is changed # to alert user they need to manually refresh their browser -APP_VER = "0.0.174" +APP_VER = "0.0.175" # https://github.com/amidaware/rmmagent -LATEST_AGENT_VER = "2.4.2" +LATEST_AGENT_VER = "2.4.3-dev" -MESH_VER = "1.0.97" +MESH_VER = "1.1.0" -NATS_SERVER_VER = "2.9.7" +NATS_SERVER_VER = "2.9.8" # for the update script, bump when need to recreate venv PIP_VER = "34" From 7e2295c38270a784f561ee1dbd3e15f80d3f3a64 Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Thu, 1 Dec 2022 00:22:09 +0000 Subject: [PATCH 12/22] rework celery config --- api/tacticalrmm/tacticalrmm/celery.py | 42 +++++++++++++-------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/api/tacticalrmm/tacticalrmm/celery.py b/api/tacticalrmm/tacticalrmm/celery.py index fdedf0f263..b2590f1f07 100644 --- a/api/tacticalrmm/tacticalrmm/celery.py +++ b/api/tacticalrmm/tacticalrmm/celery.py @@ -9,10 +9,6 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tacticalrmm.settings") app = Celery("tacticalrmm", backend="redis://" + settings.REDIS_HOST, broker="redis://" + settings.REDIS_HOST) # type: ignore - -# app.config_from_object('django.conf:settings', namespace='CELERY') -# app.broker_url = "redis://" + settings.REDIS_HOST + ":6379" -# app.result_backend = "redis://" + settings.REDIS_HOST + ":6379" app.accept_content = ["application/json"] app.result_serializer = "json" app.task_serializer = "json" @@ -38,27 +34,29 @@ "task": "autotasks.tasks.remove_orphaned_win_tasks", "schedule": crontab(minute=50, hour="12"), }, + "agent-outages-task": { + "task": "agents.tasks.agent_outages_task", + "schedule": crontab(minute="*/2"), + }, + "unsnooze-alerts": { + "task": "alerts.tasks.unsnooze_alerts", + "schedule": crontab(minute=10, hour="*"), + }, + "core-maintenance-tasks": { + "task": "core.tasks.core_maintenance_tasks", + "schedule": crontab(minute=15, hour="*"), + }, + "cache-db-fields-task": { + "task": "core.tasks.cache_db_fields_task", + "schedule": crontab(minute="*/3", hour="*"), + }, + "handle-resolved-stuff": { + "task": "core.tasks.handle_resolved_stuff", + "schedule": crontab(minute="*/2", hour="*"), + }, } @app.task(bind=True) def debug_task(self): print("Request: {0!r}".format(self.request)) - - -@app.on_after_finalize.connect -def setup_periodic_tasks(sender, **kwargs): - - from agents.tasks import agent_outages_task - from alerts.tasks import unsnooze_alerts - from core.tasks import ( - cache_db_fields_task, - core_maintenance_tasks, - handle_resolved_stuff, - ) - - sender.add_periodic_task(60.0, agent_outages_task.s()) - sender.add_periodic_task(60.0 * 30, core_maintenance_tasks.s()) - sender.add_periodic_task(60.0 * 60, unsnooze_alerts.s()) - sender.add_periodic_task(95.0, cache_db_fields_task.s()) - sender.add_periodic_task(70.0, handle_resolved_stuff.s()) From 2526fa3c4761ae6fefdf62d1b212af3a3c1246ed Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Thu, 1 Dec 2022 06:33:27 +0000 Subject: [PATCH 13/22] fix backup/restore when OS are different --- .../core/management/commands/get_config.py | 8 ++- api/tacticalrmm/tacticalrmm/constants.py | 2 + backup.sh | 17 +++--- restore.sh | 55 ++++++++++++++++--- 4 files changed, 62 insertions(+), 20 deletions(-) diff --git a/api/tacticalrmm/core/management/commands/get_config.py b/api/tacticalrmm/core/management/commands/get_config.py index fbe9bcc9a6..2979561b24 100644 --- a/api/tacticalrmm/core/management/commands/get_config.py +++ b/api/tacticalrmm/core/management/commands/get_config.py @@ -1,3 +1,5 @@ +from urllib.parse import urlparse + from django.conf import settings from django.core.management.base import BaseCommand @@ -22,6 +24,8 @@ def handle(self, *args, **kwargs): self.stdout.write(settings.NATS_SERVER_VER) case "frontend": self.stdout.write(settings.CORS_ORIGIN_WHITELIST[0]) + case "webdomain": + self.stdout.write(urlparse(settings.CORS_ORIGIN_WHITELIST[0]).netloc) case "djangoadmin": url = f"https://{settings.ALLOWED_HOSTS[0]}/{settings.ADMIN_URL}" self.stdout.write(url) @@ -39,7 +43,7 @@ def handle(self, *args, **kwargs): self.stdout.write(settings.DATABASES["default"]["HOST"]) case "dbport": self.stdout.write(settings.DATABASES["default"]["PORT"]) - case "meshsite" | "meshuser" | "meshtoken": + case "meshsite" | "meshuser" | "meshtoken" | "meshdomain": from core.models import CoreSettings core: "CoreSettings" = CoreSettings.objects.first() @@ -47,6 +51,8 @@ def handle(self, *args, **kwargs): obj = core.mesh_site elif kwargs["name"] == "meshuser": obj = core.mesh_username + elif kwargs["name"] == "meshdomain": + obj = urlparse(core.mesh_site).netloc else: obj = core.mesh_token diff --git a/api/tacticalrmm/tacticalrmm/constants.py b/api/tacticalrmm/tacticalrmm/constants.py index b4bca3a6cb..060b1e0d77 100644 --- a/api/tacticalrmm/tacticalrmm/constants.py +++ b/api/tacticalrmm/tacticalrmm/constants.py @@ -426,6 +426,7 @@ class DebugLogType(models.TextChoices): "meshver", "natsver", "frontend", + "webdomain", "djangoadmin", "setuptoolsver", "wheelver", @@ -437,4 +438,5 @@ class DebugLogType(models.TextChoices): "meshsite", "meshuser", "meshtoken", + "meshdomain", ) diff --git a/backup.sh b/backup.sh index 2d23f335d1..f621c3d505 100755 --- a/backup.sh +++ b/backup.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -SCRIPT_VERSION="20" +SCRIPT_VERSION="21" SCRIPT_URL='https://raw.githubusercontent.com/amidaware/tacticalrmm/master/backup.sh' GREEN='\033[0;32m' @@ -27,10 +27,6 @@ if [ $EUID -eq 0 ]; then exit 1 fi -POSTGRES_USER=$(grep -w USER /rmm/api/tacticalrmm/tacticalrmm/local_settings.py | sed 's/^.*: //' | sed 's/.//' | sed -r 's/.{2}$//') -POSTGRES_PW=$(grep -w PASSWORD /rmm/api/tacticalrmm/tacticalrmm/local_settings.py | sed 's/^.*: //' | sed 's/.//' | sed -r 's/.{2}$//') - - if [ ! -d /rmmbackups ]; then sudo mkdir /rmmbackups sudo chown ${USER}:${USER} /rmmbackups @@ -56,6 +52,8 @@ mkdir ${tmp_dir}/systemd mkdir ${tmp_dir}/rmm mkdir ${tmp_dir}/confd +POSTGRES_USER=$(/rmm/api/env/bin/python /rmm/api/tacticalrmm/manage.py get_config dbuser) +POSTGRES_PW=$(/rmm/api/env/bin/python /rmm/api/tacticalrmm/manage.py get_config dbpw) pg_dump --dbname=postgresql://"${POSTGRES_USER}":"${POSTGRES_PW}"@127.0.0.1:5432/tacticalrmm | gzip -9 > ${tmp_dir}/postgres/db-${dt_now}.psql.gz @@ -64,14 +62,13 @@ mongodump --gzip --out=${tmp_dir}/meshcentral/mongo sudo tar -czvf ${tmp_dir}/certs/etc-letsencrypt.tar.gz -C /etc/letsencrypt . -sudo tar -czvf ${tmp_dir}/nginx/etc-nginx.tar.gz -C /etc/nginx . +for i in rmm frontend meshcentral; do + sudo cp /etc/nginx/sites-available/${i}.conf ${tmp_dir}/nginx/ +done 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}/daphne.service ${tmp_dir}/systemd/ -if [ -f "${sysd}/nats-api.service" ]; then - sudo cp ${sysd}/nats-api.service ${tmp_dir}/systemd/ -fi +sudo cp ${sysd}/rmm.service ${sysd}/celery.service ${sysd}/celerybeat.service ${sysd}/meshcentral.service ${sysd}/nats.service ${sysd}/daphne.service ${sysd}/nats-api.service ${tmp_dir}/systemd/ cat /rmm/api/tacticalrmm/tacticalrmm/private/log/django_debug.log | gzip -9 > ${tmp_dir}/rmm/debug.log.gz cp /rmm/api/tacticalrmm/tacticalrmm/local_settings.py ${tmp_dir}/rmm/ diff --git a/restore.sh b/restore.sh index d23dc6e417..43a3f709dd 100755 --- a/restore.sh +++ b/restore.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -SCRIPT_VERSION="44" +SCRIPT_VERSION="45" SCRIPT_URL='https://raw.githubusercontent.com/amidaware/tacticalrmm/master/restore.sh' sudo apt update @@ -139,13 +139,51 @@ echo "${nginxrepo}" | sudo tee /etc/apt/sources.list.d/nginx.list > /dev/null sudo apt update sudo apt install -y nginx sudo systemctl stop nginx -sudo rm -rf /etc/nginx -sudo mkdir /etc/nginx -sudo tar -xzf $tmp_dir/nginx/etc-nginx.tar.gz -C /etc/nginx -rmmdomain=$(grep server_name /etc/nginx/sites-available/rmm.conf | grep -v 301 | head -1 | tr -d " \t" | sed 's/.*server_name//' | tr -d ';') -frontenddomain=$(grep server_name /etc/nginx/sites-available/frontend.conf | grep -v 301 | head -1 | tr -d " \t" | sed 's/.*server_name//' | tr -d ';') -meshdomain=$(grep server_name /etc/nginx/sites-available/meshcentral.conf | grep -v 301 | head -1 | tr -d " \t" | sed 's/.*server_name//' | tr -d ';') +nginxdefaultconf='/etc/nginx/nginx.conf' + +nginxconf="$(cat << EOF +worker_rlimit_nofile 1000000; +user www-data; +worker_processes auto; +pid /run/nginx.pid; +include /etc/nginx/modules-enabled/*.conf; + +events { + worker_connections 4096; +} + +http { + sendfile on; + tcp_nopush on; + types_hash_max_size 2048; + server_names_hash_bucket_size 64; + include /etc/nginx/mime.types; + default_type application/octet-stream; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers on; + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + gzip on; + include /etc/nginx/conf.d/*.conf; + include /etc/nginx/sites-enabled/*; +} +EOF +)" +echo "${nginxconf}" | sudo tee $nginxdefaultconf > /dev/null + +for i in sites-available sites-enabled; do + sudo mkdir -p /etc/nginx/$i +done + +for i in rmm frontend meshcentral; do + sudo cp ${tmp_dir}/nginx/${i}.conf /etc/nginx/sites-available/ + sudo ln -s /etc/nginx/sites-available/${i}.conf /etc/nginx/sites-enabled/${i}.conf +done + +rmmdomain=$(/rmm/api/env/bin/python /rmm/api/tacticalrmm/manage.py get_config api) +frontenddomain=$(/rmm/api/env/bin/python /rmm/api/tacticalrmm/manage.py get_config webdomain) +meshdomain=$(/rmm/api/env/bin/python /rmm/api/tacticalrmm/manage.py get_config meshdomain) print_green 'Restoring hosts file' @@ -218,8 +256,7 @@ wget -qO - https://www.mongodb.org/static/pgp/server-4.4.asc | sudo apt-key add echo "$mongodb_repo" | sudo tee /etc/apt/sources.list.d/mongodb-org-4.4.list sudo apt update sudo apt install -y mongodb-org -sudo systemctl enable mongod -sudo systemctl restart mongod +sudo systemctl enable --now mongod sleep 5 mongorestore --gzip $tmp_dir/meshcentral/mongo From dacedf4018e93e8ed7957b12c58d344dea70170e Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Sat, 3 Dec 2022 08:14:53 +0000 Subject: [PATCH 14/22] add ram check --- install.sh | 8 +++++++- restore.sh | 6 ++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/install.sh b/install.sh index e72c88a5a2..efa5aca1f3 100644 --- a/install.sh +++ b/install.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -SCRIPT_VERSION="70" +SCRIPT_VERSION="71" SCRIPT_URL='https://raw.githubusercontent.com/amidaware/tacticalrmm/master/install.sh' sudo apt install -y curl wget dirmngr gnupg lsb-release @@ -35,6 +35,12 @@ if [ "$arch" != "x86_64" ]; then exit 1 fi +memTotal=$(grep -i memtotal /proc/meminfo | awk '{print $2}') +if [[ $memTotal -lt 3627528 ]]; then + echo -ne "${RED}ERROR: A minimum of 4GB of RAM is required.${NC}\n" + exit 1 +fi + osname=$(lsb_release -si); osname=${osname^} osname=$(echo "$osname" | tr '[A-Z]' '[a-z]') fullrel=$(lsb_release -sd) diff --git a/restore.sh b/restore.sh index 43a3f709dd..12a22ae18d 100755 --- a/restore.sh +++ b/restore.sh @@ -35,6 +35,12 @@ if [ "$arch" != "x86_64" ]; then exit 1 fi +memTotal=$(grep -i memtotal /proc/meminfo | awk '{print $2}') +if [[ $memTotal -lt 3627528 ]]; then + echo -ne "${RED}ERROR: A minimum of 4GB of RAM is required.${NC}\n" + exit 1 +fi + osname=$(lsb_release -si); osname=${osname^} osname=$(echo "$osname" | tr '[A-Z]' '[a-z]') fullrel=$(lsb_release -sd) From 719ba56c597cc87b81fd7a5ad5c9c6dac59c749a Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Sat, 3 Dec 2022 09:55:37 +0000 Subject: [PATCH 15/22] remove db hit --- api/tacticalrmm/apiv3/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/api/tacticalrmm/apiv3/views.py b/api/tacticalrmm/apiv3/views.py index 4d41c62041..c370ac1f3e 100644 --- a/api/tacticalrmm/apiv3/views.py +++ b/api/tacticalrmm/apiv3/views.py @@ -577,6 +577,5 @@ class AgentConfig(APIView): permission_classes = [IsAuthenticated] def get(self, request, agentid): - get_object_or_404(Agent.objects.only("pk", "agent_id"), agent_id=agentid) ret = get_agent_config() return Response(ret._to_dict()) From ab6227828bb284702edbfb155f9b38374879b85b Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Sat, 3 Dec 2022 09:56:44 +0000 Subject: [PATCH 16/22] add env vars for script checks --- .../checks/migrations/0031_check_env_vars.py | 25 +++++++++++++++++++ api/tacticalrmm/checks/models.py | 6 +++++ api/tacticalrmm/checks/serializers.py | 8 ++++++ 3 files changed, 39 insertions(+) create mode 100644 api/tacticalrmm/checks/migrations/0031_check_env_vars.py diff --git a/api/tacticalrmm/checks/migrations/0031_check_env_vars.py b/api/tacticalrmm/checks/migrations/0031_check_env_vars.py new file mode 100644 index 0000000000..a939562452 --- /dev/null +++ b/api/tacticalrmm/checks/migrations/0031_check_env_vars.py @@ -0,0 +1,25 @@ +# Generated by Django 4.1.3 on 2022-12-03 09:38 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("checks", "0030_alter_checkresult_retcode"), + ] + + operations = [ + migrations.AddField( + model_name="check", + name="env_vars", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.TextField(blank=True, null=True), + blank=True, + default=list, + null=True, + size=None, + ), + ), + ] diff --git a/api/tacticalrmm/checks/models.py b/api/tacticalrmm/checks/models.py index 5d6536ca8f..00b3716928 100644 --- a/api/tacticalrmm/checks/models.py +++ b/api/tacticalrmm/checks/models.py @@ -98,6 +98,12 @@ class Check(BaseAuditModel): blank=True, default=list, ) + env_vars = ArrayField( + models.TextField(null=True, blank=True), + null=True, + blank=True, + default=list, + ) info_return_codes = ArrayField( models.PositiveIntegerField(), null=True, diff --git a/api/tacticalrmm/checks/serializers.py b/api/tacticalrmm/checks/serializers.py index fb93a45174..0e6127657b 100644 --- a/api/tacticalrmm/checks/serializers.py +++ b/api/tacticalrmm/checks/serializers.py @@ -158,6 +158,7 @@ class CheckRunnerGetSerializer(serializers.ModelSerializer): # only send data needed for agent to run a check script = ScriptCheckSerializer(read_only=True) script_args = serializers.SerializerMethodField() + env_vars = serializers.SerializerMethodField() def get_script_args(self, obj): if obj.check_type != CheckType.SCRIPT: @@ -168,6 +169,13 @@ def get_script_args(self, obj): agent=agent, shell=obj.script.shell, args=obj.script_args ) + def get_env_vars(self, obj): + if obj.check_type != CheckType.SCRIPT: + return [] + + # check's env_vars override the script's env vars + return obj.env_vars or obj.script.env_vars + class Meta: model = Check exclude = [ From 77d44f25f98186f95bbe98bc564347315dd50a2a Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Sun, 4 Dec 2022 02:11:49 +0000 Subject: [PATCH 17/22] create and log an agent history event for alert failure/resolved script runs --- api/tacticalrmm/alerts/models.py | 19 +++++++++++++++++-- api/tacticalrmm/alerts/tests.py | 3 +++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/api/tacticalrmm/alerts/models.py b/api/tacticalrmm/alerts/models.py index 393929429b..8077bda5df 100644 --- a/api/tacticalrmm/alerts/models.py +++ b/api/tacticalrmm/alerts/models.py @@ -10,6 +10,7 @@ from logs.models import BaseAuditModel, DebugLog from tacticalrmm.constants import ( + AgentHistoryType, AgentMonType, AlertSeverity, AlertType, @@ -268,7 +269,7 @@ def create_or_return_task_alert( def handle_alert_failure( cls, instance: Union[Agent, TaskResult, CheckResult] ) -> None: - from agents.models import Agent + from agents.models import Agent, AgentHistory from autotasks.models import TaskResult from checks.models import CheckResult @@ -462,11 +463,18 @@ def handle_alert_failure( and run_script_action and not alert.action_run ): + hist = AgentHistory.objects.create( + agent=agent, + type=AgentHistoryType.SCRIPT_RUN, + script=alert_template.action, + username="alert-action-failure", + ) r = agent.run_script( scriptpk=alert_template.action.pk, args=alert.parse_script_args(alert_template.action_args), timeout=alert_template.action_timeout, wait=True, + history_pk=hist.pk, full=True, run_on_any=True, run_as_user=False, @@ -492,7 +500,7 @@ def handle_alert_failure( def handle_alert_resolve( cls, instance: Union[Agent, TaskResult, CheckResult] ) -> None: - from agents.models import Agent + from agents.models import Agent, AgentHistory from autotasks.models import TaskResult from checks.models import CheckResult @@ -586,11 +594,18 @@ def handle_alert_resolve( and run_script_action and not alert.resolved_action_run ): + hist = AgentHistory.objects.create( + agent=agent, + type=AgentHistoryType.SCRIPT_RUN, + script=alert_template.action, + username="alert-action-resolved", + ) r = agent.run_script( scriptpk=alert_template.resolved_action.pk, args=alert.parse_script_args(alert_template.resolved_action_args), timeout=alert_template.resolved_action_timeout, wait=True, + history_pk=hist.pk, full=True, run_on_any=True, run_as_user=False, diff --git a/api/tacticalrmm/alerts/tests.py b/api/tacticalrmm/alerts/tests.py index c8220c04d1..ed75443ea0 100644 --- a/api/tacticalrmm/alerts/tests.py +++ b/api/tacticalrmm/alerts/tests.py @@ -1373,6 +1373,7 @@ def test_alert_actions( ): from agents.tasks import agent_outages_task + from agents.models import AgentHistory # Setup cmd mock success = { @@ -1427,6 +1428,7 @@ def test_alert_actions( "payload": {"code": failure_action.code, "shell": failure_action.shell}, "run_as_user": False, "env_vars": ["hello=world", "foo=bar"], + "id": AgentHistory.objects.last().pk, # type: ignore } nats_cmd.assert_called_with(data, timeout=30, wait=True) @@ -1457,6 +1459,7 @@ def test_alert_actions( "payload": {"code": resolved_action.code, "shell": resolved_action.shell}, "run_as_user": False, "env_vars": ["resolved=action", "env=vars"], + "id": AgentHistory.objects.last().pk, # type: ignore } nats_cmd.assert_called_with(data, timeout=35, wait=True) From 6800b9aaae154ffbe175d83547079c8c7b71c084 Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Sun, 4 Dec 2022 06:14:37 +0000 Subject: [PATCH 18/22] fix mgmt command --- restore.sh | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/restore.sh b/restore.sh index 12a22ae18d..e0fd3aa072 100755 --- a/restore.sh +++ b/restore.sh @@ -187,20 +187,6 @@ for i in rmm frontend meshcentral; do sudo ln -s /etc/nginx/sites-available/${i}.conf /etc/nginx/sites-enabled/${i}.conf done -rmmdomain=$(/rmm/api/env/bin/python /rmm/api/tacticalrmm/manage.py get_config api) -frontenddomain=$(/rmm/api/env/bin/python /rmm/api/tacticalrmm/manage.py get_config webdomain) -meshdomain=$(/rmm/api/env/bin/python /rmm/api/tacticalrmm/manage.py get_config meshdomain) - - -print_green 'Restoring hosts file' - -HAS_11=$(grep 127.0.1.1 /etc/hosts) -if [[ $HAS_11 ]]; then - sudo sed -i "/127.0.1.1/s/$/ ${rmmdomain} ${frontenddomain} ${meshdomain}/" /etc/hosts -else - echo "127.0.1.1 ${rmmdomain} ${frontenddomain} ${meshdomain}" | sudo tee --append /etc/hosts > /dev/null -fi - print_green 'Restoring certbot' sudo apt install -y software-properties-common @@ -350,8 +336,19 @@ python manage.py reload_nats python manage.py post_update_tasks API=$(python manage.py get_config api) WEB_VERSION=$(python manage.py get_config webversion) +webdomain=$(python manage.py get_config webdomain) +meshdomain=$(python manage.py get_config meshdomain) deactivate +print_green 'Restoring hosts file' + +HAS_11=$(grep 127.0.1.1 /etc/hosts) +if [[ $HAS_11 ]]; then + sudo sed -i "/127.0.1.1/s/$/ ${API} ${webdomain} ${meshdomain}/" /etc/hosts +else + echo "127.0.1.1 ${API} ${webdomain} ${meshdomain}" | sudo tee --append /etc/hosts > /dev/null +fi + sudo systemctl enable nats.service sudo systemctl start nats.service From 71e9fa3d1660dab86d0d1fad52be4d255e0ae589 Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Sun, 4 Dec 2022 07:11:01 +0000 Subject: [PATCH 19/22] update name and deps --- api/tacticalrmm/agents/models.py | 1 + go.mod | 4 ++-- go.sum | 8 ++++---- main.go | 2 +- natsapi/utils.go | 2 +- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/api/tacticalrmm/agents/models.py b/api/tacticalrmm/agents/models.py index 518292da89..c983c18d4e 100644 --- a/api/tacticalrmm/agents/models.py +++ b/api/tacticalrmm/agents/models.py @@ -803,6 +803,7 @@ async def nats_cmd( options = { "servers": f"tls://{settings.ALLOWED_HOSTS[0]}:{nats_std_port}", "user": "tacticalrmm", + "name": "trmm-django", "password": settings.SECRET_KEY, "connect_timeout": 3, "max_reconnect_attempts": 2, diff --git a/go.mod b/go.mod index 7e269c4d72..20e1fd1d59 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,8 @@ require ( github.com/golang/protobuf v1.5.2 // indirect github.com/jmoiron/sqlx v1.3.5 github.com/lib/pq v1.10.7 - github.com/nats-io/nats-server/v2 v2.9.6 // indirect - github.com/nats-io/nats.go v1.19.1 + github.com/nats-io/nats-server/v2 v2.9.8 // indirect + github.com/nats-io/nats.go v1.20.0 github.com/ugorji/go/codec v1.2.7 github.com/wh1te909/trmm-shared v0.0.0-20220227075846-f9f757361139 google.golang.org/protobuf v1.28.0 // indirect diff --git a/go.sum b/go.sum index 8cda96579c..3fd27a00b1 100644 --- a/go.sum +++ b/go.sum @@ -17,10 +17,10 @@ github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRU github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g= github.com/nats-io/jwt/v2 v2.3.0 h1:z2mA1a7tIf5ShggOFlR1oBPgd6hGqcDYsISxZByUzdI= -github.com/nats-io/nats-server/v2 v2.9.6 h1:RTtK+rv/4CcliOuqGsy58g7MuWkBaWmF5TUNwuUo9Uw= -github.com/nats-io/nats-server/v2 v2.9.6/go.mod h1:AB6hAnGZDlYfqb7CTAm66ZKMZy9DpfierY1/PbpvI2g= -github.com/nats-io/nats.go v1.19.1 h1:pDQZthDfxRMSJ0ereExAM9ODf3JyS42Exk7iCMdbpec= -github.com/nats-io/nats.go v1.19.1/go.mod h1:tLqubohF7t4z3du1QDPYJIQQyhb4wl6DhjxEajSI7UA= +github.com/nats-io/nats-server/v2 v2.9.8 h1:jgxZsv+A3Reb3MgwxaINcNq/za8xZInKhDg9Q0cGN1o= +github.com/nats-io/nats-server/v2 v2.9.8/go.mod h1:AB6hAnGZDlYfqb7CTAm66ZKMZy9DpfierY1/PbpvI2g= +github.com/nats-io/nats.go v1.20.0 h1:T8JJnQfVSdh1CzGiwAOv5hEobYCBho/0EupGznYw0oM= +github.com/nats-io/nats.go v1.20.0/go.mod h1:tLqubohF7t4z3du1QDPYJIQQyhb4wl6DhjxEajSI7UA= github.com/nats-io/nkeys v0.3.0 h1:cgM5tL53EvYRU+2YLXIK0G2mJtK12Ft9oeooSZMA2G8= github.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= diff --git a/main.go b/main.go index 82cab80af8..5039ed281e 100644 --- a/main.go +++ b/main.go @@ -12,7 +12,7 @@ import ( ) var ( - version = "3.3.2" + version = "3.3.3" log = logrus.New() ) diff --git a/natsapi/utils.go b/natsapi/utils.go index 64154de7e9..d704474f29 100644 --- a/natsapi/utils.go +++ b/natsapi/utils.go @@ -15,7 +15,7 @@ import ( func setupNatsOptions(key string) []nats.Option { opts := []nats.Option{ - nats.Name("TacticalRMM"), + nats.Name("trmm-nats-api"), nats.UserInfo("tacticalrmm", key), nats.ReconnectWait(time.Second * 2), nats.RetryOnFailedConnect(true), From c12bede9809bf2d4618fcad69c507bd694eef3f2 Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Sun, 4 Dec 2022 07:14:05 +0000 Subject: [PATCH 20/22] update bin --- natsapi/bin/nats-api | Bin 6684672 -> 6684672 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/natsapi/bin/nats-api b/natsapi/bin/nats-api index 1a5f1164c5c4fac4fc3d7cd3e9602c3ffe05b75f..29acc7f2f7594ae7b58a2277475d6462ec156523 100755 GIT binary patch delta 52084 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�EMU6WB(LX&iFm{e&Cwi*<)`6 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 delta 52108 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�Z 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 Nn=j@4=Gh(me*pHUB5D8t From 91b858bf3397acb58ff96827561f6886886a883f Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Sun, 4 Dec 2022 23:31:57 +0000 Subject: [PATCH 21/22] bump versions --- api/tacticalrmm/tacticalrmm/settings.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/tacticalrmm/tacticalrmm/settings.py b/api/tacticalrmm/tacticalrmm/settings.py index bf9a8ab80c..a2d651f5a3 100644 --- a/api/tacticalrmm/tacticalrmm/settings.py +++ b/api/tacticalrmm/tacticalrmm/settings.py @@ -20,17 +20,17 @@ AUTH_USER_MODEL = "accounts.User" # latest release -TRMM_VERSION = "0.15.4-dev" +TRMM_VERSION = "0.15.4" # https://github.com/amidaware/tacticalrmm-web -WEB_VERSION = "0.101.8-dev" +WEB_VERSION = "0.101.9" # bump this version everytime vue code is changed # to alert user they need to manually refresh their browser APP_VER = "0.0.175" # https://github.com/amidaware/rmmagent -LATEST_AGENT_VER = "2.4.3-dev" +LATEST_AGENT_VER = "2.4.3" MESH_VER = "1.1.0" From 448c59ea8848a409fad1c1c4944b28cfbde48e11 Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Sun, 4 Dec 2022 23:33:05 +0000 Subject: [PATCH 22/22] bump docker nats --- docker/containers/tactical-nats/dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/containers/tactical-nats/dockerfile b/docker/containers/tactical-nats/dockerfile index 7a758f32f0..1bc3b12772 100644 --- a/docker/containers/tactical-nats/dockerfile +++ b/docker/containers/tactical-nats/dockerfile @@ -1,4 +1,4 @@ -FROM nats:2.9.6-alpine +FROM nats:2.9.8-alpine ENV TACTICAL_DIR /opt/tactical ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready