add software installation via chocolatey
This commit is contained in:
parent
b7575e31e0
commit
7caeaf03c0
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 3.0.2 on 2020-01-21 15:00
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('agents', '0016_remove_agent_status'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='agent',
|
||||
name='choco_installed',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
|
@ -49,6 +49,7 @@ class Agent(models.Model):
|
|||
needs_reboot = models.BooleanField(default=False)
|
||||
managed_by_wsus = models.BooleanField(default=False)
|
||||
is_updating = models.BooleanField(default=False)
|
||||
choco_installed = models.BooleanField(default=False)
|
||||
|
||||
def __str__(self):
|
||||
return self.hostname
|
||||
|
|
|
@ -41,6 +41,7 @@ from winupdate.models import WinUpdate, WinUpdatePolicy
|
|||
from agents.tasks import uninstall_agent_task, sync_salt_modules_task
|
||||
from winupdate.tasks import check_for_updates_task
|
||||
from agents.serializers import AgentHostnameSerializer
|
||||
from software.tasks import install_chocolatey, get_installed_software
|
||||
|
||||
logger.configure(**settings.LOG_CONFIG)
|
||||
|
||||
|
@ -68,7 +69,7 @@ class UploadMeshAgent(APIView):
|
|||
@permission_classes((IsAuthenticated,))
|
||||
def trigger_patch_scan(request):
|
||||
agent = get_object_or_404(Agent, agent_id=request.data["agentid"])
|
||||
check_for_updates_task.delay(agent.pk)
|
||||
check_for_updates_task.delay(agent.pk, wait=False)
|
||||
|
||||
if request.data["reboot"]:
|
||||
agent.needs_reboot = True
|
||||
|
@ -339,10 +340,14 @@ def update(request):
|
|||
)
|
||||
|
||||
sync_salt_modules_task.delay(agent.pk)
|
||||
get_installed_software.delay(agent.pk)
|
||||
|
||||
if not agent.choco_installed:
|
||||
install_chocolatey.delay(agent.pk, wait=True)
|
||||
|
||||
# check for updates if this is fresh agent install
|
||||
if not WinUpdate.objects.filter(agent=agent).exists():
|
||||
check_for_updates_task.delay(agent.pk)
|
||||
check_for_updates_task.delay(agent.pk, wait=True)
|
||||
|
||||
return Response("ok")
|
||||
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
from django.contrib import admin
|
||||
from .models import ChocoSoftware, ChocoLog, InstalledSoftware
|
||||
|
||||
|
||||
class ChocoAdmin(admin.ModelAdmin):
|
||||
readonly_fields = ("added",)
|
||||
|
||||
|
||||
class ChocoLogAdmin(admin.ModelAdmin):
|
||||
readonly_fields = ("time",)
|
||||
|
||||
|
||||
admin.site.register(ChocoSoftware, ChocoAdmin)
|
||||
admin.site.register(ChocoLog, ChocoLogAdmin)
|
||||
admin.site.register(InstalledSoftware)
|
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class SoftwareConfig(AppConfig):
|
||||
name = "software"
|
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 3.0.2 on 2020-01-10 00:20
|
||||
|
||||
import django.contrib.postgres.fields.jsonb
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ChocoSoftware',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('chocos', django.contrib.postgres.fields.jsonb.JSONField()),
|
||||
('added', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -0,0 +1,26 @@
|
|||
# Generated by Django 3.0.2 on 2020-02-02 01:31
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('agents', '0017_agent_choco_installed'),
|
||||
('software', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ChocoLog',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('version', models.CharField(max_length=255)),
|
||||
('message', models.TextField()),
|
||||
('time', models.DateTimeField(auto_now_add=True)),
|
||||
('agent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='chocolog', to='agents.Agent')),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -0,0 +1,29 @@
|
|||
# Generated by Django 3.0.2 on 2020-02-02 04:27
|
||||
|
||||
import django.contrib.postgres.fields.jsonb
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('agents', '0017_agent_choco_installed'),
|
||||
('software', '0002_chocolog'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='chocolog',
|
||||
name='installed',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='InstalledSoftware',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('software', django.contrib.postgres.fields.jsonb.JSONField()),
|
||||
('agent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='agents.Agent')),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -0,0 +1,41 @@
|
|||
from django.db import models
|
||||
from django.contrib.postgres.fields import JSONField
|
||||
|
||||
from agents.models import Agent
|
||||
|
||||
|
||||
class ChocoSoftware(models.Model):
|
||||
chocos = JSONField()
|
||||
added = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
@classmethod
|
||||
def sort_by_highest(cls):
|
||||
from .serializers import ChocoSoftwareSerializer
|
||||
|
||||
chocos = cls.objects.all()
|
||||
sizes = [
|
||||
{"size": len(ChocoSoftwareSerializer(i).data["chocos"]), "pk": i.pk}
|
||||
for i in chocos
|
||||
]
|
||||
biggest = max(range(len(sizes)), key=lambda index: sizes[index]["size"])
|
||||
return int(sizes[biggest]["pk"])
|
||||
|
||||
|
||||
class ChocoLog(models.Model):
|
||||
agent = models.ForeignKey(Agent, related_name="chocolog", on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=255)
|
||||
version = models.CharField(max_length=255)
|
||||
message = models.TextField()
|
||||
installed = models.BooleanField(default=False)
|
||||
time = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.agent.hostname} | {self.name} | {self.time}"
|
||||
|
||||
|
||||
class InstalledSoftware(models.Model):
|
||||
agent = models.ForeignKey(Agent, on_delete=models.CASCADE)
|
||||
software = JSONField()
|
||||
|
||||
def __str__(self):
|
||||
return self.agent.hostname
|
|
@ -0,0 +1,15 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from .models import ChocoSoftware, InstalledSoftware
|
||||
|
||||
|
||||
class ChocoSoftwareSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ChocoSoftware
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class InstalledSoftwareSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = InstalledSoftware
|
||||
fields = "__all__"
|
|
@ -0,0 +1,173 @@
|
|||
from time import sleep
|
||||
from loguru import logger
|
||||
from tacticalrmm.celery import app
|
||||
from django.conf import settings
|
||||
|
||||
from agents.models import Agent
|
||||
from .models import ChocoSoftware, ChocoLog, InstalledSoftware
|
||||
|
||||
logger.configure(**settings.LOG_CONFIG)
|
||||
|
||||
|
||||
@app.task()
|
||||
def install_chocolatey(pk, wait=False):
|
||||
if wait:
|
||||
sleep(10)
|
||||
agent = Agent.objects.get(pk=pk)
|
||||
r = agent.salt_api_cmd(
|
||||
hostname=agent.salt_id,
|
||||
timeout=300,
|
||||
func="chocolatey.bootstrap",
|
||||
arg="force=True",
|
||||
)
|
||||
try:
|
||||
r.json()
|
||||
except Exception as e:
|
||||
return f"error installing choco on {agent.salt_id}"
|
||||
|
||||
if type(r.json()) is dict:
|
||||
try:
|
||||
output = r.json()["return"][0][agent.salt_id].lower()
|
||||
except Exception:
|
||||
return f"error installing choco on {agent.salt_id}"
|
||||
|
||||
success = ["chocolatey", "is", "now", "ready"]
|
||||
|
||||
if all(x in output for x in success):
|
||||
agent.choco_installed = True
|
||||
agent.save(update_fields=["choco_installed"])
|
||||
logger.info(f"Installed chocolatey on {agent.salt_id}")
|
||||
return f"Installed choco on {agent.salt_id}"
|
||||
else:
|
||||
return f"error installing choco on {agent.salt_id}"
|
||||
|
||||
|
||||
@app.task
|
||||
def update_chocos():
|
||||
agents = Agent.objects.only("pk")
|
||||
online = [x for x in agents if x.status == "online" and x.choco_installed]
|
||||
|
||||
while 1:
|
||||
for agent in online:
|
||||
try:
|
||||
ret = agent.salt_api_cmd(
|
||||
hostname=agent.salt_id, timeout=15, func="test.ping"
|
||||
)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
try:
|
||||
data = ret.json()["return"][0][agent.salt_id]
|
||||
except Exception:
|
||||
continue
|
||||
else:
|
||||
if data:
|
||||
install = agent.salt_api_cmd(
|
||||
hostname=agent.salt_id,
|
||||
timeout=180,
|
||||
func="chocolatey.bootstrap",
|
||||
arg="force=True",
|
||||
)
|
||||
resp = agent.salt_api_cmd(
|
||||
hostname=agent.salt_id, timeout=200, func="chocolatey.list"
|
||||
)
|
||||
ret = resp.json()["return"][0][agent.salt_id]
|
||||
|
||||
try:
|
||||
chocos = [{"name": k, "version": v[0]} for k, v in ret.items()]
|
||||
except AttributeError:
|
||||
continue
|
||||
else:
|
||||
# somtimes chocolatey api is down or buggy and doesn't return the full list of software
|
||||
if len(chocos) < 4000:
|
||||
continue
|
||||
else:
|
||||
logger.info(
|
||||
f"Chocos were updated using agent {agent.salt_id}"
|
||||
)
|
||||
ChocoSoftware(chocos=chocos).save()
|
||||
break
|
||||
|
||||
break
|
||||
|
||||
return "ok"
|
||||
|
||||
|
||||
@app.task
|
||||
def get_installed_software(pk):
|
||||
agent = Agent.objects.get(pk=pk)
|
||||
r = agent.salt_api_cmd(hostname=agent.salt_id, timeout=30, func="pkg.list_pkgs")
|
||||
try:
|
||||
output = r.json()["return"][0][agent.salt_id]
|
||||
except Exception:
|
||||
logger.error(f"Unable to get installed software on {agent.salt_id}")
|
||||
return "error"
|
||||
|
||||
try:
|
||||
software = [{"name": k, "version": v} for k, v in output.items()]
|
||||
except Exception:
|
||||
logger.error(f"Unable to get installed software on {agent.salt_id}")
|
||||
return "error"
|
||||
|
||||
if not InstalledSoftware.objects.filter(agent=agent).exists():
|
||||
InstalledSoftware(agent=agent, software=software).save()
|
||||
else:
|
||||
current = InstalledSoftware.objects.filter(agent=agent).get()
|
||||
current.software = software
|
||||
current.save(update_fields=["software"])
|
||||
|
||||
return "ok"
|
||||
|
||||
|
||||
@app.task
|
||||
def install_program(pk, name, version):
|
||||
agent = Agent.objects.get(pk=pk)
|
||||
|
||||
r = agent.salt_api_cmd(
|
||||
hostname=agent.salt_id,
|
||||
timeout=1000,
|
||||
func="chocolatey.install",
|
||||
arg=[name, f"version={version}"],
|
||||
)
|
||||
try:
|
||||
r.json()
|
||||
except Exception as e:
|
||||
logger.error(f"Error installing {name} {version} on {agent.salt_id}: {e}")
|
||||
return "error"
|
||||
|
||||
if type(r.json()) is dict:
|
||||
try:
|
||||
output = r.json()["return"][0][agent.salt_id].lower()
|
||||
except Exception as e:
|
||||
logger.error(f"Error installing {name} {version} on {agent.salt_id}: {e}")
|
||||
return "error"
|
||||
|
||||
success = [
|
||||
"install",
|
||||
"of",
|
||||
name.lower(),
|
||||
"was",
|
||||
"successful",
|
||||
"has",
|
||||
"been",
|
||||
"installed",
|
||||
]
|
||||
duplicate = [name.lower(), "already", "installed", "--force", "reinstall"]
|
||||
|
||||
installed = False
|
||||
|
||||
if all(x in output for x in success):
|
||||
installed = True
|
||||
logger.info(f"Successfully installed {name} {version} on {agent.salt_id}")
|
||||
elif all(x in output for x in duplicate):
|
||||
logger.warning(f"Already installed: {name} {version} on {agent.salt_id}")
|
||||
else:
|
||||
logger.error(f"Something went wrong - {name} {version} on {agent.salt_id}")
|
||||
|
||||
ChocoLog(
|
||||
agent=agent, name=name, version=version, message=output, installed=installed
|
||||
).save()
|
||||
|
||||
get_installed_software.delay(agent.pk)
|
||||
|
||||
return "ok"
|
|
@ -0,0 +1 @@
|
|||
from django.test import TestCase
|
|
@ -0,0 +1,9 @@
|
|||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path("chocos/", views.chocos),
|
||||
path("install/", views.install),
|
||||
path("installed/<pk>/", views.get_installed),
|
||||
path("refresh/<pk>/", views.refresh_installed),
|
||||
]
|
|
@ -0,0 +1,68 @@
|
|||
from django.conf import settings
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from rest_framework.decorators import (
|
||||
api_view,
|
||||
authentication_classes,
|
||||
permission_classes,
|
||||
)
|
||||
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
|
||||
from agents.models import Agent
|
||||
from .models import ChocoSoftware, InstalledSoftware
|
||||
from .serializers import ChocoSoftwareSerializer, InstalledSoftwareSerializer
|
||||
from .tasks import install_program
|
||||
|
||||
|
||||
@api_view()
|
||||
def chocos(request):
|
||||
pk = ChocoSoftware.sort_by_highest()
|
||||
choco = ChocoSoftware.objects.get(pk=pk)
|
||||
return Response(ChocoSoftwareSerializer(choco).data)
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
def install(request):
|
||||
pk = request.data["pk"]
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
name = request.data["name"]
|
||||
version = request.data["version"]
|
||||
install_program.delay(pk, name, version)
|
||||
return Response(f"{name} will be installed shortly on {agent.hostname}")
|
||||
|
||||
|
||||
@api_view()
|
||||
def get_installed(request, pk):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
try:
|
||||
software = InstalledSoftware.objects.filter(agent=agent).get()
|
||||
except Exception:
|
||||
return Response([])
|
||||
else:
|
||||
return Response(InstalledSoftwareSerializer(software).data)
|
||||
|
||||
|
||||
@api_view()
|
||||
def refresh_installed(request, pk):
|
||||
agent = get_object_or_404(Agent, pk=pk)
|
||||
r = agent.salt_api_cmd(hostname=agent.salt_id, timeout=30, func="pkg.list_pkgs")
|
||||
try:
|
||||
output = r.json()["return"][0][agent.salt_id]
|
||||
except Exception:
|
||||
return Response("error", status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
software = [{"name": k, "version": v} for k, v in output.items()]
|
||||
except Exception:
|
||||
return Response("error", status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if not InstalledSoftware.objects.filter(agent=agent).exists():
|
||||
InstalledSoftware(agent=agent, software=software).save()
|
||||
else:
|
||||
current = InstalledSoftware.objects.filter(agent=agent).get()
|
||||
current.software = software
|
||||
current.save(update_fields=["software"])
|
||||
|
||||
return Response("ok")
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import absolute_import, unicode_literals
|
||||
import os
|
||||
from celery import Celery
|
||||
from celery.schedules import crontab
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tacticalrmm.settings")
|
||||
|
||||
|
@ -14,6 +15,13 @@ app.task_serializer = "json"
|
|||
app.conf.task_track_started = True
|
||||
app.autodiscover_tasks()
|
||||
|
||||
app.conf.beat_schedule = {
|
||||
'update-chocos': {
|
||||
'task': 'software.tasks.update_chocos',
|
||||
'schedule': crontab(minute=0, hour=4),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@app.task(bind=True)
|
||||
def debug_task(self):
|
||||
|
|
|
@ -23,6 +23,7 @@ INSTALLED_APPS = [
|
|||
'checks',
|
||||
'services',
|
||||
'winupdate',
|
||||
'software',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
|
|
@ -17,4 +17,5 @@ urlpatterns = [
|
|||
path("checks/", include("checks.urls")),
|
||||
path("services/", include("services.urls")),
|
||||
path("winupdate/", include("winupdate.urls")),
|
||||
path("software/", include("software.urls")),
|
||||
]
|
||||
|
|
|
@ -411,6 +411,7 @@ export default {
|
|||
this.$store.dispatch("loadSummary", pk);
|
||||
this.$store.dispatch("loadChecks", pk);
|
||||
this.$store.dispatch("loadWinUpdates", pk);
|
||||
this.$store.dispatch("loadInstalledSoftware", pk);
|
||||
},
|
||||
overdueAlert(category, pk, alert_action) {
|
||||
const action = alert_action ? "enabled" : "disabled";
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
<template>
|
||||
<div v-if="!Array.isArray(software) || !software.length">No software</div>
|
||||
<div v-else>
|
||||
<q-btn
|
||||
size="sm"
|
||||
color="grey-5"
|
||||
icon="fas fa-plus"
|
||||
label="Install Software"
|
||||
text-color="black"
|
||||
@click="showInstallSoftware = true"
|
||||
/>
|
||||
<q-btn dense flat push @click="refreshSoftware" icon="refresh" />
|
||||
<q-table
|
||||
class="software-sticky-header-table"
|
||||
dense
|
||||
:data="software"
|
||||
:columns="columns"
|
||||
:pagination.sync="pagination"
|
||||
binary-state-sort
|
||||
hide-bottom
|
||||
row-key="name"
|
||||
:loading="loading"
|
||||
>
|
||||
<template v-slot:loading>
|
||||
<q-inner-loading showing color="primary" />
|
||||
</template>
|
||||
</q-table>
|
||||
|
||||
<q-dialog v-model="showInstallSoftware">
|
||||
<InstallSoftware @close="showInstallSoftware = false" :agentpk="selectedAgentPk" />
|
||||
</q-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import { mapGetters } from "vuex";
|
||||
import { mapState } from "vuex";
|
||||
import InstallSoftware from "@/components/modals/software/InstallSoftware";
|
||||
export default {
|
||||
name: "SoftwareTab",
|
||||
components: {
|
||||
InstallSoftware
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showInstallSoftware: false,
|
||||
loading: false,
|
||||
pagination: {
|
||||
rowsPerPage: 0,
|
||||
sortBy: "name",
|
||||
descending: false
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
name: "name",
|
||||
align: "left",
|
||||
label: "Name",
|
||||
field: "name",
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: "version",
|
||||
align: "left",
|
||||
label: "Version",
|
||||
field: "version",
|
||||
sortable: false
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
refreshSoftware() {
|
||||
const pk = this.selectedAgentPk;
|
||||
this.loading = true;
|
||||
axios
|
||||
.get(`/software/refresh/${pk}`)
|
||||
.then(r => {
|
||||
this.$store.dispatch("loadInstalledSoftware", pk);
|
||||
this.loading = false;
|
||||
})
|
||||
.catch(e => {
|
||||
this.loading = false;
|
||||
this.notifyError("Unable to contact the agent");
|
||||
});
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(["selectedAgentPk"]),
|
||||
...mapState({
|
||||
software: state => state.installedSoftware
|
||||
})
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.software-sticky-header-table {
|
||||
/* max height is important */
|
||||
.q-table__middle {
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
.q-table__top, .q-table__bottom, thead tr:first-child th {
|
||||
background-color: #f5f4f2;
|
||||
}
|
||||
|
||||
thead tr:first-child th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
opacity: 1;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -14,6 +14,7 @@
|
|||
<q-tab name="summary" icon="fas fa-server" size="xs" label="Summary" />
|
||||
<q-tab name="checks" icon="computer" label="Checks" />
|
||||
<q-tab name="patches" label="Patches" />
|
||||
<q-tab name="software" label="Software" />
|
||||
</q-tabs>
|
||||
<q-separator />
|
||||
<q-tab-panels v-model="subtab" :animated="false">
|
||||
|
@ -26,6 +27,9 @@
|
|||
<q-tab-panel name="patches">
|
||||
<WindowsUpdates />
|
||||
</q-tab-panel>
|
||||
<q-tab-panel name="software">
|
||||
<SoftwareTab />
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -34,12 +38,14 @@
|
|||
import SummaryTab from '@/components/SummaryTab';
|
||||
import ChecksTab from '@/components/ChecksTab';
|
||||
import WindowsUpdates from '@/components/WindowsUpdates';
|
||||
import SoftwareTab from '@/components/SoftwareTab';
|
||||
export default {
|
||||
name: "SubTableTabs",
|
||||
components: {
|
||||
SummaryTab,
|
||||
ChecksTab,
|
||||
WindowsUpdates
|
||||
WindowsUpdates,
|
||||
SoftwareTab,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
|
@ -0,0 +1,131 @@
|
|||
<template>
|
||||
<q-card style="width: 50vw; max-width: 80vw;">
|
||||
<q-card-section>
|
||||
<q-table
|
||||
class="choco-sticky-header-table"
|
||||
title="Software"
|
||||
dense
|
||||
:data="chocos"
|
||||
:columns="columns"
|
||||
:pagination.sync="pagination"
|
||||
:filter="filter"
|
||||
binary-state-sort
|
||||
hide-bottom
|
||||
virtual-scroll
|
||||
:rows-per-page-options="[0]"
|
||||
row-key="name"
|
||||
>
|
||||
<template v-slot:top>
|
||||
<q-input v-model="filter" outlined label="Search" dense clearable>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="search" />
|
||||
</template>
|
||||
</q-input>
|
||||
<q-space />
|
||||
<q-btn icon="close" flat round dense v-close-popup />
|
||||
</template>
|
||||
<template slot="body" slot-scope="props" :props="props">
|
||||
<q-tr :props="props">
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
size="sm"
|
||||
color="grey-5"
|
||||
icon="fas fa-plus"
|
||||
text-color="black"
|
||||
@click="install(props.row.name, props.row.version)"
|
||||
/>
|
||||
</q-td>
|
||||
<q-td @click="showDescription(props.row.name)">
|
||||
<span style="cursor:pointer;color:blue;text-decoration:underline">{{ props.row.name }}</span>
|
||||
</q-td>
|
||||
<q-td>{{ props.row.version }}</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import { mapState } from "vuex";
|
||||
import mixins from "@/mixins/mixins";
|
||||
export default {
|
||||
name: "InstallSoftware",
|
||||
props: ["agentpk"],
|
||||
mixins: [mixins],
|
||||
data() {
|
||||
return {
|
||||
filter: "",
|
||||
chocos: [],
|
||||
pagination: {
|
||||
rowsPerPage: 0,
|
||||
sortBy: "name",
|
||||
descending: false
|
||||
},
|
||||
columns: [
|
||||
{ name: "install", align: "left", label: "Install", sortable: false },
|
||||
{
|
||||
name: "name",
|
||||
align: "left",
|
||||
label: "Name",
|
||||
field: "name",
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: "version",
|
||||
align: "left",
|
||||
label: "Version",
|
||||
field: "version",
|
||||
sortable: false
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getChocos() {
|
||||
axios.get("/software/chocos/").then(r => {
|
||||
this.chocos = r.data.chocos;
|
||||
});
|
||||
},
|
||||
showDescription(name) {
|
||||
window.open(`https://chocolatey.org/packages/${name}`, "_blank");
|
||||
},
|
||||
install(name, version) {
|
||||
const data = { name: name, version: version, pk: this.agentpk };
|
||||
axios
|
||||
.post("/software/install/", data)
|
||||
.then(r => {
|
||||
this.$emit("close");
|
||||
this.notifySuccess(r.data);
|
||||
})
|
||||
.catch(e => {
|
||||
this.notifyError("Something went wrong");
|
||||
});
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getChocos();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.choco-sticky-header-table {
|
||||
/* max height is important */
|
||||
.q-table__middle {
|
||||
max-height: 30vw;
|
||||
}
|
||||
|
||||
.q-table__top, .q-table__bottom, thead tr:first-child th {
|
||||
background-color: #f5f4f2;
|
||||
}
|
||||
|
||||
thead tr:first-child th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
opacity: 1;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -22,7 +22,8 @@ export const store = new Vuex.Store({
|
|||
winUpdates: {},
|
||||
agentChecks: {},
|
||||
agentTableLoading: false,
|
||||
treeLoading: false
|
||||
treeLoading: false,
|
||||
installedSoftware: []
|
||||
},
|
||||
getters: {
|
||||
loggedIn(state) {
|
||||
|
@ -76,6 +77,9 @@ export const store = new Vuex.Store({
|
|||
SET_WIN_UPDATE(state, updates) {
|
||||
state.winUpdates = updates;
|
||||
},
|
||||
SET_INSTALLED_SOFTWARE(state, software) {
|
||||
state.installedSoftware = software;
|
||||
},
|
||||
setChecks(state, checks) {
|
||||
state.agentChecks = checks;
|
||||
},
|
||||
|
@ -87,6 +91,11 @@ export const store = new Vuex.Store({
|
|||
}
|
||||
},
|
||||
actions: {
|
||||
loadInstalledSoftware(context, pk) {
|
||||
axios.get(`/software/installed/${pk}`).then(r => {
|
||||
context.commit("SET_INSTALLED_SOFTWARE", r.data.software);
|
||||
});
|
||||
},
|
||||
loadWinUpdates(context, pk) {
|
||||
axios.get(`/winupdate/${pk}/getwinupdates/`).then(r => {
|
||||
context.commit("SET_WIN_UPDATE", r.data);
|
||||
|
|
|
@ -183,6 +183,7 @@ export default {
|
|||
this.$store.dispatch("loadSummary", pk);
|
||||
this.$store.dispatch("loadChecks", pk);
|
||||
this.$store.dispatch("loadWinUpdates", pk);
|
||||
this.$store.dispatch("loadInstalledSoftware", pk);
|
||||
}
|
||||
},
|
||||
loadFrame(activenode) {
|
||||
|
|
Loading…
Reference in New Issue