wulf-pulse/lib/services/veeam-client.ts
lorentz 5dc7a7e66b feat: Veeam VSPC backup integration - sync, compliance, UI
- Database: 7 Veeam tables + backup_type_udf column on configuration_items
- API Client: VSPC REST API v3 client with pagination, rate limiting, Bearer auth
- Sync Service: full/incremental sync for orgs, servers, repos, jobs, agent jobs, workloads
- Scheduler: veeam-incremental (30min) and veeam-full (daily 2AM) schedules
- Compliance Engine: cross-references Autotask config items vs Veeam workloads
- API Endpoints: backup-status, companies, workloads, jobs, repos, compliance, sync
- UI: Backup Status page with Overview + Contract Compliance tabs
- Navigation: added Backup Status link with HardDrive icon
- Docker: added VEEAM_VSPC_URL and VEEAM_VSPC_API_KEY env vars to compose
2026-02-11 21:04:28 -05:00

226 lines
7.1 KiB
TypeScript

import {
VspcListResponse,
VspcOrganization,
VspcBackupServer,
VspcBackupJob,
VspcBackupAgentJob,
VspcProtectedWorkload,
VspcRepository,
} from '@/lib/types/veeam';
export interface VeeamClientConfig {
baseUrl: string;
apiKey: string;
}
export class VeeamClient {
private config: VeeamClientConfig;
private requestTimestamps: number[] = [];
private readonly RATE_LIMIT = 100; // requests per minute
private readonly RATE_LIMIT_WINDOW = 60000; // 1 minute in milliseconds
private readonly DEFAULT_PAGE_SIZE = 500;
constructor(config: VeeamClientConfig) {
this.config = config;
}
/**
* Get authorization headers for VSPC API
*/
private getAuthHeaders(): HeadersInit {
return {
'Authorization': `Bearer ${this.config.apiKey}`,
'Accept': 'application/json',
'Content-Type': 'application/json',
};
}
/**
* Check and enforce rate limiting
*/
private async checkRateLimit(): Promise<void> {
const now = Date.now();
this.requestTimestamps = this.requestTimestamps.filter(
(timestamp) => now - timestamp < this.RATE_LIMIT_WINDOW
);
if (this.requestTimestamps.length >= this.RATE_LIMIT) {
const oldestInWindow = this.requestTimestamps[0];
const waitMs = this.RATE_LIMIT_WINDOW - (now - oldestInWindow) + 100;
console.warn(`Veeam VSPC rate limit reached (${this.RATE_LIMIT}/min), waiting ${waitMs}ms`);
await new Promise(resolve => setTimeout(resolve, waitMs));
}
this.requestTimestamps.push(Date.now());
}
/**
* Make a single API call with error handling and rate limiting
*/
private async makeApiCall<T>(url: string): Promise<T> {
await this.checkRateLimit();
try {
const response = await fetch(url, {
method: 'GET',
headers: this.getAuthHeaders(),
// VSPC may use self-signed certs
...(process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0' ? {} : {}),
});
if (!response.ok) {
const errorText = await response.text();
let errorDetail = errorText;
try {
const errorJson = JSON.parse(errorText);
errorDetail = JSON.stringify(errorJson.errors || errorJson, null, 2);
} catch {
// keep raw text
}
console.error(`Veeam VSPC API error: ${response.status} ${response.statusText}`, errorDetail);
throw new Error(`Veeam VSPC API request failed: ${response.status} ${response.statusText} - ${errorDetail}`);
}
const text = await response.text();
if (!text) {
return { meta: { pagingInfo: { total: 0, count: 0, offset: 0 } }, data: [] } as T;
}
return JSON.parse(text) as T;
} catch (error) {
if (error instanceof Error && error.message.startsWith('Veeam VSPC API request failed')) {
throw error;
}
console.error('Veeam VSPC API call failed:', error);
throw error;
}
}
/**
* Build a URL with query parameters
*/
private buildUrl(path: string, params: Record<string, string | number> = {}): string {
const baseUrl = this.config.baseUrl.replace(/\/$/, '');
const fullUrl = `${baseUrl}/api/v3${path}`;
const url = new URL(fullUrl);
for (const [key, value] of Object.entries(params)) {
url.searchParams.set(key, String(value));
}
return url.toString();
}
/**
* Fetch all pages of a paginated VSPC API endpoint
*/
private async fetchAllPages<T>(path: string, filter?: string): Promise<T[]> {
const allItems: T[] = [];
let offset = 0;
let total = 0;
do {
const params: Record<string, string | number> = {
offset,
limit: this.DEFAULT_PAGE_SIZE,
};
if (filter) {
params.filter = filter;
}
const url = this.buildUrl(path, params);
const response = await this.makeApiCall<VspcListResponse<T>>(url);
if (response.data && response.data.length > 0) {
allItems.push(...response.data);
}
total = response.meta?.pagingInfo?.total ?? 0;
offset += this.DEFAULT_PAGE_SIZE;
if (response.data.length > 0) {
console.log(`Veeam VSPC: fetched ${allItems.length}/${total} from ${path}`);
}
} while (offset < total);
return allItems;
}
// ============================================================================
// Data Fetching Methods
// ============================================================================
/**
* Fetch all organizations (tenants/companies)
*/
async getOrganizations(): Promise<VspcOrganization[]> {
console.log('Fetching Veeam VSPC organizations...');
const orgs = await this.fetchAllPages<VspcOrganization>('/organizations');
console.log(`Fetched ${orgs.length} Veeam organizations`);
return orgs;
}
/**
* Fetch all backup servers
*/
async getBackupServers(): Promise<VspcBackupServer[]> {
console.log('Fetching Veeam VSPC backup servers...');
const servers = await this.fetchAllPages<VspcBackupServer>('/infrastructure/backupServers');
console.log(`Fetched ${servers.length} Veeam backup servers`);
return servers;
}
/**
* Fetch all backup server jobs (VM-level backup jobs)
*/
async getBackupJobs(filter?: string): Promise<VspcBackupJob[]> {
console.log('Fetching Veeam VSPC backup server jobs...');
const jobs = await this.fetchAllPages<VspcBackupJob>('/infrastructure/backupServers/jobs', filter);
console.log(`Fetched ${jobs.length} Veeam backup server jobs`);
return jobs;
}
/**
* Fetch all backup agent jobs (workstation/physical server backup jobs)
*/
async getBackupAgentJobs(filter?: string): Promise<VspcBackupAgentJob[]> {
console.log('Fetching Veeam VSPC backup agent jobs...');
const jobs = await this.fetchAllPages<VspcBackupAgentJob>('/infrastructure/backupAgents/jobs', filter);
console.log(`Fetched ${jobs.length} Veeam backup agent jobs`);
return jobs;
}
/**
* Fetch all protected workloads (virtual machines)
*/
async getProtectedWorkloads(): Promise<VspcProtectedWorkload[]> {
console.log('Fetching Veeam VSPC protected workloads...');
const workloads = await this.fetchAllPages<VspcProtectedWorkload>('/protectedWorkloads/virtualMachines');
console.log(`Fetched ${workloads.length} Veeam protected workloads`);
return workloads;
}
/**
* Fetch all repositories
*/
async getRepositories(): Promise<VspcRepository[]> {
console.log('Fetching Veeam VSPC repositories...');
const repos = await this.fetchAllPages<VspcRepository>('/infrastructure/backupServers/repositories');
console.log(`Fetched ${repos.length} Veeam repositories`);
return repos;
}
/**
* Test API connectivity by fetching a single organization
*/
async testConnection(): Promise<boolean> {
try {
const url = this.buildUrl('/organizations', { limit: 1 });
const response = await this.makeApiCall<VspcListResponse<VspcOrganization>>(url);
const total = response.meta?.pagingInfo?.total ?? 0;
console.log(`Veeam VSPC connection test successful: ${total} organizations available`);
return true;
} catch (error) {
console.error('Veeam VSPC connection test failed:', error);
return false;
}
}
}