rework bulk action modal. start running bulk actions on next agent checkin

This commit is contained in:
sadnub
2021-07-30 12:48:47 -04:00
parent b5b5ddc047
commit a8df08cf4e
7 changed files with 310 additions and 282 deletions

View File

@@ -22,3 +22,10 @@ export async function runScript(payload) {
return data
} catch (e) { }
}
export async function runBulkAction(payload) {
const { data } = await axios.post("/agents/bulk/", payload)
return data
}

View File

@@ -7,4 +7,11 @@ export async function fetchClients() {
const { data } = await axios.get(`${baseUrl}/clients/`)
return data
} catch (e) { }
}
export async function fetchSites() {
try {
const { data } = await axios.get(`${baseUrl}/sites/`)
return data
} catch (e) { }
}

View File

@@ -105,15 +105,15 @@
<q-menu auto-close>
<q-list dense style="min-width: 100px">
<!-- bulk command -->
<q-item clickable v-close-popup @click="showBulkActionModal('command')">
<q-item clickable v-close-popup @click="showBulkAction('command')">
<q-item-section>Bulk Command</q-item-section>
</q-item>
<!-- bulk script -->
<q-item clickable v-close-popup @click="showBulkActionModal('script')">
<q-item clickable v-close-popup @click="showBulkAction('script')">
<q-item-section>Bulk Script</q-item-section>
</q-item>
<!-- bulk patch management -->
<q-item clickable v-close-popup @click="showBulkActionModal('scan')">
<q-item clickable v-close-popup @click="showBulkAction('patch')">
<q-item-section>Bulk Patch Management</q-item-section>
</q-item>
<!-- server maintenance -->
@@ -176,10 +176,6 @@
<q-dialog v-model="showUploadMesh">
<UploadMesh @close="showUploadMesh = false" />
</q-dialog>
<!-- Bulk action modal -->
<q-dialog v-model="showBulkAction" @hide="closeBulkActionModal" position="top">
<BulkAction :mode="bulkMode" @close="closeBulkActionModal" />
</q-dialog>
<!-- Agent Deployment -->
<q-dialog v-model="showDeployment">
<Deployment @close="showDeployment = false" />
@@ -228,7 +224,6 @@ export default {
InstallAgent,
UploadMesh,
AdminManager,
BulkAction,
Deployment,
ServerMaintenance,
CodeSign,
@@ -242,9 +237,7 @@ export default {
showAdminManager: false,
showInstallAgent: false,
showUploadMesh: false,
showBulkAction: false,
showPendingActions: false,
bulkMode: null,
showDeployment: false,
showCodeSign: false,
};
@@ -333,6 +326,14 @@ export default {
component: ScriptManager,
});
},
showBulkAction(mode) {
this.$q.dialog({
component: BulkAction,
componentProps: {
mode: mode,
},
});
},
showDebugLog() {
this.$q.dialog({
component: DialogWrapper,

View File

@@ -1,290 +1,293 @@
<template>
<q-card style="min-width: 50vw">
<q-card-section class="row items-center">
<div class="text-h6">
<q-dialog ref="dialogRef" @hide="onDialogHide">
<q-card class="q-dialog-plugin" style="min-width: 50vw">
<q-bar>
{{ modalTitle }}
<div v-if="modalCaption !== null" class="text-caption">{{ modalCaption }}</div>
</div>
<q-space />
<q-btn icon="close" flat round dense v-close-popup />
<br />
</q-card-section>
<q-form @submit.prevent="send">
<q-card-section>
<div class="q-pa-none">
<q-space />
<q-btn dense flat icon="close" v-close-popup>
<q-tooltip class="bg-white text-primary">Close</q-tooltip>
</q-btn>
</q-bar>
<q-form @submit.prevent="submit">
<q-card-section>
<p>Agent Type</p>
<div class="q-gutter-sm">
<q-radio dense v-model="monType" val="all" label="All" />
<q-radio dense v-model="monType" val="servers" label="Servers" />
<q-radio dense v-model="monType" val="workstations" label="Workstations" />
</div>
</div>
</q-card-section>
<q-card-section>
<div class="q-pa-none">
<q-option-group v-model="monType" :options="monTypeOptions" color="primary" dense inline class="q-pl-sm" />
</q-card-section>
<q-card-section>
<p>Choose Target</p>
<div class="q-gutter-sm">
<q-radio dense v-model="target" val="client" label="Client" @update:model-value="agentMultiple = []" />
<q-radio
dense
v-model="target"
val="site"
label="Site"
@update:model-value="
() => {
agentMultiple = [];
site = sites[0];
}
"
/>
<q-radio dense v-model="target" val="agents" label="Selected Agents" />
<q-radio dense v-model="target" val="all" label="All Agents" @update:model-value="agentMultiple = []" />
</div>
</div>
</q-card-section>
<q-option-group v-model="target" :options="targetOptions" color="primary" dense inline class="q-pl-sm" />
</q-card-section>
<q-card-section v-if="target === 'client' || target === 'site'" class="q-pb-none">
<q-select
dense
:rules="[val => !!val || '*Required']"
outlined
options-dense
label="Select client"
v-model="client"
:options="client_options"
@update:model-value="target === 'site' ? (site = sites[0]) : () => {}"
/>
<q-select
v-if="target === 'site'"
dense
:rules="[val => !!val || '*Required']"
outlined
options-dense
label="Select site"
v-model="site"
:options="sites"
/>
</q-card-section>
<q-card-section v-if="target === 'agents'">
<q-select
dense
options-dense
filled
v-model="agentMultiple"
multiple
:options="agents"
use-chips
stack-label
map-options
emit-value
label="Select Agents"
/>
</q-card-section>
<q-card-section>
<tactical-dropdown
v-if="target === 'client'"
:rules="[val => !!val || '*Required']"
v-model="client"
:options="clientOptions"
label="Select Client"
outlined
mapOptions
filterable
/>
<tactical-dropdown
v-else-if="target === 'site'"
:rules="[val => !!val || '*Required']"
v-model="site"
:options="siteOptions"
label="Select Site"
outlined
mapOptions
filterable
/>
<tactical-dropdown
v-else-if="target === 'agents'"
v-model="agents"
:options="agentOptions"
label="Select Agents"
filled
multiple
mapOptions
filterable
/>
</q-card-section>
<q-card-section v-if="mode === 'script'" class="q-pt-none">
<q-select
:rules="[val => !!val || '*Required']"
dense
outlined
v-model="scriptPK"
:options="scriptOptions"
label="Select script"
map-options
emit-value
options-dense
@update:model-value="setScriptDefaults"
>
<template v-slot:option="scope">
<q-item v-if="!scope.opt.category" v-bind="scope.itemProps" class="q-pl-lg">
<q-item-section>
<q-item-label v-html="scope.opt.label"></q-item-label>
</q-item-section>
</q-item>
<q-item-label v-if="scope.opt.category" v-bind="scope.itemProps" header class="q-pa-sm">{{
scope.opt.category
}}</q-item-label>
</template>
</q-select>
</q-card-section>
<q-card-section v-if="mode === 'script'" class="q-pt-none">
<q-select
label="Script Arguments (press Enter after typing each argument)"
filled
dense
v-model="args"
use-input
use-chips
multiple
hide-dropdown-icon
input-debounce="0"
new-value-mode="add"
/>
</q-card-section>
<q-card-section v-if="mode === 'script'" class="q-pt-none">
<tactical-dropdown
:rules="[val => !!val || '*Required']"
v-model="script"
:options="scriptOptions"
label="Select Script"
outlined
mapOptions
filterable
/>
</q-card-section>
<q-card-section v-if="mode === 'script'" class="q-pt-none">
<q-select
label="Script Arguments (press Enter after typing each argument)"
filled
dense
v-model="args"
use-input
use-chips
multiple
hide-dropdown-icon
input-debounce="0"
new-value-mode="add"
/>
</q-card-section>
<q-card-section v-if="mode === 'command'">
<p>Shell</p>
<div class="q-gutter-sm">
<q-radio dense v-model="shell" val="cmd" label="CMD" />
<q-radio dense v-model="shell" val="powershell" label="Powershell" />
</div>
</q-card-section>
<q-card-section v-if="mode === 'command'">
<q-input
v-model="cmd"
outlined
label="Command"
stack-label
:placeholder="
shell === 'cmd' ? 'rmdir /S /Q C:\\Windows\\System32' : 'Remove-Item -Recurse -Force C:\\Windows\\System32'
"
:rules="[val => !!val || '*Required']"
/>
</q-card-section>
<q-card-section v-if="mode === 'command'">
<p>Shell</p>
<q-option-group v-model="shell" :options="shellOptions" color="primary" dense inline class="q-pl-sm" />
</q-card-section>
<q-card-section v-if="mode === 'command'">
<q-input
v-model="cmd"
outlined
label="Command"
stack-label
:placeholder="
shell === 'cmd'
? 'rmdir /S /Q C:\\Windows\\System32'
: 'Remove-Item -Recurse -Force C:\\Windows\\System32'
"
:rules="[val => !!val || '*Required']"
/>
</q-card-section>
<q-card-section v-if="mode === 'script' || mode === 'command'">
<q-input
v-model.number="timeout"
dense
outlined
type="number"
style="max-width: 150px"
label="Timeout (seconds)"
stack-label
:rules="[val => !!val || '*Required', val => val >= 5 || 'Minimum is 5 seconds']"
/>
</q-card-section>
<q-card-section v-if="mode === 'script' || mode === 'command'">
<q-input
v-model.number="timeout"
dense
outlined
type="number"
style="max-width: 150px"
label="Timeout (seconds)"
stack-label
:rules="[val => !!val || '*Required', val => val >= 5 || 'Minimum is 5 seconds']"
/>
</q-card-section>
<q-card-section v-if="mode === 'scan'">
<div class="q-pa-none">
<q-card-section v-if="mode === 'patch'">
<p>Action</p>
<div class="q-gutter-sm">
<q-radio dense v-model="selected_mode" val="scan" label="Run Patch Status Scan" />
<q-radio dense v-model="selected_mode" val="install" label="Install Pending Patches Now" />
</div>
</div>
</q-card-section>
<q-option-group
v-model="patchMode"
:options="patchModeOptions"
color="primary"
dense
inline
class="q-pl-sm"
/>
</q-card-section>
<q-card-actions align="center">
<q-btn label="Run" color="primary" class="full-width" type="submit" />
</q-card-actions>
</q-form>
</q-card>
<q-card-section v-show="false">
<q-checkbox v-model="offlineAgents" label="Offline Agents (Run on next checkin)">
<q-tooltip>If the agent is offline, a pending action will be created to run on agent checkin</q-tooltip>
</q-checkbox>
</q-card-section>
<q-card-actions align="right">
<q-btn label="Cancel" v-close-popup />
<q-btn label="Run" color="primary" type="submit" :disable="loading" :loading="loading" />
</q-card-actions>
</q-form>
</q-card>
</q-dialog>
</template>
<script>
import { mapState } from "vuex";
import mixins from "@/mixins/mixins";
// composition imports
import { ref, computed, watch, onMounted } from "vue";
import { useStore } from "vuex";
import { useDialogPluginComponent } from "quasar";
import { useScriptDropdown } from "@/composables/scripts";
import { useAgentDropdown } from "@/composables/agents";
import { useClientDropdown, useSiteDropdown } from "@/composables/clients";
import { runBulkAction } from "@/api/agents";
import { notifySuccess } from "@/utils/notify";
// ui imports
import TacticalDropdown from "@/components/ui/TacticalDropdown";
// static data
const monTypeOptions = [
{ label: "All", value: "all" },
{ label: "Servers", value: "servers" },
{ label: "Workstations", value: "workstations" },
];
const targetOptions = [
{ label: "Client", value: "client" },
{ label: "Site", value: "site" },
{ label: "Selected Agents", value: "agents" },
{ label: "All", value: "all" },
];
const shellOptions = [
{ label: "CMD", value: "cmd" },
{ label: "Powershell", value: "powershell" },
];
const patchModeOptions = [
{ label: "Scan", value: "scan" },
{ label: "Install", value: "install" },
];
export default {
name: "BulkAction",
emits: ["close"],
mixins: [mixins],
components: { TacticalDropdown },
emits: [...useDialogPluginComponent.emits],
props: {
mode: !String,
},
data() {
return {
target: "client",
monType: "all",
selected_mode: null,
scriptOptions: [],
scriptPK: null,
timeout: 900,
client: null,
client_options: [],
site: null,
agents: [],
agentMultiple: [],
args: [],
cmd: "",
shell: "cmd",
modalTitle: null,
modalCaption: null,
};
},
computed: {
...mapState(["showCommunityScripts"]),
sites() {
return !!this.client ? this.formatSiteOptions(this.client.sites) : [];
},
},
methods: {
setScriptDefaults() {
const script = this.scriptOptions.find(i => i.value === this.scriptPK);
setup(props) {
// setup vuex store
const store = useStore();
const showCommunityScripts = computed(() => store.state.showCommunityScripts);
this.timeout = script.timeout;
this.args = script.args;
},
send() {
this.$q.loading.show();
const data = {
mode: this.selected_mode,
monType: this.monType,
target: this.target,
site: this.site.value,
client: this.client.value,
agentPKs: this.agentMultiple,
scriptPK: this.scriptPK,
timeout: this.timeout,
args: this.args,
shell: this.shell,
timeout: this.timeout,
cmd: this.cmd,
// quasar dialog setup
const { dialogRef, onDialogHide } = useDialogPluginComponent();
// dropdown setup
const { script, scriptOptions, defaultTimeout, defaultArgs, getScriptOptions } = useScriptDropdown();
const { agents, agentOptions, getAgentOptions } = useAgentDropdown();
const { site, siteOptions, getSiteOptions } = useSiteDropdown();
const { client, clientOptions, getClientOptions } = useClientDropdown();
// bulk action logic
const target = ref("client");
const monType = ref("all");
const cmd = ref("");
const shell = ref("cmd");
const patchMode = ref("scan");
const offlineAgents = ref(false);
const loading = ref(false);
watch(target, () => (agents.value = []));
async function submit() {
loading.value = true;
const payload = {
mode: props.mode,
monType: monType.value,
target: target.value,
site: site.value,
client: client.value,
agents: agents.value,
script: script.value,
timeout: defaultTimeout.value,
args: defaultArgs.value,
shell: shell.value,
cmd: cmd.value,
patchMode: patchMode.value,
offlineAgents: offlineAgents.value,
};
this.$axios
.post("/agents/bulk/", data)
.then(r => {
this.$q.loading.hide();
this.$emit("close");
this.notifySuccess(r.data);
})
.catch(e => {
this.$q.loading.hide();
});
},
getClients() {
this.$axios
.get("/clients/clients/")
.then(r => {
this.client_options = this.formatClientOptions(r.data);
this.client = this.client_options[0];
this.site = this.sites[0];
})
.catch(e => {});
},
getAgents() {
this.$axios
.get("/agents/listagentsnodetail/")
.then(r => {
const ret = r.data.map(agent => ({ label: agent.hostname, value: agent.pk }));
this.agents = Object.freeze(ret.sort((a, b) => a.label.localeCompare(b.label)));
})
.catch(e => {});
},
setTitles() {
switch (this.mode) {
case "command":
this.modalTitle = "Run Bulk Command";
this.modalCaption = "Run a shell command on multiple agents in parallel";
break;
case "script":
this.modalTitle = "Run Bulk Script";
this.modalCaption = "Run a script on multiple agents in parallel";
break;
case "scan":
this.modalTitle = "Bulk Patch Management";
break;
}
},
},
mounted() {
this.setTitles();
this.getClients();
this.getAgents();
this.getScriptOptions(this.showCommunityScripts).then(options => (this.scriptOptions = Object.freeze(options)));
try {
const data = await runBulkAction(payload);
notifySuccess(data);
} catch (e) {}
this.selected_mode = this.mode;
loading.value = false;
}
// set modal title and caption
const modalTitle = computed(() => {
return props.mode === "command"
? "Run Bulk Command"
: props.mode === "script"
? "Run Bulk Script"
: props.mode === "scan"
? "Bulk Patch Management"
: "";
});
// component lifecycle hooks
onMounted(() => {
getAgentOptions();
getSiteOptions();
getClientOptions();
if (props.mode === "script") getScriptOptions(showCommunityScripts);
});
return {
// reactive data
target,
monType,
client,
site,
agents,
cmd,
shell,
patchMode,
script,
scriptOptions,
timeout: defaultTimeout.value,
args: defaultArgs.value,
agentOptions,
clientOptions,
siteOptions,
offlineAgents,
loading,
// non-reactive data
monTypeOptions,
targetOptions,
shellOptions,
patchModeOptions,
//computed
modalTitle,
//methods
submit,
// quasar dialog plugin
dialogRef,
onDialogHide,
};
},
};
</script>

View File

@@ -5,7 +5,8 @@ import { formatAgentOptions } from "@/utils/format"
// agent dropdown
export function useAgentDropdown() {
const agent = ref(null)
const agents = ref([])
const agentOptions = ref([])
// specifing flat returns an array of hostnames versus {value:id, label: hostname}
@@ -15,6 +16,8 @@ export function useAgentDropdown() {
return {
//data
agent,
agents,
agentOptions,
//methods

View File

@@ -4,7 +4,8 @@ import { fetchClients } from "@/api/clients"
import { formatClientOptions, formatSiteOptions } from "@/utils/format"
export function useClientDropdown() {
const client = ref(null)
const clients = ref([])
const clientOptions = ref([])
async function getClientOptions(flat = false) {
@@ -13,6 +14,8 @@ export function useClientDropdown() {
return {
//data
client,
clients,
clientOptions,
//methods
@@ -21,14 +24,18 @@ export function useClientDropdown() {
}
export function useSiteDropdown() {
const site = ref(null)
const sites = ref([])
const siteOptions = ref([])
async function getSiteOptions() {
siteOptions.value = formatSiteOptions(await fetchSites())
siteOptions.value = formatSiteOptions(await fetchClients())
}
return {
//data
site,
sites,
siteOptions,
//methods

View File

@@ -7,7 +7,7 @@ export function useScriptDropdown() {
const scriptOptions = ref([])
const defaultTimeout = ref(30)
const defaultArgs = ref([])
const scriptPK = ref(null)
const script = ref(null)
// specifing flat returns an array of script names versus {value:id, label: hostname}
async function getScriptOptions(showCommunityScripts = false, flat = false) {
@@ -15,17 +15,17 @@ export function useScriptDropdown() {
}
// watch scriptPk for changes and update the default timeout and args
watch(scriptPK, (newValue, oldValue) => {
watch(script, (newValue, oldValue) => {
if (newValue) {
const script = scriptOptions.value.find(i => i.value === newValue);
defaultTimeout.value = script.timeout;
defaultArgs.value = script.args;
const tmpScript = scriptOptions.value.find(i => i.value === newValue);
defaultTimeout.value = tmpScript.timeout;
defaultArgs.value = tmpScript.args;
}
})
return {
//data
scriptPK,
script,
scriptOptions,
defaultTimeout,
defaultArgs,