From ab7c481f83b50835a9c498f15bbc421357398412 Mon Sep 17 00:00:00 2001 From: sadnub Date: Sat, 28 Nov 2020 09:47:27 -0500 Subject: [PATCH 01/45] Create docker-build-push.yml --- .github/workflows/docker-build-push.yml | 88 +++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 .github/workflows/docker-build-push.yml diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml new file mode 100644 index 0000000000..c4b0f34dcb --- /dev/null +++ b/.github/workflows/docker-build-push.yml @@ -0,0 +1,88 @@ +name: Publish Tactical Docker Images +on: + push: + tags: + - "v*.*.*" +jobs: + docker: + name: Build and Push Docker Images + runs-on: ubuntu-latest + steps: + - name: Check out the repo + uses: actions/checkout@v2 + + - name: Get Github Tag + id: prep + run: | + echo ::set-output name=version::${GITHUB_REF#refs/tags/v} + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Login to DockerHub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and Push Tactical Image + uses: docker/build-push-action@v2 + with: + context: . + push: true + pull: true + file: ./docker/containers/tactical/dockerfile + platforms: linux/amd64 + tags: tacticalrmm/tactical:${{ steps.prep.outputs.version }},sadnub/tactical:latest + + - name: Build and Push Tactical MeshCentral Image + uses: docker/build-push-action@v2 + with: + context: . + push: true + pull: true + file: ./docker/containers/tactical-meshcentral/dockerfile + platforms: linux/amd64 + tags: tacticalrmm/tactical-meshcentral:${{ steps.prep.outputs.version }},sadnub/tactical-meshcentral:latest + + - name: Build and Push Tactical NATS Image + uses: docker/build-push-action@v2 + with: + context: . + push: true + pull: true + file: ./docker/containers/tactical-nats/dockerfile + platforms: linux/amd64 + tags: tacticalrmm/tactical-nats:${{ steps.prep.outputs.version }},sadnub/tactical-nats:latest + + - name: Build and Push Tactical Salt Image + uses: docker/build-push-action@v2 + with: + context: . + push: true + pull: true + file: ./docker/containers/tactical-salt/dockerfile + platforms: linux/amd64 + tags: tacticalrmm/tactical-salt:${{ steps.prep.outputs.version }},sadnub/tactical-salt:latest + + - name: Build and Push Tactical Frontend Image + uses: docker/build-push-action@v2 + with: + context: . + push: true + pull: true + file: ./docker/containers/tactical-frontend/dockerfile + platforms: linux/amd64 + tags: tacticalrmm/tactical-frontend:${{ steps.prep.outputs.version }},sadnub/tactical-frontend:latest + + - name: Build and Push Tactical Nginx Image + uses: docker/build-push-action@v2 + with: + context: . + push: true + pull: true + file: ./docker/containers/tactical-nginx/dockerfile + platforms: linux/amd64 + tags: tacticalrmm/tactical-nginx:${{ steps.prep.outputs.version }},sadnub/tactical-nginx:latest From e6aa2c3b786d10b6b7958ff5838fe404473185d4 Mon Sep 17 00:00:00 2001 From: sadnub Date: Sat, 28 Nov 2020 09:47:41 -0500 Subject: [PATCH 02/45] Delete docker-build-publish.yml --- .github/workflows/docker-build-publish.yml | 87 ---------------------- 1 file changed, 87 deletions(-) delete mode 100644 .github/workflows/docker-build-publish.yml diff --git a/.github/workflows/docker-build-publish.yml b/.github/workflows/docker-build-publish.yml deleted file mode 100644 index b53e75ed39..0000000000 --- a/.github/workflows/docker-build-publish.yml +++ /dev/null @@ -1,87 +0,0 @@ -name: Publish Tactical Docker Images -on: - release: - types: [published] -jobs: - docker: - name: Build and Push Docker Images - runs-on: ubuntu-latest - steps: - - name: Check out the repo - uses: actions/checkout@v2 - - - name: Get Github Tag - id: prep - run: | - echo ::set-output name=version::${GITHUB_REF#refs/tags/v} - - name: Set up QEMU - uses: docker/setup-qemu-action@v1 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 - - - name: Login to DockerHub - uses: docker/login-action@v1 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Build and Push Tactical Image - uses: docker/build-push-action@v2 - with: - context: . - push: true - pull: true - file: ./docker/containers/tactical/dockerfile - platforms: linux/amd64 - tags: tacticalrmm/tactical:${{ steps.prep.outputs.version }},sadnub/tactical:latest - - - name: Build and Push Tactical MeshCentral Image - uses: docker/build-push-action@v2 - with: - context: . - push: true - pull: true - file: ./docker/containers/tactical-meshcentral/dockerfile - platforms: linux/amd64 - tags: tacticalrmm/tactical-meshcentral:${{ steps.prep.outputs.version }},sadnub/tactical-meshcentral:latest - - - name: Build and Push Tactical NATS Image - uses: docker/build-push-action@v2 - with: - context: . - push: true - pull: true - file: ./docker/containers/tactical-nats/dockerfile - platforms: linux/amd64 - tags: tacticalrmm/tactical-nats:${{ steps.prep.outputs.version }},sadnub/tactical-nats:latest - - - name: Build and Push Tactical Salt Image - uses: docker/build-push-action@v2 - with: - context: . - push: true - pull: true - file: ./docker/containers/tactical-salt/dockerfile - platforms: linux/amd64 - tags: tacticalrmm/tactical-salt:${{ steps.prep.outputs.version }},sadnub/tactical-salt:latest - - - name: Build and Push Tactical Frontend Image - uses: docker/build-push-action@v2 - with: - context: . - push: true - pull: true - file: ./docker/containers/tactical-frontend/dockerfile - platforms: linux/amd64 - tags: tacticalrmm/tactical-frontend:${{ steps.prep.outputs.version }},sadnub/tactical-frontend:latest - - - name: Build and Push Tactical Nginx Image - uses: docker/build-push-action@v2 - with: - context: . - push: true - pull: true - file: ./docker/containers/tactical-nginx/dockerfile - platforms: linux/amd64 - tags: tacticalrmm/tactical-nginx:${{ steps.prep.outputs.version }},sadnub/tactical-nginx:latest From c28c1efbb19eb9f8c073802954b0c4bab4bcbffe Mon Sep 17 00:00:00 2001 From: Josh Date: Sun, 29 Nov 2020 02:13:50 +0000 Subject: [PATCH 03/45] Add pending actions to agent table and filter --- api/tacticalrmm/agents/serializers.py | 5 ++++ .../management/commands/initial_mesh_setup.py | 23 +++++++++++------- docker/docker-compose.yml | 4 +++- web/src/components/AgentTable.vue | 15 ++++++++++++ web/src/views/Dashboard.vue | 24 +++++++++++++++++++ 5 files changed, 62 insertions(+), 9 deletions(-) diff --git a/api/tacticalrmm/agents/serializers.py b/api/tacticalrmm/agents/serializers.py index 51d5ebbce4..1365b454d4 100644 --- a/api/tacticalrmm/agents/serializers.py +++ b/api/tacticalrmm/agents/serializers.py @@ -36,12 +36,16 @@ class Meta: class AgentTableSerializer(serializers.ModelSerializer): patches_pending = serializers.ReadOnlyField(source="has_patches_pending") + pending_actions = serializers.SerializerMethodField() status = serializers.ReadOnlyField() checks = serializers.ReadOnlyField() last_seen = serializers.SerializerMethodField() client_name = serializers.ReadOnlyField(source="client.name") site_name = serializers.ReadOnlyField(source="site.name") + def get_pending_actions(self, obj): + return obj.pendingactions.filter(status="pending").count() + def get_last_seen(self, obj): if obj.time_zone is not None: agent_tz = pytz.timezone(obj.time_zone) @@ -62,6 +66,7 @@ class Meta: "description", "needs_reboot", "patches_pending", + "pending_actions", "status", "overdue_text_alert", "overdue_email_alert", diff --git a/api/tacticalrmm/core/management/commands/initial_mesh_setup.py b/api/tacticalrmm/core/management/commands/initial_mesh_setup.py index 323a52d569..7baecfb350 100644 --- a/api/tacticalrmm/core/management/commands/initial_mesh_setup.py +++ b/api/tacticalrmm/core/management/commands/initial_mesh_setup.py @@ -12,12 +12,11 @@ class Command(BaseCommand): async def websocket_call(self, mesh_settings): - token = get_auth_token( - mesh_settings.mesh_username, mesh_settings.mesh_token - ) + token = get_auth_token(mesh_settings.mesh_username, mesh_settings.mesh_token) - if settings.MESH_WS_URL: - uri = f"{settings.MESH_WS_URL}/control.ashx?auth={token}" + if settings.DOCKER_BUILD: + site = mesh_settings.mesh_site.replace("https", "ws") + uri = f"{site}:443/control.ashx?auth={token}" else: site = mesh_settings.mesh_site.replace("https", "wss") uri = f"{site}/control.ashx?auth={token}" @@ -52,11 +51,17 @@ def handle(self, *args, **kwargs): try: # Check for Mesh Username - if not mesh_settings.mesh_username or settings.MESH_USERNAME != mesh_settings.mesh_username: + if ( + not mesh_settings.mesh_username + or settings.MESH_USERNAME != mesh_settings.mesh_username + ): mesh_settings.mesh_username = settings.MESH_USERNAME # Check for Mesh Site - if not mesh_settings.mesh_site or settings.MESH_SITE != mesh_settings.mesh_site: + if ( + not mesh_settings.mesh_site + or settings.MESH_SITE != mesh_settings.mesh_site + ): mesh_settings.mesh_site = settings.MESH_SITE # Check for Mesh Token @@ -75,7 +80,9 @@ def handle(self, *args, **kwargs): return try: - asyncio.get_event_loop().run_until_complete(self.websocket_call(mesh_settings)) + asyncio.get_event_loop().run_until_complete( + self.websocket_call(mesh_settings) + ) self.stdout.write("Initial Mesh Central setup complete") except websockets.exceptions.ConnectionClosedError: self.stdout.write( diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 85ee32a324..5beb06d535 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -101,7 +101,9 @@ services: MONGODB_USER: ${MONGODB_USER} MONGODB_PASSWORD: ${MONGODB_PASSWORD} networks: - - proxy + proxy: + aliases: + - ${MESH_HOST} - mesh-db volumes: - tactical_data:/opt/tactical diff --git a/web/src/components/AgentTable.vue b/web/src/components/AgentTable.vue index e0555f8605..51ff828a05 100644 --- a/web/src/components/AgentTable.vue +++ b/web/src/components/AgentTable.vue @@ -46,6 +46,13 @@ + @@ -138,7 +138,7 @@ dense @input="checkAlert(props.row.id, 'Email', props.row.email_alert, props.row.managed_by_policy)" v-model="props.row.email_alert" - :disabled="props.row.managed_by_policy" + :disable="props.row.managed_by_policy" /> diff --git a/web/src/components/modals/agents/PatchPolicyForm.vue b/web/src/components/modals/agents/PatchPolicyForm.vue index 902c534648..ed1236bf04 100644 --- a/web/src/components/modals/agents/PatchPolicyForm.vue +++ b/web/src/components/modals/agents/PatchPolicyForm.vue @@ -93,7 +93,7 @@
Day of month to run:
Scheduled Time:
- +
@@ -205,17 +202,17 @@
From d39bdce926dde8d7d48f7c375e461b328fdd4496 Mon Sep 17 00:00:00 2001 From: Josh Date: Sun, 29 Nov 2020 03:30:31 +0000 Subject: [PATCH 07/45] add install agent to site context menu --- .../management/commands/get_mesh_exe_url.py | 9 +++--- .../components/modals/agents/InstallAgent.vue | 18 ++++++++++-- web/src/views/Dashboard.vue | 28 +++++++++++++++++++ 3 files changed, 48 insertions(+), 7 deletions(-) diff --git a/api/tacticalrmm/core/management/commands/get_mesh_exe_url.py b/api/tacticalrmm/core/management/commands/get_mesh_exe_url.py index d07b9ec1e6..ab5bb57e97 100644 --- a/api/tacticalrmm/core/management/commands/get_mesh_exe_url.py +++ b/api/tacticalrmm/core/management/commands/get_mesh_exe_url.py @@ -11,12 +11,11 @@ class Command(BaseCommand): help = "Sets up initial mesh central configuration" async def websocket_call(self, mesh_settings): - token = get_auth_token( - mesh_settings.mesh_username, mesh_settings.mesh_token - ) + token = get_auth_token(mesh_settings.mesh_username, mesh_settings.mesh_token) - if settings.MESH_WS_URL: - uri = f"{settings.MESH_WS_URL}/control.ashx?auth={token}" + if settings.DOCKER_BUILD: + site = mesh_settings.mesh_site.replace("https", "ws") + uri = f"{site}:443/control.ashx?auth={token}" else: site = mesh_settings.mesh_site.replace("https", "wss") uri = f"{site}/control.ashx?auth={token}" diff --git a/web/src/components/modals/agents/InstallAgent.vue b/web/src/components/modals/agents/InstallAgent.vue index a1bf77014c..874b868e12 100644 --- a/web/src/components/modals/agents/InstallAgent.vue +++ b/web/src/components/modals/agents/InstallAgent.vue @@ -87,6 +87,9 @@ export default { name: "InstallAgent", mixins: [mixins], components: { AgentDownload }, + props: { + sitepk: Number, + }, data() { return { client_options: [], @@ -110,8 +113,19 @@ export default { .get("/clients/clients/") .then(r => { this.client_options = this.formatClientOptions(r.data); - this.client = this.client_options[0]; - this.site = this.sites[0]; + if (this.sitepk !== undefined && this.sitepk !== null) { + this.client_options.forEach(client => { + let site = client.sites.find(site => site.id === this.sitepk); + + if (site !== undefined) { + this.client = client; + this.site = { value: site.id, label: site.name }; + } + }); + } else { + this.client = this.client_options[0]; + this.site = this.sites[0]; + } this.$q.loading.hide(); }) .catch(() => { diff --git a/web/src/views/Dashboard.vue b/web/src/views/Dashboard.vue index b0d285ca0c..2bb0f63b4c 100644 --- a/web/src/views/Dashboard.vue +++ b/web/src/views/Dashboard.vue @@ -136,6 +136,18 @@ {{ menuMaintenanceText(props.node) }} + + + + + Install Agent + + @@ -338,6 +350,10 @@ + + + + @@ -352,6 +368,7 @@ import AlertsIcon from "@/components/AlertsIcon"; import PolicyAdd from "@/components/automation/modals/PolicyAdd"; import ClientsForm from "@/components/modals/clients/ClientsForm"; import SitesForm from "@/components/modals/clients/SitesForm"; +import InstallAgent from "@/components/modals/agents/InstallAgent"; export default { components: { @@ -362,6 +379,7 @@ export default { PolicyAdd, ClientsForm, SitesForm, + InstallAgent, }, data() { return { @@ -370,6 +388,8 @@ export default { showSitesFormModal: false, showPolicyAddModal: false, deleteEditModalPk: null, + showInstallAgentModal: false, + sitePk: null, clientOp: null, policyAddType: null, policyAddPk: null, @@ -619,6 +639,14 @@ export default { this.deleteEditModalPk = null; this.clientOp = null; }, + showInstallAgent(node) { + this.sitePk = node.id; + this.showInstallAgentModal = true; + }, + closeInstallAgent() { + this.showInstallAgentModal = false; + this.sitePk = null; + }, reload() { this.$store.dispatch("reload"); }, From 216f7a38cfd179623781929a8dc0ed5bdae772aa Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Sun, 29 Nov 2020 04:15:57 +0000 Subject: [PATCH 08/45] support mesh > 0.6.84 wh1te909/rmmagent@85aab2facff2a6e499599ad5bb2f911a93932928 --- api/tacticalrmm/agents/tests.py | 9 ++++++++- api/tacticalrmm/agents/views.py | 14 +++----------- api/tacticalrmm/apiv3/views.py | 10 +++++++++- web/src/components/AgentTable.vue | 21 --------------------- 4 files changed, 20 insertions(+), 34 deletions(-) diff --git a/api/tacticalrmm/agents/tests.py b/api/tacticalrmm/agents/tests.py index 9b1cafec39..df7a31a9b2 100644 --- a/api/tacticalrmm/agents/tests.py +++ b/api/tacticalrmm/agents/tests.py @@ -428,7 +428,14 @@ def test_meshcentral_tabs(self, mock_token): self.assertIn("&viewmode=13", r.data["file"]) self.assertIn("&viewmode=12", r.data["terminal"]) self.assertIn("&viewmode=11", r.data["control"]) - self.assertIn("mstsc.html?login=", r.data["webrdp"]) + + self.assertIn("&gotonode=", r.data["file"]) + self.assertIn("&gotonode=", r.data["terminal"]) + self.assertIn("&gotonode=", r.data["control"]) + + self.assertIn("?login=", r.data["file"]) + self.assertIn("?login=", r.data["terminal"]) + self.assertIn("?login=", r.data["control"]) self.assertEqual(self.agent.hostname, r.data["hostname"]) self.assertEqual(self.agent.client.name, r.data["client"]) diff --git a/api/tacticalrmm/agents/views.py b/api/tacticalrmm/agents/views.py index 3c52ac1630..36286c09cc 100644 --- a/api/tacticalrmm/agents/views.py +++ b/api/tacticalrmm/agents/views.py @@ -126,16 +126,9 @@ def meshcentral(request, pk): if token == "err": return notify_error("Invalid mesh token") - control = ( - f"{core.mesh_site}/?login={token}&node={agent.mesh_node_id}&viewmode=11&hide=31" - ) - terminal = ( - f"{core.mesh_site}/?login={token}&node={agent.mesh_node_id}&viewmode=12&hide=31" - ) - file = ( - f"{core.mesh_site}/?login={token}&node={agent.mesh_node_id}&viewmode=13&hide=31" - ) - webrdp = f"{core.mesh_site}/mstsc.html?login={token}&node={agent.mesh_node_id}" + control = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=11&hide=31" + terminal = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=12&hide=31" + file = f"{core.mesh_site}/?login={token}&gotonode={agent.mesh_node_id}&viewmode=13&hide=31" AuditLog.audit_mesh_session(username=request.user.username, hostname=agent.hostname) @@ -144,7 +137,6 @@ def meshcentral(request, pk): "control": control, "terminal": terminal, "file": file, - "webrdp": webrdp, "status": agent.status, "client": agent.client.name, "site": agent.site.name, diff --git a/api/tacticalrmm/apiv3/views.py b/api/tacticalrmm/apiv3/views.py index ebefd87e4e..bf0dc053a1 100644 --- a/api/tacticalrmm/apiv3/views.py +++ b/api/tacticalrmm/apiv3/views.py @@ -386,7 +386,15 @@ def get(self, request, pk): def patch(self, request, pk): agent = get_object_or_404(Agent, pk=pk) - agent.mesh_node_id = request.data["nodeidhex"] + + if "nodeidhex" in request.data: + # agent <= 1.1.0 + nodeid = request.data["nodeidhex"] + else: + # agent >= 1.1.1 + nodeid = request.data["nodeid"] + + agent.mesh_node_id = nodeid agent.save(update_fields=["mesh_node_id"]) return Response("ok") diff --git a/web/src/components/AgentTable.vue b/web/src/components/AgentTable.vue index 51ff828a05..5ae5bbdf6a 100644 --- a/web/src/components/AgentTable.vue +++ b/web/src/components/AgentTable.vue @@ -108,14 +108,6 @@ Take Control - - - - - - Remote Desktop - - @@ -613,19 +605,6 @@ export default { this.policyAddPk = pk; this.showPolicyAddModal = true; }, - webRDP(pk) { - this.$q.loading.show(); - this.$axios - .get(`/agents/${pk}/meshcentral/`) - .then(r => { - this.$q.loading.hide(); - openURL(r.data.webrdp); - }) - .catch(e => { - this.$q.loading.hide(); - this.notifyError(e.response.data); - }); - }, toggleMaintenance(agent) { let data = { id: agent.id, From 156c0fe7f6e8be3ff641a54b152e67ff9ed5b189 Mon Sep 17 00:00:00 2001 From: Josh Date: Sun, 29 Nov 2020 04:26:53 +0000 Subject: [PATCH 09/45] add dockerignore and get MESH_VER from settings.py --- .dockerignore | 5 +++++ docker/containers/tactical-frontend/dockerfile | 2 +- docker/containers/tactical-meshcentral/dockerfile | 7 +++++-- docker/containers/tactical-nats/dockerfile | 2 +- docker/containers/tactical-nginx/dockerfile | 2 +- docker/containers/tactical-salt/dockerfile | 2 ++ 6 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..041f7d5ad7 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +.git +.cache +**/*.env +**/env +**/node_modules diff --git a/docker/containers/tactical-frontend/dockerfile b/docker/containers/tactical-frontend/dockerfile index 15b388b1f6..5a525cbe73 100644 --- a/docker/containers/tactical-frontend/dockerfile +++ b/docker/containers/tactical-frontend/dockerfile @@ -19,7 +19,7 @@ FROM nginx:stable-alpine ENV PUBLIC_DIR /usr/share/nginx/html RUN apk add --no-cache bash -SHELL ["/bin/bash", "-c"] +SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"] COPY --from=builder /home/node/app/dist/ ${PUBLIC_DIR} diff --git a/docker/containers/tactical-meshcentral/dockerfile b/docker/containers/tactical-meshcentral/dockerfile index 8b109cc86f..a9aed465f2 100644 --- a/docker/containers/tactical-meshcentral/dockerfile +++ b/docker/containers/tactical-meshcentral/dockerfile @@ -6,9 +6,12 @@ ENV TACTICAL_DIR /opt/tactical RUN apk add --no-cache bash -SHELL ["/bin/bash", "-c"] +SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"] -RUN npm install meshcentral@0.6.84 +COPY api/tacticalrmm/tacticalrmm/settings.py /tmp/settings.py + +RUN grep -o 'MESH_VER.*' /tmp/settings.py | cut -d'"' -f 2 > /tmp/MESH_VER && \ + npm install meshcentral@$(cat /tmp/MESH_VER) COPY docker/containers/tactical-meshcentral/entrypoint.sh / RUN chmod +x /entrypoint.sh diff --git a/docker/containers/tactical-nats/dockerfile b/docker/containers/tactical-nats/dockerfile index 93609a444e..da6f51c743 100644 --- a/docker/containers/tactical-nats/dockerfile +++ b/docker/containers/tactical-nats/dockerfile @@ -5,7 +5,7 @@ ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready RUN apk add --no-cache inotify-tools supervisor bash -SHELL ["/bin/bash", "-c"] +SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"] COPY docker/containers/tactical-nats/entrypoint.sh / RUN chmod +x /entrypoint.sh diff --git a/docker/containers/tactical-nginx/dockerfile b/docker/containers/tactical-nginx/dockerfile index 279818fe5f..ec8f05c735 100644 --- a/docker/containers/tactical-nginx/dockerfile +++ b/docker/containers/tactical-nginx/dockerfile @@ -4,7 +4,7 @@ ENV TACTICAL_DIR /opt/tactical RUN apk add --no-cache openssl bash -SHELL ["/bin/bash", "-c"] +SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"] COPY docker/containers/tactical-nginx/entrypoint.sh /docker-entrypoint.d/ RUN chmod +x /docker-entrypoint.d/entrypoint.sh diff --git a/docker/containers/tactical-salt/dockerfile b/docker/containers/tactical-salt/dockerfile index 21c90ec23a..ca3dbe2431 100644 --- a/docker/containers/tactical-salt/dockerfile +++ b/docker/containers/tactical-salt/dockerfile @@ -4,6 +4,8 @@ ENV TACTICAL_DIR /opt/tactical ENV TACTICAL_READY_FILE ${TACTICAL_DIR}/tmp/tactical.ready ENV SALT_USER saltapi +SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"] + RUN apt-get update && \ apt-get install -y ca-certificates wget gnupg2 tzdata supervisor && \ wget -O - https://repo.saltstack.com/py3/ubuntu/20.04/amd64/latest/SALTSTACK-GPG-KEY.pub | apt-key add - && \ From c20751829b54ca2cbd78a2fe58944f9c5d40e5ac Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Sun, 29 Nov 2020 10:37:46 +0000 Subject: [PATCH 10/45] create migration for schedtask weekdays --- ...009_automatedtask_run_time_bit_weekdays.py | 18 ++++++++++ .../0010_migrate_days_to_bitdays.py | 33 +++++++++++++++++++ api/tacticalrmm/autotasks/models.py | 2 ++ api/tacticalrmm/tacticalrmm/utils.py | 18 ++++++++++ 4 files changed, 71 insertions(+) create mode 100644 api/tacticalrmm/autotasks/migrations/0009_automatedtask_run_time_bit_weekdays.py create mode 100644 api/tacticalrmm/autotasks/migrations/0010_migrate_days_to_bitdays.py diff --git a/api/tacticalrmm/autotasks/migrations/0009_automatedtask_run_time_bit_weekdays.py b/api/tacticalrmm/autotasks/migrations/0009_automatedtask_run_time_bit_weekdays.py new file mode 100644 index 0000000000..172d3fe704 --- /dev/null +++ b/api/tacticalrmm/autotasks/migrations/0009_automatedtask_run_time_bit_weekdays.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.3 on 2020-11-29 09:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('autotasks', '0008_auto_20201030_1515'), + ] + + operations = [ + migrations.AddField( + model_name='automatedtask', + name='run_time_bit_weekdays', + field=models.IntegerField(blank=True, null=True), + ), + ] diff --git a/api/tacticalrmm/autotasks/migrations/0010_migrate_days_to_bitdays.py b/api/tacticalrmm/autotasks/migrations/0010_migrate_days_to_bitdays.py new file mode 100644 index 0000000000..ad479fd9bb --- /dev/null +++ b/api/tacticalrmm/autotasks/migrations/0010_migrate_days_to_bitdays.py @@ -0,0 +1,33 @@ +from django.db import migrations +from tacticalrmm.utils import get_bit_days + +DAYS_OF_WEEK = { + 0: "Monday", + 1: "Tuesday", + 2: "Wednesday", + 3: "Thursday", + 4: "Friday", + 5: "Saturday", + 6: "Sunday", +} + + +def migrate_days(apps, schema_editor): + AutomatedTask = apps.get_model("autotasks", "AutomatedTask") + for task in AutomatedTask.objects.exclude(run_time_days__isnull=True).exclude( + run_time_days=[] + ): + run_days = [DAYS_OF_WEEK.get(day) for day in task.run_time_days] + task.run_time_bit_weekdays = get_bit_days(run_days) + task.save(update_fields=["run_time_bit_weekdays"]) + + +class Migration(migrations.Migration): + + dependencies = [ + ("autotasks", "0009_automatedtask_run_time_bit_weekdays"), + ] + + operations = [ + migrations.RunPython(migrate_days), + ] diff --git a/api/tacticalrmm/autotasks/models.py b/api/tacticalrmm/autotasks/models.py index 20af26cb0c..94926f913d 100644 --- a/api/tacticalrmm/autotasks/models.py +++ b/api/tacticalrmm/autotasks/models.py @@ -69,6 +69,8 @@ class AutomatedTask(BaseAuditModel): on_delete=models.SET_NULL, ) name = models.CharField(max_length=255) + run_time_bit_weekdays = models.IntegerField(null=True, blank=True) + # run_time_days is deprecated, use bit weekdays run_time_days = ArrayField( models.IntegerField(choices=RUN_TIME_DAY_CHOICES, null=True, blank=True), null=True, diff --git a/api/tacticalrmm/tacticalrmm/utils.py b/api/tacticalrmm/tacticalrmm/utils.py index 14f3d6cd96..8823797ac6 100644 --- a/api/tacticalrmm/tacticalrmm/utils.py +++ b/api/tacticalrmm/tacticalrmm/utils.py @@ -1,6 +1,7 @@ import json import os import subprocess +from typing import List from loguru import logger from django.conf import settings @@ -13,6 +14,23 @@ notify_error = lambda msg: Response(msg, status=status.HTTP_400_BAD_REQUEST) +WEEK_DAYS = { + "Sunday": 0x1, + "Monday": 0x2, + "Tuesday": 0x4, + "Wednesday": 0x8, + "Thursday": 0x10, + "Friday": 0x20, + "Saturday": 0x40, +} + + +def get_bit_days(days: List[str]) -> int: + bit_days = 0 + for day in days: + bit_days |= WEEK_DAYS.get(day) + return bit_days + def reload_nats(): users = [{"user": "tacticalrmm", "password": settings.SECRET_KEY}] From 1afe61c593e1ab1b086d4954ba697712382ca25e Mon Sep 17 00:00:00 2001 From: sadnub Date: Sun, 29 Nov 2020 14:24:32 -0500 Subject: [PATCH 11/45] fix docker-compose.yml --- docker/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 5beb06d535..12e033d215 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -104,7 +104,7 @@ services: proxy: aliases: - ${MESH_HOST} - - mesh-db + mesh-db: volumes: - tactical_data:/opt/tactical - mesh_data:/home/node/app/meshcentral-data From 426ebad300dd2b7a5d0a820088b246af7a75cae5 Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Sun, 29 Nov 2020 23:40:29 +0000 Subject: [PATCH 12/45] start moving schedtasks to nats wh1te909/rmmagent@0cde11a067e84fa175eb21249bd3681449b0853d --- .vscode/settings.json | 2 +- api/tacticalrmm/agents/models.py | 59 +------------- api/tacticalrmm/agents/tests.py | 45 ++++++----- api/tacticalrmm/agents/urls.py | 3 +- api/tacticalrmm/agents/views.py | 81 ++++++++++++------- api/tacticalrmm/logs/tasks.py | 29 ------- api/tacticalrmm/logs/tests.py | 59 +++++--------- api/tacticalrmm/logs/views.py | 20 +++-- api/tacticalrmm/winupdate/tests.py | 4 +- web/src/components/AgentTable.vue | 16 ++-- .../components/modals/agents/RebootLater.vue | 5 +- 11 files changed, 131 insertions(+), 192 deletions(-) delete mode 100644 api/tacticalrmm/logs/tasks.py diff --git a/.vscode/settings.json b/.vscode/settings.json index 44ee3c0dcc..bc3b5a375e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,7 +2,7 @@ "python.pythonPath": "api/tacticalrmm/env/bin/python", "python.languageServer": "Pylance", "python.analysis.extraPaths": [ - "api/tacticalrmm" + "api/tacticalrmm", ], "python.analysis.typeCheckingMode": "basic", "python.formatting.provider": "black", diff --git a/api/tacticalrmm/agents/models.py b/api/tacticalrmm/agents/models.py index ff02fdfd98..6c1d972ea3 100644 --- a/api/tacticalrmm/agents/models.py +++ b/api/tacticalrmm/agents/models.py @@ -1,5 +1,4 @@ import requests -import datetime as dt import time import base64 from Crypto.Cipher import AES @@ -8,9 +7,7 @@ from Crypto.Util.Padding import pad import validators import msgpack -import random import re -import string from collections import Counter from loguru import logger from packaging import version as pyver @@ -89,6 +86,10 @@ def client(self): def has_nats(self): return pyver.parse(self.version) >= pyver.parse("1.1.0") + @property + def has_gotasks(self): + return pyver.parse(self.version) >= pyver.parse("1.1.1") + @property def timezone(self): # return the default timezone unless the timezone is explicity set per agent @@ -573,58 +574,6 @@ def salt_batch_async(**kwargs): return resp - def schedule_reboot(self, obj): - - start_date = dt.datetime.strftime(obj, "%Y-%m-%d") - start_time = dt.datetime.strftime(obj, "%H:%M") - - # let windows task scheduler automatically delete the task after it runs - end_obj = obj + dt.timedelta(minutes=15) - end_date = dt.datetime.strftime(end_obj, "%Y-%m-%d") - end_time = dt.datetime.strftime(end_obj, "%H:%M") - - task_name = "TacticalRMM_SchedReboot_" + "".join( - random.choice(string.ascii_letters) for _ in range(10) - ) - - r = self.salt_api_cmd( - timeout=15, - func="task.create_task", - arg=[ - f"name={task_name}", - "force=True", - "action_type=Execute", - 'cmd="C:\\Windows\\System32\\shutdown.exe"', - 'arguments="/r /t 5 /f"', - "trigger_type=Once", - f'start_date="{start_date}"', - f'start_time="{start_time}"', - f'end_date="{end_date}"', - f'end_time="{end_time}"', - "ac_only=False", - "stop_if_on_batteries=False", - "delete_after=Immediately", - ], - ) - - if r == "error" or (isinstance(r, bool) and not r): - return "failed" - elif r == "timeout": - return "timeout" - elif isinstance(r, bool) and r: - from logs.models import PendingAction - - details = { - "taskname": task_name, - "time": str(obj), - } - PendingAction(agent=self, action_type="schedreboot", details=details).save() - - nice_time = dt.datetime.strftime(obj, "%B %d, %Y at %I:%M %p") - return {"msg": {"time": nice_time, "agent": self.hostname}} - else: - return "failed" - def not_supported(self, version_added): return pyver.parse(self.version) < pyver.parse(version_added) diff --git a/api/tacticalrmm/agents/tests.py b/api/tacticalrmm/agents/tests.py index df7a31a9b2..d3e44fb963 100644 --- a/api/tacticalrmm/agents/tests.py +++ b/api/tacticalrmm/agents/tests.py @@ -33,7 +33,7 @@ def setUp(self): client = baker.make("clients.Client", name="Google") site = baker.make("clients.Site", client=client, name="LA Office") self.agent = baker.make_recipe( - "agents.online_agent", site=site, version="1.1.0" + "agents.online_agent", site=site, version="1.1.1" ) baker.make_recipe("winupdate.winupdate_policy", agent=self.agent) @@ -186,10 +186,10 @@ def test_get_event_log(self, mock_ret): self.check_not_authenticated("get", url) @patch("agents.models.Agent.nats_cmd") - def test_power_action(self, nats_cmd): - url = f"/agents/poweraction/" + def test_reboot_now(self, nats_cmd): + url = f"/agents/reboot/" - data = {"pk": self.agent.pk, "action": "rebootnow"} + data = {"pk": self.agent.pk} nats_cmd.return_value = "ok" r = self.client.post(url, data, format="json") self.assertEqual(r.status_code, 200) @@ -222,30 +222,37 @@ def test_send_raw_cmd(self, mock_ret): self.check_not_authenticated("post", url) - @patch("agents.models.Agent.salt_api_cmd") - def test_reboot_later(self, mock_ret): - url = f"/agents/rebootlater/" + @patch("agents.models.Agent.nats_cmd") + def test_reboot_later(self, nats_cmd): + url = f"/agents/reboot/" data = { "pk": self.agent.pk, "datetime": "2025-08-29 18:41", } - mock_ret.return_value = True - r = self.client.post(url, data, format="json") + nats_cmd.return_value = "ok" + r = self.client.patch(url, data, format="json") self.assertEqual(r.status_code, 200) self.assertEqual(r.data["time"], "August 29, 2025 at 06:41 PM") self.assertEqual(r.data["agent"], self.agent.hostname) - mock_ret.return_value = "failed" - r = self.client.post(url, data, format="json") - self.assertEqual(r.status_code, 400) - - mock_ret.return_value = "timeout" - r = self.client.post(url, data, format="json") - self.assertEqual(r.status_code, 400) + nats_data = { + "func": "schedtask", + "schedtaskpayload": { + "type": "schedreboot", + "trigger": "once", + "name": r.data["task_name"], + "year": 2025, + "month": "August", + "day": 29, + "hour": 18, + "min": 41, + }, + } + nats_cmd.assert_called_with(nats_data, timeout=10) - mock_ret.return_value = False + nats_cmd.return_value = "error creating task" r = self.client.post(url, data, format="json") self.assertEqual(r.status_code, 400) @@ -253,12 +260,12 @@ def test_reboot_later(self, mock_ret): "pk": self.agent.pk, "datetime": "rm -rf /", } - r = self.client.post(url, data_invalid, format="json") + r = self.client.patch(url, data_invalid, format="json") self.assertEqual(r.status_code, 400) self.assertEqual(r.data, "Invalid date") - self.check_not_authenticated("post", url) + self.check_not_authenticated("patch", url) @patch("os.path.exists") @patch("subprocess.run") diff --git a/api/tacticalrmm/agents/urls.py b/api/tacticalrmm/agents/urls.py index bd6da60b66..51e7057b01 100644 --- a/api/tacticalrmm/agents/urls.py +++ b/api/tacticalrmm/agents/urls.py @@ -12,7 +12,6 @@ path("/agentdetail/", views.agent_detail), path("/meshcentral/", views.meshcentral), path("/getmeshexe/", views.get_mesh_exe), - path("poweraction/", views.power_action), path("uninstall/", views.uninstall), path("editagent/", views.edit_agent), path("/geteventlog///", views.get_event_log), @@ -20,7 +19,7 @@ path("updateagents/", views.update_agents), path("/getprocs/", views.get_processes), path("//killproc/", views.kill_proc), - path("rebootlater/", views.reboot_later), + path("reboot/", views.Reboot.as_view()), path("installagent/", views.install_agent), path("/ping/", views.ping), path("recover/", views.recover), diff --git a/api/tacticalrmm/agents/views.py b/api/tacticalrmm/agents/views.py index 36286c09cc..2fa13dede5 100644 --- a/api/tacticalrmm/agents/views.py +++ b/api/tacticalrmm/agents/views.py @@ -3,6 +3,8 @@ import os import subprocess import pytz +import random +import string import datetime as dt from packaging import version as pyver @@ -18,7 +20,7 @@ from .models import Agent, AgentOutage, RecoveryAction, Note from core.models import CoreSettings from scripts.models import Script -from logs.models import AuditLog +from logs.models import AuditLog, PendingAction from .serializers import ( AgentSerializer, @@ -200,19 +202,6 @@ def get_event_log(request, pk, logtype, days): return Response(r) -@api_view(["POST"]) -def power_action(request): - agent = get_object_or_404(Agent, pk=request.data["pk"]) - if not agent.has_nats: - return notify_error("Requires agent version 1.1.0 or greater") - if request.data["action"] == "rebootnow": - r = asyncio.run(agent.nats_cmd({"func": "rebootnow"}, timeout=10)) - if r != "ok": - return notify_error("Unable to contact the agent") - - return Response("ok") - - @api_view(["POST"]) def send_raw_cmd(request): agent = get_object_or_404(Agent, pk=request.data["pk"]) @@ -371,24 +360,60 @@ def overdue_action(request): return Response(agent.hostname) -@api_view(["POST"]) -def reboot_later(request): - agent = get_object_or_404(Agent, pk=request.data["pk"]) - date_time = request.data["datetime"] +class Reboot(APIView): + # reboot now + def post(self, request): + agent = get_object_or_404(Agent, pk=request.data["pk"]) + if not agent.has_nats: + return notify_error("Requires agent version 1.1.0 or greater") - try: - obj = dt.datetime.strptime(date_time, "%Y-%m-%d %H:%M") - except Exception: - return notify_error("Invalid date") + r = asyncio.run(agent.nats_cmd({"func": "rebootnow"}, timeout=10)) + if r != "ok": + return notify_error("Unable to contact the agent") - r = agent.schedule_reboot(obj) + return Response("ok") - if r == "timeout": - return notify_error("Unable to contact the agent") - elif r == "failed": - return notify_error("Something went wrong") + # reboot later + def patch(self, request): + agent = get_object_or_404(Agent, pk=request.data["pk"]) + if not agent.has_gotasks: + return notify_error("Requires agent version 1.1.1 or greater") + + try: + obj = dt.datetime.strptime(request.data["datetime"], "%Y-%m-%d %H:%M") + except Exception: + return notify_error("Invalid date") + + task_name = "TacticalRMM_SchedReboot_" + "".join( + random.choice(string.ascii_letters) for _ in range(10) + ) - return Response(r["msg"]) + nats_data = { + "func": "schedtask", + "schedtaskpayload": { + "type": "schedreboot", + "trigger": "once", + "name": task_name, + "year": int(dt.datetime.strftime(obj, "%Y")), + "month": dt.datetime.strftime(obj, "%B"), + "day": int(dt.datetime.strftime(obj, "%d")), + "hour": int(dt.datetime.strftime(obj, "%H")), + "min": int(dt.datetime.strftime(obj, "%M")), + }, + } + + r = asyncio.run(agent.nats_cmd(nats_data, timeout=10)) + if r != "ok": + return notify_error(r) + + details = {"taskname": task_name, "time": str(obj)} + PendingAction.objects.create( + agent=agent, action_type="schedreboot", details=details + ) + nice_time = dt.datetime.strftime(obj, "%B %d, %Y at %I:%M %p") + return Response( + {"time": nice_time, "agent": agent.hostname, "task_name": task_name} + ) @api_view(["POST"]) diff --git a/api/tacticalrmm/logs/tasks.py b/api/tacticalrmm/logs/tasks.py deleted file mode 100644 index 6b64222fd7..0000000000 --- a/api/tacticalrmm/logs/tasks.py +++ /dev/null @@ -1,29 +0,0 @@ -from loguru import logger -from tacticalrmm.celery import app -from django.conf import settings - -logger.configure(**settings.LOG_CONFIG) - - -@app.task -def cancel_pending_action_task(data): - - if data["action_type"] == "schedreboot" and data["status"] == "pending": - - from agents.models import Agent - - agent = Agent.objects.get(pk=data["agent"]) - - task_name = data["details"]["taskname"] - r = agent.salt_api_cmd( - timeout=30, func="task.delete_task", arg=[f"name={task_name}"] - ) - if r == "timeout" or r == "error" or (isinstance(r, bool) and not r): - logger.error( - f"Unable to contact {agent.hostname}. Task {task_name} will need to cancelled manually." - ) - return - else: - logger.info(f"Scheduled reboot cancelled on {agent.hostname}") - - return "ok" diff --git a/api/tacticalrmm/logs/tests.py b/api/tacticalrmm/logs/tests.py index c6d23b93f0..413d18a966 100644 --- a/api/tacticalrmm/logs/tests.py +++ b/api/tacticalrmm/logs/tests.py @@ -190,54 +190,31 @@ def test_all_pending_actions(self): self.check_not_authenticated("get", url) - @patch("logs.tasks.cancel_pending_action_task.delay") - def test_cancel_pending_action(self, mock_task): + @patch("agents.models.Agent.nats_cmd") + def test_cancel_pending_action(self, nats_cmd): url = "/logs/cancelpendingaction/" - pending_action = baker.make("logs.PendingAction") + # TODO fix this TypeError: Object of type coroutine is not JSON serializable + """ agent = baker.make("agents.Agent", version="1.1.1") + pending_action = baker.make( + "logs.PendingAction", + agent=agent, + details={ + "time": "2021-01-13 18:20:00", + "taskname": "TacticalRMM_SchedReboot_wYzCCDVXlc", + }, + ) - serializer = PendingActionSerializer(pending_action).data data = {"pk": pending_action.id} resp = self.client.delete(url, data, format="json") self.assertEqual(resp.status_code, 200) - mock_task.assert_called_with(serializer) + nats_data = { + "func": "delschedtask", + "schedtaskpayload": {"name": "TacticalRMM_SchedReboot_wYzCCDVXlc"}, + } + nats_cmd.assert_called_with(nats_data, timeout=10) # try request again and it should fail since pending action doesn't exist resp = self.client.delete(url, data, format="json") - self.assertEqual(resp.status_code, 404) + self.assertEqual(resp.status_code, 404) """ self.check_not_authenticated("delete", url) - - -class TestLogsTasks(TacticalTestCase): - def setUp(self): - self.authenticate() - - @patch("agents.models.Agent.salt_api_cmd") - def test_cancel_pending_action_task(self, mock_salt_cmd): - from .tasks import cancel_pending_action_task - - pending_action = baker.make( - "logs.PendingAction", - action_type="schedreboot", - status="pending", - details={"taskname": "test_name"}, - ) - - # data that is passed to the task - data = PendingActionSerializer(pending_action).data - - # set return value on mock to success - mock_salt_cmd.return_value = "success" - # call task with valid data and see if salt is called with correct data - ret = cancel_pending_action_task(data) - mock_salt_cmd.assert_called_with( - timeout=30, func="task.delete_task", arg=["name=test_name"] - ) - # this should return successful - self.assertEquals(ret, "ok") - - # this run should return false - mock_salt_cmd.reset_mock() - mock_salt_cmd.return_value = "timeout" - ret = cancel_pending_action_task(data) - self.assertEquals(ret, None) diff --git a/api/tacticalrmm/logs/views.py b/api/tacticalrmm/logs/views.py index 8db9cebc06..397cef5251 100644 --- a/api/tacticalrmm/logs/views.py +++ b/api/tacticalrmm/logs/views.py @@ -1,3 +1,4 @@ +import asyncio import subprocess from django.conf import settings @@ -18,7 +19,7 @@ from .serializers import PendingActionSerializer, AuditLogSerializer from agents.serializers import AgentHostnameSerializer from accounts.serializers import UserSerializer -from .tasks import cancel_pending_action_task +from tacticalrmm.utils import notify_error class GetAuditLogs(APIView): @@ -102,12 +103,19 @@ def all_pending_actions(request): @api_view(["DELETE"]) def cancel_pending_action(request): action = get_object_or_404(PendingAction, pk=request.data["pk"]) - data = PendingActionSerializer(action).data - cancel_pending_action_task.delay(data) + if not action.agent.has_gotasks: + return notify_error("Requires agent version 1.1.1 or greater") + + nats_data = { + "func": "delschedtask", + "schedtaskpayload": {"name": action.details["taskname"]}, + } + r = asyncio.run(action.agent.nats_cmd(nats_data, timeout=10)) + if r != "ok": + return notify_error(r) + action.delete() - return Response( - f"{action.agent.hostname}: {action.description} will be cancelled shortly" - ) + return Response(f"{action.agent.hostname}: {action.description} was cancelled") @api_view() diff --git a/api/tacticalrmm/winupdate/tests.py b/api/tacticalrmm/winupdate/tests.py index 02869b247b..f235003ebb 100644 --- a/api/tacticalrmm/winupdate/tests.py +++ b/api/tacticalrmm/winupdate/tests.py @@ -175,7 +175,7 @@ def test_check_agent_update_daily_schedule(self, agent_salt_cmd): agent_salt_cmd.assert_called_with(func="win_agent.install_updates") self.assertEquals(agent_salt_cmd.call_count, 2) - @patch("agents.models.Agent.salt_api_async") + """ @patch("agents.models.Agent.salt_api_async") def test_check_agent_update_monthly_schedule(self, agent_salt_cmd): from .tasks import check_agent_update_schedule_task @@ -204,7 +204,7 @@ def test_check_agent_update_monthly_schedule(self, agent_salt_cmd): check_agent_update_schedule_task() agent_salt_cmd.assert_called_with(func="win_agent.install_updates") - self.assertEquals(agent_salt_cmd.call_count, 2) + self.assertEquals(agent_salt_cmd.call_count, 2) """ @patch("agents.models.Agent.salt_api_cmd") def test_check_for_updates(self, salt_api_cmd): diff --git a/web/src/components/AgentTable.vue b/web/src/components/AgentTable.vue index 5ae5bbdf6a..4e4bc9a72e 100644 --- a/web/src/components/AgentTable.vue +++ b/web/src/components/AgentTable.vue @@ -555,13 +555,17 @@ export default { persistent: true, }) .onOk(() => { - const data = { pk: pk, action: "rebootnow" }; - axios.post("/agents/poweraction/", data).then(r => { - this.$q.dialog({ - title: `Restarting ${hostname}`, - message: `${hostname} will now be restarted`, + this.$q.loading.show(); + this.$axios + .post("/agents/reboot/", { pk: pk }) + .then(r => { + this.$q.loading.hide(); + this.notifySuccess(`${hostname} will now be restarted`); + }) + .catch(e => { + this.$q.loading.hide(); + this.notifyError(e.response.data); }); - }); }); }, agentRowSelected(pk) { diff --git a/web/src/components/modals/agents/RebootLater.vue b/web/src/components/modals/agents/RebootLater.vue index 9ebe48cfb2..d6fa6a939f 100644 --- a/web/src/components/modals/agents/RebootLater.vue +++ b/web/src/components/modals/agents/RebootLater.vue @@ -33,7 +33,6 @@ \ No newline at end of file From 4bf2dc9ecef6f66e2dab786886f3c26dec131e9c Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Tue, 1 Dec 2020 04:44:38 +0000 Subject: [PATCH 20/45] don't create unnecessary outage records --- api/tacticalrmm/agents/tasks.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/api/tacticalrmm/agents/tasks.py b/api/tacticalrmm/agents/tasks.py index 14fda380d3..4399288fa8 100644 --- a/api/tacticalrmm/agents/tasks.py +++ b/api/tacticalrmm/agents/tasks.py @@ -331,19 +331,22 @@ def agent_recovery_sms_task(pk): @app.task def agent_outages_task(): - agents = Agent.objects.only("pk") + agents = Agent.objects.only( + "pk", "last_seen", "overdue_time", "overdue_email_alert", "overdue_text_alert" + ) for agent in agents: - if agent.status == "overdue": - outages = AgentOutage.objects.filter(agent=agent) - if outages and outages.last().is_active: - continue + if agent.overdue_email_alert or agent.overdue_text_alert: + if agent.status == "overdue": + outages = AgentOutage.objects.filter(agent=agent) + if outages and outages.last().is_active: + continue - outage = AgentOutage(agent=agent) - outage.save() + outage = AgentOutage(agent=agent) + outage.save() - if agent.overdue_email_alert and not agent.maintenance_mode: - agent_outage_email_task.delay(pk=outage.pk) + if agent.overdue_email_alert and not agent.maintenance_mode: + agent_outage_email_task.delay(pk=outage.pk) - if agent.overdue_text_alert and not agent.maintenance_mode: - agent_outage_sms_task.delay(pk=outage.pk) + if agent.overdue_text_alert and not agent.maintenance_mode: + agent_outage_sms_task.delay(pk=outage.pk) From 948b56d0e618dd8f3f5957ef751d1af9ed888898 Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Tue, 1 Dec 2020 04:47:09 +0000 Subject: [PATCH 21/45] add a ghetto check for non standard cert --- api/tacticalrmm/tacticalrmm/utils.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/api/tacticalrmm/tacticalrmm/utils.py b/api/tacticalrmm/tacticalrmm/utils.py index fe5e8ee43b..288f59934d 100644 --- a/api/tacticalrmm/tacticalrmm/utils.py +++ b/api/tacticalrmm/tacticalrmm/utils.py @@ -57,6 +57,7 @@ def bitdays_to_string(day: int) -> str: return ", ".join(ret) + def filter_software(sw: SoftwareList) -> SoftwareList: ret: SoftwareList = [] printable = set(string.printable) @@ -73,9 +74,10 @@ def filter_software(sw: SoftwareList) -> SoftwareList: "uninstall": s["uninstall"], } ) - + return ret + def reload_nats(): users = [{"user": "tacticalrmm", "password": settings.SECRET_KEY}] agents = Agent.objects.prefetch_related("user").only("pk", "agent_id") @@ -91,14 +93,24 @@ def reload_nats(): if not settings.DOCKER_BUILD: domain = settings.ALLOWED_HOSTS[0].split(".", 1)[1] - cert_path = f"/etc/letsencrypt/live/{domain}" + if hasattr(settings, "CERT_FILE") and hasattr(settings, "KEY_FILE"): + if os.path.exists(settings.CERT_FILE) and os.path.exists(settings.KEY_FILE): + cert_file = settings.CERT_FILE + key_file = settings.KEY_FILE + else: + cert_file = f"/etc/letsencrypt/live/{domain}/fullchain.pem" + key_file = f"/etc/letsencrypt/live/{domain}/privkey.pem" + else: + cert_file = f"/etc/letsencrypt/live/{domain}/fullchain.pem" + key_file = f"/etc/letsencrypt/live/{domain}/privkey.pem" else: - cert_path = "/opt/tactical/certs" + cert_file = f"/opt/tactical/certs/fullchain.pem" + key_file = f"/opt/tactical/certs/privkey.pem" config = { "tls": { - "cert_file": f"{cert_path}/fullchain.pem", - "key_file": f"{cert_path}/privkey.pem", + "cert_file": cert_file, + "key_file": key_file, }, "authorization": {"users": users}, "max_payload": 2048576005, From 6e751e7a9b0bbad73391f23720c9e7bdfc38116d Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Tue, 1 Dec 2020 04:51:51 +0000 Subject: [PATCH 22/45] remove bg task that's handled by the agent now --- api/tacticalrmm/agents/tasks.py | 19 -------------- api/tacticalrmm/agents/tests.py | 38 --------------------------- api/tacticalrmm/tacticalrmm/celery.py | 4 --- 3 files changed, 61 deletions(-) diff --git a/api/tacticalrmm/agents/tasks.py b/api/tacticalrmm/agents/tasks.py index 4399288fa8..e232936359 100644 --- a/api/tacticalrmm/agents/tasks.py +++ b/api/tacticalrmm/agents/tasks.py @@ -209,25 +209,6 @@ def batch_sync_modules_task(): sleep(10) -@app.task -def batch_sysinfo_task(): - # update system info using WMI - agents = Agent.objects.all() - - agents_nats = [agent for agent in agents if agent.has_nats] - minions = [ - agent.salt_id - for agent in agents - if not agent.has_nats and pyver.parse(agent.version) >= pyver.parse("0.11.0") - ] - - if minions: - Agent.salt_batch_async(minions=minions, func="win_agent.local_sys_info") - - for agent in agents_nats: - asyncio.run(agent.nats_cmd({"func": "sysinfo"}, wait=False)) - - @app.task def uninstall_agent_task(salt_id, has_nats): attempts = 0 diff --git a/api/tacticalrmm/agents/tests.py b/api/tacticalrmm/agents/tests.py index d3e44fb963..ca3e7379dd 100644 --- a/api/tacticalrmm/agents/tests.py +++ b/api/tacticalrmm/agents/tests.py @@ -18,7 +18,6 @@ get_wmi_detail_task, sync_salt_modules_task, batch_sync_modules_task, - batch_sysinfo_task, OLD_64_PY_AGENT, OLD_32_PY_AGENT, ) @@ -801,43 +800,6 @@ def test_batch_sync_modules_task(self, mock_sleep, salt_batch_async): self.assertEqual(salt_batch_async.call_count, 4) self.assertEqual(ret.status, "SUCCESS") - @patch("agents.models.Agent.nats_cmd") - @patch("agents.models.Agent.salt_batch_async", return_value=None) - @patch("agents.tasks.sleep", return_value=None) - def test_batch_sysinfo_task(self, mock_sleep, salt_batch_async, nats_cmd): - - self.agents_nats = baker.make_recipe( - "agents.agent", version="1.1.0", _quantity=20 - ) - # test nats - ret = batch_sysinfo_task.s().apply() - self.assertEqual(nats_cmd.call_count, 20) - nats_cmd.assert_called_with({"func": "sysinfo"}, wait=False) - self.assertEqual(ret.status, "SUCCESS") - - self.agents_salt = baker.make_recipe( - "agents.agent", version="1.0.2", _quantity=70 - ) - - minions = [i.salt_id for i in self.agents_salt] - - ret = batch_sysinfo_task.s().apply() - self.assertEqual(salt_batch_async.call_count, 1) - salt_batch_async.assert_called_with( - minions=minions, func="win_agent.local_sys_info" - ) - self.assertEqual(ret.status, "SUCCESS") - salt_batch_async.reset_mock() - [i.delete() for i in self.agents_salt] - - # test old agents, should not run - self.agents_old = baker.make_recipe( - "agents.agent", version="0.10.2", _quantity=70 - ) - ret = batch_sysinfo_task.s().apply() - salt_batch_async.assert_not_called() - self.assertEqual(ret.status, "SUCCESS") - @patch("agents.models.Agent.salt_api_async", return_value=None) @patch("agents.tasks.sleep", return_value=None) def test_update_salt_minion_task(self, mock_sleep, salt_api_async): diff --git a/api/tacticalrmm/tacticalrmm/celery.py b/api/tacticalrmm/tacticalrmm/celery.py index 83c438a992..6253004fed 100644 --- a/api/tacticalrmm/tacticalrmm/celery.py +++ b/api/tacticalrmm/tacticalrmm/celery.py @@ -37,10 +37,6 @@ "task": "agents.tasks.batch_sync_modules_task", "schedule": crontab(minute=25, hour="*/4"), }, - "sys-info": { - "task": "agents.tasks.batch_sysinfo_task", - "schedule": crontab(minute=15, hour="*/2"), - }, "update-salt": { "task": "agents.tasks.update_salt_minion_task", "schedule": crontab(minute=20, hour="*/6"), From ba028cde0ce0c9e17656918ee2915e9eb238f6df Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Tue, 1 Dec 2020 05:00:13 +0000 Subject: [PATCH 23/45] remove old api app --- api/tacticalrmm/api/__init__.py | 0 api/tacticalrmm/api/apps.py | 5 - api/tacticalrmm/api/migrations/__init__.py | 0 api/tacticalrmm/api/urls.py | 11 -- api/tacticalrmm/api/views.py | 149 --------------------- api/tacticalrmm/apiv3/tests.py | 4 - api/tacticalrmm/tacticalrmm/settings.py | 1 - api/tacticalrmm/tacticalrmm/urls.py | 1 - 8 files changed, 171 deletions(-) delete mode 100644 api/tacticalrmm/api/__init__.py delete mode 100644 api/tacticalrmm/api/apps.py delete mode 100644 api/tacticalrmm/api/migrations/__init__.py delete mode 100644 api/tacticalrmm/api/urls.py delete mode 100644 api/tacticalrmm/api/views.py diff --git a/api/tacticalrmm/api/__init__.py b/api/tacticalrmm/api/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/api/tacticalrmm/api/apps.py b/api/tacticalrmm/api/apps.py deleted file mode 100644 index 14b89a8298..0000000000 --- a/api/tacticalrmm/api/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class ApiConfig(AppConfig): - name = "api" diff --git a/api/tacticalrmm/api/migrations/__init__.py b/api/tacticalrmm/api/migrations/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/api/tacticalrmm/api/urls.py b/api/tacticalrmm/api/urls.py deleted file mode 100644 index e804bc4ecb..0000000000 --- a/api/tacticalrmm/api/urls.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.urls import path -from . import views -from apiv3 import views as v3_views - -urlpatterns = [ - path("triggerpatchscan/", views.trigger_patch_scan), - path("/checkrunner/", views.CheckRunner.as_view()), - path("/taskrunner/", views.TaskRunner.as_view()), - path("/saltinfo/", views.SaltInfo.as_view()), - path("/meshinfo/", v3_views.MeshInfo.as_view()), -] diff --git a/api/tacticalrmm/api/views.py b/api/tacticalrmm/api/views.py deleted file mode 100644 index f2b687a189..0000000000 --- a/api/tacticalrmm/api/views.py +++ /dev/null @@ -1,149 +0,0 @@ -from loguru import logger - -from django.conf import settings -from django.shortcuts import get_object_or_404 -from django.utils import timezone as djangotime - -from rest_framework.response import Response -from rest_framework.views import APIView -from rest_framework.authentication import TokenAuthentication -from rest_framework.permissions import IsAuthenticated -from rest_framework.decorators import ( - api_view, - authentication_classes, - permission_classes, -) - -from agents.models import Agent -from checks.models import Check -from autotasks.models import AutomatedTask - -from winupdate.tasks import check_for_updates_task - -from autotasks.serializers import TaskRunnerGetSerializer, TaskRunnerPatchSerializer -from checks.serializers import CheckRunnerGetSerializer, CheckResultsSerializer - - -logger.configure(**settings.LOG_CONFIG) - - -@api_view(["PATCH"]) -@authentication_classes((TokenAuthentication,)) -@permission_classes((IsAuthenticated,)) -def trigger_patch_scan(request): - agent = get_object_or_404(Agent, agent_id=request.data["agent_id"]) - reboot_policy = agent.get_patch_policy().reboot_after_install - reboot = False - - if reboot_policy == "always": - reboot = True - - if request.data["reboot"]: - if reboot_policy == "required": - reboot = True - elif reboot_policy == "never": - agent.needs_reboot = True - agent.save(update_fields=["needs_reboot"]) - - if reboot: - r = agent.salt_api_cmd( - timeout=15, - func="system.reboot", - arg=7, - kwargs={"in_seconds": True}, - ) - - if r == "timeout" or r == "error" or (isinstance(r, bool) and not r): - check_for_updates_task.apply_async( - queue="wupdate", kwargs={"pk": agent.pk, "wait": False} - ) - else: - logger.info(f"{agent.hostname} is rebooting after updates were installed.") - else: - check_for_updates_task.apply_async( - queue="wupdate", kwargs={"pk": agent.pk, "wait": False} - ) - - return Response("ok") - - -class CheckRunner(APIView): - """ - For windows agent - """ - - authentication_classes = [TokenAuthentication] - permission_classes = [IsAuthenticated] - - def get(self, request, pk): - agent = get_object_or_404(Agent, pk=pk) - checks = Check.objects.filter(agent__pk=pk, overriden_by_policy=False) - - ret = { - "agent": agent.pk, - "check_interval": agent.check_interval, - "checks": CheckRunnerGetSerializer(checks, many=True).data, - } - return Response(ret) - - def patch(self, request, pk): - check = get_object_or_404(Check, pk=pk) - - if check.check_type != "cpuload" and check.check_type != "memory": - serializer = CheckResultsSerializer( - instance=check, data=request.data, partial=True - ) - serializer.is_valid(raise_exception=True) - serializer.save(last_run=djangotime.now()) - - else: - check.last_run = djangotime.now() - check.save(update_fields=["last_run"]) - - check.handle_check(request.data) - - return Response("ok") - - -class TaskRunner(APIView): - """ - For the windows python agent - """ - - authentication_classes = [TokenAuthentication] - permission_classes = [IsAuthenticated] - - def get(self, request, pk): - - task = get_object_or_404(AutomatedTask, pk=pk) - return Response(TaskRunnerGetSerializer(task).data) - - def patch(self, request, pk): - task = get_object_or_404(AutomatedTask, pk=pk) - - serializer = TaskRunnerPatchSerializer( - instance=task, data=request.data, partial=True - ) - serializer.is_valid(raise_exception=True) - serializer.save(last_run=djangotime.now()) - return Response("ok") - - -class SaltInfo(APIView): - authentication_classes = [TokenAuthentication] - permission_classes = [IsAuthenticated] - - def get(self, request, pk): - agent = get_object_or_404(Agent, pk=pk) - ret = { - "latestVer": settings.LATEST_SALT_VER, - "currentVer": agent.salt_ver, - "salt_id": agent.salt_id, - } - return Response(ret) - - def patch(self, request, pk): - agent = get_object_or_404(Agent, pk=pk) - agent.salt_ver = request.data["ver"] - agent.save(update_fields=["salt_ver"]) - return Response("ok") diff --git a/api/tacticalrmm/apiv3/tests.py b/api/tacticalrmm/apiv3/tests.py index 8d42e43a61..b5be518f8c 100644 --- a/api/tacticalrmm/apiv3/tests.py +++ b/api/tacticalrmm/apiv3/tests.py @@ -45,15 +45,11 @@ def test_get_salt_minion(self): def test_get_mesh_info(self): url = f"/api/v3/{self.agent.pk}/meshinfo/" - url2 = f"/api/v1/{self.agent.pk}/meshinfo/" r = self.client.get(url) self.assertEqual(r.status_code, 200) - r = self.client.get(url2) - self.assertEqual(r.status_code, 200) self.check_not_authenticated("get", url) - self.check_not_authenticated("get", url2) def test_get_winupdater(self): url = f"/api/v3/{self.agent.agent_id}/winupdater/" diff --git a/api/tacticalrmm/tacticalrmm/settings.py b/api/tacticalrmm/tacticalrmm/settings.py index 95300534bc..78847765d9 100644 --- a/api/tacticalrmm/tacticalrmm/settings.py +++ b/api/tacticalrmm/tacticalrmm/settings.py @@ -58,7 +58,6 @@ "knox", "corsheaders", "accounts", - "api", "apiv2", "apiv3", "clients", diff --git a/api/tacticalrmm/tacticalrmm/urls.py b/api/tacticalrmm/tacticalrmm/urls.py index 83e9e9beb8..82b6a46e03 100644 --- a/api/tacticalrmm/tacticalrmm/urls.py +++ b/api/tacticalrmm/tacticalrmm/urls.py @@ -10,7 +10,6 @@ path("login/", LoginView.as_view()), path("logout/", knox_views.LogoutView.as_view()), path("logoutall/", knox_views.LogoutAllView.as_view()), - path("api/v1/", include("api.urls")), path("api/v2/", include("apiv2.urls")), path("api/v3/", include("apiv3.urls")), path("clients/", include("clients.urls")), From 17ac92a9d08ea16265073ccc8d884bb49d747c98 Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Tue, 1 Dec 2020 05:16:37 +0000 Subject: [PATCH 24/45] remove dead code --- api/tacticalrmm/agents/models.py | 3 --- api/tacticalrmm/checks/views.py | 41 -------------------------------- 2 files changed, 44 deletions(-) diff --git a/api/tacticalrmm/agents/models.py b/api/tacticalrmm/agents/models.py index 6c1d972ea3..91dfe1e217 100644 --- a/api/tacticalrmm/agents/models.py +++ b/api/tacticalrmm/agents/models.py @@ -574,9 +574,6 @@ def salt_batch_async(**kwargs): return resp - def not_supported(self, version_added): - return pyver.parse(self.version) < pyver.parse(version_added) - def delete_superseded_updates(self): try: pks = [] # list of pks to delete diff --git a/api/tacticalrmm/checks/views.py b/api/tacticalrmm/checks/views.py index aa4950ce23..a0e22a65bb 100644 --- a/api/tacticalrmm/checks/views.py +++ b/api/tacticalrmm/checks/views.py @@ -36,17 +36,6 @@ def post(self, request): else: agent = get_object_or_404(Agent, pk=request.data["pk"]) parent = {"agent": agent} - added = "0.11.0" - if ( - request.data["check"]["check_type"] == "script" - and request.data["check"]["script_args"] - and agent.not_supported(version_added=added) - ): - return notify_error( - { - "non_field_errors": f"Script arguments only available in agent {added} or greater" - } - ) script = None if "script" in request.data["check"]: @@ -58,13 +47,6 @@ def post(self, request): request.data["check"]["check_type"] == "eventlog" and request.data["check"]["event_id_is_wildcard"] ): - if agent and agent.not_supported(version_added="0.10.2"): - return notify_error( - { - "non_field_errors": "Wildcard is only available in agent 0.10.2 or greater" - } - ) - request.data["check"]["event_id"] = 0 serializer = CheckSerializer( @@ -116,31 +98,8 @@ def patch(self, request, pk): pass else: if request.data["event_id_is_wildcard"]: - if check.agent.not_supported(version_added="0.10.2"): - return notify_error( - { - "non_field_errors": "Wildcard is only available in agent 0.10.2 or greater" - } - ) - request.data["event_id"] = 0 - elif check.check_type == "script": - added = "0.11.0" - try: - request.data["script_args"] - except KeyError: - pass - else: - if request.data["script_args"] and check.agent.not_supported( - version_added=added - ): - return notify_error( - { - "non_field_errors": f"Script arguments only available in agent {added} or greater" - } - ) - serializer = CheckSerializer(instance=check, data=request.data, partial=True) serializer.is_valid(raise_exception=True) obj = serializer.save() From 502bd2a191ae4f08d55278ab4303976778fbe560 Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Tue, 1 Dec 2020 05:16:47 +0000 Subject: [PATCH 25/45] patch nats --- api/tacticalrmm/automation/tests.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/tacticalrmm/automation/tests.py b/api/tacticalrmm/automation/tests.py index 2ea4127587..09daa97ecb 100644 --- a/api/tacticalrmm/automation/tests.py +++ b/api/tacticalrmm/automation/tests.py @@ -1051,7 +1051,8 @@ def test_run_policy_task(self, run_win_task): for task in tasks: run_win_task.assert_any_call(task.id) - def test_update_policy_tasks(self): + @patch("agents.models.Agent.nats_cmd") + def test_update_policy_tasks(self, nats_cmd): from .tasks import update_policy_task_fields_task from autotasks.models import AutomatedTask From 95dce9e992f9842088b2266d6e18724e8ee68a86 Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Tue, 1 Dec 2020 05:52:32 +0000 Subject: [PATCH 26/45] check for supported agent --- api/tacticalrmm/autotasks/views.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api/tacticalrmm/autotasks/views.py b/api/tacticalrmm/autotasks/views.py index 29d28f3f2f..0e94d00593 100644 --- a/api/tacticalrmm/autotasks/views.py +++ b/api/tacticalrmm/autotasks/views.py @@ -38,6 +38,9 @@ def post(self, request): parent = {"policy": policy} else: agent = get_object_or_404(Agent, pk=data["agent"]) + if not agent.has_gotasks: + return notify_error("Requires agent version 1.1.1 or greater") + parent = {"agent": agent} check = None From eeed81392f103bb864ea6903a77e4247bb6c7c40 Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Tue, 1 Dec 2020 05:55:27 +0000 Subject: [PATCH 27/45] add rm orphaned tasks to maintenance tab --- api/tacticalrmm/core/views.py | 13 +++++++++++++ .../components/modals/core/ServerMaintenance.vue | 4 ++++ 2 files changed, 17 insertions(+) diff --git a/api/tacticalrmm/core/views.py b/api/tacticalrmm/core/views.py index 88b0632cf5..52c8462f49 100644 --- a/api/tacticalrmm/core/views.py +++ b/api/tacticalrmm/core/views.py @@ -97,6 +97,19 @@ def server_maintenance(request): reload_nats() return Response("Reloading Nats Configuration.") + if request.data["action"] == "rm_orphaned_tasks": + from agents.models import Agent + from autotasks.tasks import remove_orphaned_win_tasks + + agents = Agent.objects.all() + online = [i for i in agents if i.status == "online"] + for agent in online: + remove_orphaned_win_tasks.delay(agent.pk) + + return Response( + "The task has been initiated. Check the Debug Log in the UI for progress." + ) + if request.data["action"] == "prune_db": from agents.models import AgentOutage from logs.models import AuditLog, PendingAction diff --git a/web/src/components/modals/core/ServerMaintenance.vue b/web/src/components/modals/core/ServerMaintenance.vue index 30deeb4f6c..798cdef3fc 100644 --- a/web/src/components/modals/core/ServerMaintenance.vue +++ b/web/src/components/modals/core/ServerMaintenance.vue @@ -61,6 +61,10 @@ export default { label: "Reload Nats Configuration", value: "reload_nats", }, + { + label: "Remove Orphaned Tasks", + value: "rm_orphaned_tasks", + }, { label: "Prune DB Tables", value: "prune_db", From faeec00b3936ea67bf4aa69f40e3a2f2c309e1e8 Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Tue, 1 Dec 2020 06:16:09 +0000 Subject: [PATCH 28/45] remove more tasks now handled by the agent --- api/tacticalrmm/agents/tasks.py | 11 ------ api/tacticalrmm/agents/tests.py | 14 -------- api/tacticalrmm/apiv3/views.py | 5 +-- api/tacticalrmm/software/tasks.py | 26 -------------- api/tacticalrmm/software/tests.py | 56 +------------------------------ 5 files changed, 2 insertions(+), 110 deletions(-) diff --git a/api/tacticalrmm/agents/tasks.py b/api/tacticalrmm/agents/tasks.py index e232936359..cbb59a7d46 100644 --- a/api/tacticalrmm/agents/tasks.py +++ b/api/tacticalrmm/agents/tasks.py @@ -175,17 +175,6 @@ def update_salt_minion_task(): sleep(20) -@app.task -def get_wmi_detail_task(pk): - agent = Agent.objects.get(pk=pk) - if agent.has_nats: - asyncio.run(agent.nats_cmd({"func": "sysinfo"}, wait=False)) - else: - agent.salt_api_async(timeout=30, func="win_agent.local_sys_info") - - return "ok" - - @app.task def sync_salt_modules_task(pk): agent = Agent.objects.get(pk=pk) diff --git a/api/tacticalrmm/agents/tests.py b/api/tacticalrmm/agents/tests.py index ca3e7379dd..94871d6c92 100644 --- a/api/tacticalrmm/agents/tests.py +++ b/api/tacticalrmm/agents/tests.py @@ -15,7 +15,6 @@ from .tasks import ( auto_self_agent_update_task, update_salt_minion_task, - get_wmi_detail_task, sync_salt_modules_task, batch_sync_modules_task, OLD_64_PY_AGENT, @@ -752,19 +751,6 @@ def setUp(self): self.authenticate() self.setup_coresettings() - @patch("agents.models.Agent.nats_cmd") - @patch("agents.models.Agent.salt_api_async", return_value=None) - def test_get_wmi_detail_task(self, salt_api_async, nats_cmd): - self.agent_salt = baker.make_recipe("agents.agent", version="1.0.2") - ret = get_wmi_detail_task.s(self.agent_salt.pk).apply() - salt_api_async.assert_called_with(timeout=30, func="win_agent.local_sys_info") - self.assertEqual(ret.status, "SUCCESS") - - self.agent_nats = baker.make_recipe("agents.agent", version="1.1.0") - ret = get_wmi_detail_task.s(self.agent_nats.pk).apply() - nats_cmd.assert_called_with({"func": "sysinfo"}, wait=False) - self.assertEqual(ret.status, "SUCCESS") - @patch("agents.models.Agent.salt_api_cmd") def test_sync_salt_modules_task(self, salt_api_cmd): self.agent = baker.make_recipe("agents.agent") diff --git a/api/tacticalrmm/apiv3/views.py b/api/tacticalrmm/apiv3/views.py index 38664e73c0..e870ad1b07 100644 --- a/api/tacticalrmm/apiv3/views.py +++ b/api/tacticalrmm/apiv3/views.py @@ -28,11 +28,10 @@ from agents.tasks import ( agent_recovery_email_task, agent_recovery_sms_task, - get_wmi_detail_task, sync_salt_modules_task, ) from winupdate.tasks import check_for_updates_task -from software.tasks import get_installed_software, install_chocolatey +from software.tasks import install_chocolatey from checks.utils import bytes2human from tacticalrmm.utils import notify_error, reload_nats, filter_software, SoftwareList @@ -123,8 +122,6 @@ def post(self, request): serializer.save(last_seen=djangotime.now()) sync_salt_modules_task.delay(agent.pk) - get_installed_software.delay(agent.pk) - get_wmi_detail_task.delay(agent.pk) check_for_updates_task.apply_async( queue="wupdate", kwargs={"pk": agent.pk, "wait": True} ) diff --git a/api/tacticalrmm/software/tasks.py b/api/tacticalrmm/software/tasks.py index e9052f1f54..021148390a 100644 --- a/api/tacticalrmm/software/tasks.py +++ b/api/tacticalrmm/software/tasks.py @@ -87,30 +87,6 @@ def update_chocos(): return "ok" -@app.task -def get_installed_software(pk): - agent = Agent.objects.get(pk=pk) - if not agent.has_nats: - logger.error(f"{agent.salt_id} software list only available in agent >= 1.1.0") - return - - r = asyncio.run(agent.nats_cmd({"func": "softwarelist"}, timeout=20)) - if r == "timeout" or r == "natsdown": - logger.error(f"{agent.salt_id} {r}") - return - - sw = filter_software(r) - - if not InstalledSoftware.objects.filter(agent=agent).exists(): - InstalledSoftware(agent=agent, software=sw).save() - else: - s = agent.installedsoftware_set.first() - s.software = sw - s.save(update_fields=["software"]) - - return "ok" - - @app.task def install_program(pk, name, version): agent = Agent.objects.get(pk=pk) @@ -155,6 +131,4 @@ def install_program(pk, name, version): agent=agent, name=name, version=version, message=output, installed=installed ).save() - get_installed_software.delay(agent.pk) - return "ok" diff --git a/api/tacticalrmm/software/tests.py b/api/tacticalrmm/software/tests.py index 04da686e51..eb2738388b 100644 --- a/api/tacticalrmm/software/tests.py +++ b/api/tacticalrmm/software/tests.py @@ -120,61 +120,8 @@ def test_update_chocos(self, salt_api_cmd): salt_api_cmd.assert_any_call(timeout=200, func="chocolatey.list") self.assertEquals(salt_api_cmd.call_count, 2) - @patch("agents.models.Agent.nats_cmd") - def test_get_installed_software(self, nats_cmd): - from .tasks import get_installed_software - - agent = baker.make_recipe("agents.agent") - - nats_return = [ - { - "name": "Mozilla Maintenance Service", - "size": "336.9 kB", - "source": "", - "version": "73.0.1", - "location": "", - "publisher": "Mozilla", - "uninstall": '"C:\\Program Files (x86)\\Mozilla Maintenance Service\\uninstall.exe"', - "install_date": "0001-01-01 00:00:00 +0000 UTC", - }, - { - "name": "OpenVPN 2.4.9-I601-Win10 ", - "size": "8.7 MB", - "source": "", - "version": "2.4.9-I601-Win10", - "location": "C:\\Program Files\\OpenVPN\\", - "publisher": "OpenVPN Technologies, Inc.", - "uninstall": "C:\\Program Files\\OpenVPN\\Uninstall.exe", - "install_date": "0001-01-01 00:00:00 +0000 UTC", - }, - { - "name": "Microsoft Office Professional Plus 2019 - en-us", - "size": "0 B", - "source": "", - "version": "16.0.10368.20035", - "location": "C:\\Program Files\\Microsoft Office", - "publisher": "Microsoft Corporation", - "uninstall": '"C:\\Program Files\\Common Files\\Microsoft Shared\\ClickToRun\\OfficeClickToRun.exe" scenario=install scenariosubtype=ARP sourcetype=None productstoremove=ProPlus2019Volume.16_en-us_x-none culture=en-us version.16=16.0', - "install_date": "0001-01-01 00:00:00 +0000 UTC", - }, - ] - - # test failed attempt - nats_cmd.return_value = "timeout" - ret = get_installed_software(agent.pk) - self.assertFalse(ret) - nats_cmd.assert_called_with({"func": "softwarelist"}, timeout=20) - nats_cmd.reset_mock() - - # test successful attempt - nats_cmd.return_value = nats_return - ret = get_installed_software(agent.pk) - self.assertTrue(ret) - nats_cmd.assert_called_with({"func": "softwarelist"}, timeout=20) - @patch("agents.models.Agent.salt_api_cmd") - @patch("software.tasks.get_installed_software.delay") - def test_install_program(self, get_installed_software, salt_api_cmd): + def test_install_program(self, salt_api_cmd): from .tasks import install_program agent = baker.make_recipe("agents.agent") @@ -195,6 +142,5 @@ def test_install_program(self, get_installed_software, salt_api_cmd): salt_api_cmd.assert_called_with( timeout=900, func="chocolatey.install", arg=["git", "version=2.3.4"] ) - get_installed_software.assert_called_with(agent.pk) self.assertTrue(ChocoLog.objects.filter(agent=agent, name="git").exists()) From 3e7dcb2755fa489924762faf522efb3dee717a28 Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Tue, 1 Dec 2020 06:27:34 +0000 Subject: [PATCH 29/45] don't hide refresh when sw list empty --- web/src/components/SoftwareTab.vue | 1 - 1 file changed, 1 deletion(-) diff --git a/web/src/components/SoftwareTab.vue b/web/src/components/SoftwareTab.vue index cadc088430..505fc7156a 100644 --- a/web/src/components/SoftwareTab.vue +++ b/web/src/components/SoftwareTab.vue @@ -1,6 +1,5 @@