From da16e5ff56aef8426b52d5ae09893f220a13b72b Mon Sep 17 00:00:00 2001 From: mpuckett Date: Thu, 25 Jun 2026 18:02:16 -0500 Subject: [PATCH] Initial --- README.md | 50 ++- client/src/App.vue | 80 ++++- .../components/hosts/VCenterImportModal.vue | 45 ++- client/src/style.css | 154 +++++++- client/src/views/SystemJobsView.vue | 241 +++++++++++++ server/controllers/systemJobController.js | 18 + server/data/helpCatalog.js | 14 +- server/db.js | 14 + server/models/mappers.js | 16 + server/models/systemJobModel.js | 55 +++ server/routes/index.js | 2 + server/routes/systemJobRoutes.js | 11 + server/services/powershellRunner.js | 107 +++++- server/services/systemJobService.js | 257 ++++++++++++++ server/services/vcenterService.js | 334 ++++++++++++++++-- server/test/jobs.test.mjs | 39 ++ server/test/vcenter.test.mjs | 107 +++++- 17 files changed, 1475 insertions(+), 69 deletions(-) create mode 100644 client/src/views/SystemJobsView.vue create mode 100644 server/controllers/systemJobController.js create mode 100644 server/models/systemJobModel.js create mode 100644 server/routes/systemJobRoutes.js create mode 100644 server/services/systemJobService.js diff --git a/README.md b/README.md index 3f7a292..3f98fc8 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ The current UI is a Vue/Vite view layer over the Express API. All create/update/ - Host Library with searchable/sortable/paginated table, modal add/edit flow, VMware vCenter VM import wizard, standalone ESXi connection records, and manual/dynamic Host Groups for RunPlan and script-test targeting. - Credential Vault as its own menu item with encrypted-at-rest secrets and modal add/edit flow. - RunPlan Library with searchable/sortable/paginated table, modal composer, and execute action. +- Admin System Jobs console for background worker run-now actions, stuck execution cancellation, and audited job-management history. - Aggregate job log viewer with search/filter/pagination. - System log tab for Winston request/application logs. - Users & Groups administration with modal create flows. @@ -508,10 +509,10 @@ Payload: | `POST` | `/api/hosts` | User | Create host. | | `GET` | `/api/hosts/import/vcenter/status` | User | Return legacy effective vCenter status plus visible saved VMware connection records. | | `GET` | `/api/hosts/import/vcenter/connections` | User | List visible saved VMware connections. | -| `POST` | `/api/hosts/import/vcenter/connections` | User | Create an encrypted VMware connection record. Use `targetType: "vcenter"` for inventory or `targetType: "host"` for standalone ESXi Web Services testing. | +| `POST` | `/api/hosts/import/vcenter/connections` | User | Create an encrypted VMware connection record. Use `targetType: "vcenter"` for vCenter inventory or `targetType: "host"` for standalone ESXi Web Services inventory. | | `PUT` | `/api/hosts/import/vcenter/connections/:connectionId` | User | Update a visible VMware connection; omit password to keep the existing secret. | | `DELETE` | `/api/hosts/import/vcenter/connections/:connectionId` | User | Delete a visible VMware connection. | -| `POST` | `/api/hosts/import/vcenter/connections/:connectionId/test` | User | Verify a VMware connection. vCenter records authenticate and list VM summaries; standalone host records call the vSphere Web Services SOAP endpoint. | +| `POST` | `/api/hosts/import/vcenter/connections/:connectionId/test` | User | Verify a VMware connection. vCenter records authenticate and list VM summaries; standalone ESXi records authenticate to `/sdk` and query VM inventory through the vSphere Web Services API. | | `POST` | `/api/hosts/import/vcenter/preview` | User | Authenticate to vCenter, search VM inventory, and return import candidates. | | `POST` | `/api/hosts/import/vcenter` | User | Import selected vCenter VM candidates as hosts, attaching the chosen credential/defaults. | | `GET` | `/api/hosts/groups` | User | List visible manual and dynamic Host Groups with member counts. | @@ -645,13 +646,23 @@ by the Credential Vault. `targetType: "vcenter"` records use the vSphere Automation API or legacy REST API for inventory and VM power-state checks. `targetType: "host"` records are standalone host connections that use the vSphere Web Services SOAP endpoint at `/sdk`; they validate direct ESXi -connectivity but cannot perform vCenter VM inventory imports. Existing +connectivity and enumerate VM inventory directly from the selected ESXi host in +the VMware import wizard. Existing `vcenter_*` settings and `VCENTER_*` environment variables remain supported as the `Configured default` import connection for single-vCenter deployments or Docker-provided configuration. VMware vCenter import can use a saved vCenter system selected by `connectionId` or the legacy configured default from Config / Integrations (`vcenter_*` settings or `VCENTER_*` environment variables). `apiMode` / `vcenter_api_mode` can be `auto`, `api`, or `rest`. `api` uses the current vSphere Automation API flow (`POST /api/session`, `GET /api/vcenter/vm`). `rest` uses the legacy/community vCenter REST flow (`POST /rest/com/vmware/cis/session`, `GET /rest/vcenter/vm`). `auto` tries the current `/api` profile first and falls back to `/rest` when the endpoint is not supported. +When the import wizard selects a standalone ESXi connection (`targetType: +"host"`), preview uses the vSphere Web Services API sequence +`RetrieveServiceContent`, `Login`, `CreateContainerView`, and +`RetrievePropertiesEx` against `/sdk` to enumerate `VirtualMachine` managed +objects. Filter fields work the same as vCenter imports. Imported hosts store +the selected ESXi connection plus the VM managed-object reference in +`sourceRef`, so script tests and RunPlans can run a direct ESXi power-state +check before spawning PowerShell. + For exact VM-name searches (`field: "name"`, `operator: "equals"`), POSHManager uses the documented server-side name filters: `names=` for `/api/vcenter/vm` and `filter.names=` for `/rest/vcenter/vm`. Contains/starts-with/ends-with searches are filtered locally because vCenter VM list filters do not provide wildcard contains semantics. The combined `hostname` field searches both the VM inventory name and the VMware Tools guest `host_name`/FQDN after enrichment. VM display-name matches are checked first, then the rest of the inventory is still enriched for guest-only hostname matches. Networking/IP enrichment is only requested for candidates that match the filter. FQDN and IP discovery depends on VMware Tools data; when guest data is unavailable, the import falls back to the VM name as the host address and records the limitation in host notes. For `field: "name"` filters, the API scans the full VM summary list returned by vCenter before guest enrichment, so large inventories are not accidentally missed by the preview limit. The preview response includes `diagnostics` with `totalFromVCenter`, `summaryMatched`, `scanned`, `resultLimit`, and `sampleNames`; the wizard displays those values when troubleshooting a no-result preview. @@ -703,14 +714,13 @@ hosts with the same name, address, or FQDN are skipped and returned in the When a script test or RunPlan targets a host with `sourceType: "vmware"`, the runner checks the current vCenter power state for `sourceRef` through the stored -vCenter connection before spawning PowerShell. VMs that are not `POWERED_ON` +VMware connection before spawning PowerShell. vCenter imports use the selected +vCenter API profile; standalone ESXi imports use the vSphere Web Services API +against `/sdk`. VMs that are not `POWERED_ON` are logged and marked `skipped` for that host, so powered-off machines are not -treated like script failures. If the vCenter connection is missing or the +treated like script failures. If the VMware connection is missing or the management API is temporarily unavailable, the runner logs the reason and continues execution rather than blocking the whole job on a control-plane issue. -Standalone ESXi host connection records do not provide vCenter VM inventory -state, so VMware-sourced imported guests continue to use their saved vCenter -source connection for pre-run power checks. ### Script Library @@ -1154,6 +1164,9 @@ with per-host log rows. | `POST` | `/api/runplans/:id/execute` | User | Queue/execute a RunPlan. | | `GET` | `/api/jobs` | User | List jobs. | | `GET` | `/api/jobs/:id` | User | Read job with logs. | +| `GET` | `/api/system-jobs` | Admin | List running/queued execution jobs, controllable background workers, and recent job-management audit actions. | +| `POST` | `/api/system-jobs/:jobId/run` | Admin | Run a supported background worker now, such as dynamic Host Group sync, catalog auto-check, or Intune auto-promotion. | +| `POST` | `/api/system-jobs/:jobId/cancel` | Admin | Cancel a queued/running script execution job, signal active PowerShell child processes, and audit the action. | RunPlan payload: @@ -1205,6 +1218,27 @@ Windows-specific environment variables, and alias-heavy script style. RunPlan execution and Script Test / Run also write the same warning summary into per-host job logs for Linux targets. +Admin System Jobs console: + +```bash +curl http://localhost:3000/api/system-jobs \ + -H "Authorization: Bearer $TOKEN" + +curl -X POST http://localhost:3000/api/system-jobs/worker%3Ahost-group-sync/run \ + -H "Authorization: Bearer $TOKEN" + +curl -X POST http://localhost:3000/api/system-jobs/job_123/cancel \ + -H "Authorization: Bearer $TOKEN" \ + -H 'Content-Type: application/json' \ + -d '{"reason":"Operator canceled stuck remoting session"}' +``` + +`/api/system-jobs` is admin-only. It combines execution jobs from RunPlans and +script tests with API-managed background workers. Canceling an execution job +marks queued/running host rows as canceled, writes a job log line, attempts to +terminate active PowerShell child processes, and records the management action +in the `system_job_actions` audit table. + ### Settings And Logs | Method | Route | Auth | Description | diff --git a/client/src/App.vue b/client/src/App.vue index 498a5bf..1c9df06 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -597,6 +597,16 @@ + +
script.id === selectedScript.value.id); if (refreshedSelection) selectScript(refreshedSelection); @@ -2532,7 +2552,7 @@ async function previewVCenterImport(payload) { vcenterImportError.value = ''; vcenterTraceEntries.value = [{ info: true, - message: 'vCenter preview request is running.', + message: 'VMware host preview request is running.', details: { filter: payload.filter, limit: payload.limit, @@ -2550,7 +2570,7 @@ async function previewVCenterImport(payload) { details: { route: '/api/hosts/import/vcenter/preview', elapsedMs: 8000, - note: 'If this remains here, the backend is still waiting on vCenter DNS, TCP, TLS, authentication, or VM inventory response.' + note: 'If this remains here, the backend is still waiting on VMware DNS, TCP, TLS, authentication, or inventory response.' } } ]; @@ -2564,7 +2584,7 @@ async function previewVCenterImport(payload) { details: { route: '/api/hosts/import/vcenter/preview', elapsedMs: 18000, - note: 'The backend vCenter request timeout should return a traceable timeout shortly unless the API process is blocked before dispatch.' + note: 'The backend VMware request timeout should return a traceable timeout shortly unless the API process is blocked before dispatch.' } } ]; @@ -2581,11 +2601,14 @@ async function previewVCenterImport(payload) { }]; vcenterPreviewRows.value = result.candidates || []; vcenterTraceOpen.value = true; - notify(`Found ${result.count || 0} vCenter host candidate(s) from ${result.diagnostics?.totalFromVCenter ?? 'unknown'} VM(s)`); + const standalone = result.diagnostics?.standaloneHost; + notify(standalone + ? `Prepared ${result.count || 0} standalone ESXi host candidate(s)` + : `Found ${result.count || 0} vCenter host candidate(s) from ${result.diagnostics?.totalFromVCenter ?? 'unknown'} VM(s)`); } catch (err) { vcenterTraceEntries.value = err.payload?.trace?.length ? err.payload.trace : [{ info: true, - message: 'Preview failed before a vCenter HTTP trace could be captured.', + message: 'Preview failed before a VMware API trace could be captured.', details: { error: err.message, status: err.status || null } }]; vcenterTraceOpen.value = true; @@ -2602,7 +2625,7 @@ async function importVCenterHosts(payload) { vcenterImportError.value = ''; vcenterTraceEntries.value = [{ info: true, - message: 'vCenter import request is running.', + message: 'VMware host import request is running.', details: { vmIds: payload.vmIds, startedAt: new Date().toISOString() } }]; vcenterTraceOpen.value = true; @@ -2619,7 +2642,7 @@ async function importVCenterHosts(payload) { } catch (err) { vcenterTraceEntries.value = err.payload?.trace?.length ? err.payload.trace : [{ info: true, - message: 'Import failed before a vCenter HTTP trace could be captured.', + message: 'Import failed before a VMware API trace could be captured.', details: { error: err.message, status: err.status || null } }]; vcenterTraceOpen.value = true; @@ -2717,6 +2740,49 @@ async function loadSystemLogs() { systemLogs.value = await api.get('/api/logs/system?limit=300'); } +async function refreshSystemJobs(quiet = false) { + if (user.value?.role !== 'admin') return; + systemJobsLoading.value = true; + try { + const result = await api.get('/api/system-jobs'); + systemJobs.value = result.jobs || []; + systemJobActions.value = result.actions || []; + } catch (err) { + if (!quiet) notify(err.message, 'error'); + } finally { + systemJobsLoading.value = false; + } +} + +async function runSystemJob(jobId) { + systemJobsLoading.value = true; + try { + const result = await api.post(`/api/system-jobs/${encodeURIComponent(jobId)}/run`, {}); + notify(result.action?.message || 'System job completed'); + await refreshSystemJobs(true); + } catch (err) { + notify(err.message, 'error'); + await refreshSystemJobs(true); + } finally { + systemJobsLoading.value = false; + } +} + +async function cancelSystemJob({ id, reason }) { + systemJobsLoading.value = true; + try { + const result = await api.post(`/api/system-jobs/${encodeURIComponent(id)}/cancel`, { reason }); + notify(result.action?.message || 'Cancel requested'); + jobs.value = await api.get('/api/jobs'); + await refreshSystemJobs(true); + } catch (err) { + notify(err.message, 'error'); + await refreshSystemJobs(true); + } finally { + systemJobsLoading.value = false; + } +} + async function saveSettings() { const payload = {}; Object.entries(settingsDraft).forEach(([key, row]) => { payload[key] = row.value; }); diff --git a/client/src/components/hosts/VCenterImportModal.vue b/client/src/components/hosts/VCenterImportModal.vue index b6c4ef7..499d228 100644 --- a/client/src/components/hosts/VCenterImportModal.vue +++ b/client/src/components/hosts/VCenterImportModal.vue @@ -1,9 +1,9 @@