feat(260923-dhh): Proxmox-Modul Aufgabe 4 - Hintergrundabfrage je Mandant, Verbindungstest
- proxmox-scheduler.service.ts: ein Cron-Auftrag je aktivem Mandant (proxmox-poll:<tenantId>), onApplicationBootstrap (nicht onModuleInit, Tender-Muster), Abfrageintervall = kleinstes pollIntervalMin der aktiven Server, ein fehlgeschlagener Server bricht die Tick-Schleife nicht ab, refreshTenant() zieht nach jedem Speichern sofort nach - proxmox.service.ts: loadActiveServersForScheduler() als einziger forSystem()-Aufruf des Moduls (Erlaubnisliste in rls-access-inventory.spec.ts), testConnection() schreibt nicht ins Zwischenlager, pollServer() bekommt eine Zehn-Sekunden-Sperre (T-DHH-06) - proxmox.controller.ts: POST servers/:id/test, create() zieht den Planer nach dem Anlegen sofort nach - Zugriffsklassifikation: proxmoxServer wechselt auf system-gebunden (Startpfad des Planers), proxmoxServerStatus bleibt gebunden Tore: api 1306/1306 (>=1240), type-check 4/4, rls-access-inventory und rls-coverage gruen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@ import { Injectable, Logger } from '@nestjs/common';
|
||||
import type { ProxmoxServer } from '@prisma/client';
|
||||
import { CryptoService } from '../crypto/crypto.service';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { forTenant } from '../prisma/prisma-tenant.extension';
|
||||
import { forSystem, forTenant } from '../prisma/prisma-tenant.extension';
|
||||
import { buildTicketCookieHeader, buildTokenAuthHeader, loginTicket } from './proxmox-auth';
|
||||
import { proxmoxGet, type ProxmoxGetResult } from './proxmox-client.service';
|
||||
import { listPbsDatastoreNames, normalizePbs, normalizePmg, normalizePve } from './proxmox-normalize';
|
||||
@@ -53,6 +53,14 @@ const SAFE_SERVER_SELECT = {
|
||||
*/
|
||||
const PBS_SNAPSHOT_QUERY_CAP = 10;
|
||||
|
||||
/**
|
||||
* Zehn-Sekunden-Sperre fuer `pollServer` (Aufgabe 4, T-DHH-06): ein Klick
|
||||
* auf "Jetzt aktualisieren" darf nicht zu ungebremsten Anfragen gegen die
|
||||
* Fremd-API werden. Regulaer fragt ohnehin nur der Planer mit begrenzter
|
||||
* Frequenz ab (D-05).
|
||||
*/
|
||||
const POLL_LOCK_MS = 10_000;
|
||||
|
||||
function truncateRaw(body: unknown): string {
|
||||
let text: string;
|
||||
try {
|
||||
@@ -354,16 +362,37 @@ export class ProxmoxService {
|
||||
/**
|
||||
* Fragt genau einen Server ab und schreibt das Ergebnis ins Zwischenlager.
|
||||
* Liefert `null`, wenn der Server unter diesem Mandanten nicht existiert.
|
||||
*
|
||||
* Zehn-Sekunden-Sperre (Aufgabe 4, `<behavior>`, T-DHH-06): ein zweiter
|
||||
* Durchlauf innerhalb von zehn Sekunden nach dem letzten fragt Proxmox
|
||||
* NICHT erneut, sondern liefert den vorhandenen Zwischenlagerstand —
|
||||
* Schutz davor, dass ein Klick in der Oberflaeche zu ungebremsten
|
||||
* Anfragen gegen die Fremd-API wird.
|
||||
*/
|
||||
async pollServer(tenantId: string, serverId: string): Promise<ProxmoxPollResult | null> {
|
||||
const tenantPrisma = forTenant(this.prisma, tenantId);
|
||||
const server = await tenantPrisma.proxmoxServer.findUnique({
|
||||
where: { id: serverId },
|
||||
include: { status: true },
|
||||
});
|
||||
if (!server || server.tenantId !== tenantId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cachedStatus = server.status;
|
||||
if (cachedStatus?.lastPolledAt) {
|
||||
const ageMs = Date.now() - cachedStatus.lastPolledAt.getTime();
|
||||
if (ageMs < POLL_LOCK_MS) {
|
||||
return {
|
||||
reachable: cachedStatus.reachable,
|
||||
errorKind: cachedStatus.errorKind as ProxmoxErrorKind | null,
|
||||
errorDetail: cachedStatus.errorDetail,
|
||||
metrics: cachedStatus.metrics as ProxmoxPollResult['metrics'],
|
||||
rawSample: cachedStatus.rawSample,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const result = await this.pollOne(server);
|
||||
const now = new Date();
|
||||
|
||||
@@ -393,4 +422,68 @@ export class ProxmoxService {
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verbindungstest (Aufgabe 4, `<behavior>`, `POST servers/:id/test`):
|
||||
* benutzt denselben Klienten und dieselbe Fehleruebersetzung wie der
|
||||
* Planer, schreibt aber NICHT ins Zwischenlager — ein Testklick darf den
|
||||
* zuletzt gemessenen Stand nicht ueberschreiben (Vorbild
|
||||
* `TenderEmailConfigService.testConnection`/LDAP-Test). Keine
|
||||
* Zehn-Sekunden-Sperre: ein Test ist ein bewusster Einzelklick, kein
|
||||
* automatisierter Auffrischungsweg.
|
||||
*/
|
||||
async testConnection(tenantId: string, serverId: string): Promise<ProxmoxPollResult | null> {
|
||||
const tenantPrisma = forTenant(this.prisma, tenantId);
|
||||
const server = await tenantPrisma.proxmoxServer.findUnique({ where: { id: serverId } });
|
||||
if (!server || server.tenantId !== tenantId) {
|
||||
return null;
|
||||
}
|
||||
return this.pollOne(server);
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktive Server-IDs eines Mandanten fuer den Planer-Tick (gebunden).
|
||||
*/
|
||||
async listActiveServerIdsForTenant(tenantId: string): Promise<string[]> {
|
||||
const tenantPrisma = forTenant(this.prisma, tenantId);
|
||||
const rows = await tenantPrisma.proxmoxServer.findMany({
|
||||
where: { tenantId, isActive: true },
|
||||
select: { id: true },
|
||||
});
|
||||
return rows.map((r) => r.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Abfrageintervalle der aktiven Server eines Mandanten (gebunden) — der
|
||||
* Controller ruft dies nach jedem Anlegen/Speichern, um den Planer
|
||||
* sofort nachzuziehen (`ProxmoxSchedulerService.refreshTenant`).
|
||||
*/
|
||||
async loadActiveServersForTenantScheduling(
|
||||
tenantId: string,
|
||||
): Promise<{ pollIntervalMin: number }[]> {
|
||||
const tenantPrisma = forTenant(this.prisma, tenantId);
|
||||
return tenantPrisma.proxmoxServer.findMany({
|
||||
where: { tenantId, isActive: true },
|
||||
select: { pollIntervalMin: true },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Startpfad des Planers — der EINZIGE Systemkontext-Aufruf dieses Moduls
|
||||
* (`FORSYSTEM_ALLOWED_CALL_SITES`, `rls-access-inventory.spec.ts`, Aufgabe 4):
|
||||
* `const systemPrisma = forSystem(this.prisma);`, nur lesend, OHNE
|
||||
* `include` auf das Zwischenlager — die Zwischenlagertabelle hat bewusst
|
||||
* keine Systemlese-Regel, das Nachziehen laeuft je Zeile gebunden
|
||||
* (Muster `DkvSchedulerService`/`DashboardImagesService`, einmal lesen,
|
||||
* viele bedienen).
|
||||
*/
|
||||
async loadActiveServersForScheduler(): Promise<
|
||||
{ id: string; tenantId: string; pollIntervalMin: number }[]
|
||||
> {
|
||||
const systemPrisma = forSystem(this.prisma);
|
||||
return systemPrisma.proxmoxServer.findMany({
|
||||
where: { isActive: true },
|
||||
select: { id: true, tenantId: true, pollIntervalMin: true },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user