import { VspcListResponse, VspcOrganization, VspcBackupServer, VspcBackupJob, VspcBackupAgentJob, VspcProtectedWorkload, VspcRepository, VspcBackupAgent, VspcAlarm, } 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 { 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(url: string): Promise { 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 { 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(path: string, filter?: string): Promise { const allItems: T[] = []; let offset = 0; let total = 0; do { const params: Record = { offset, limit: this.DEFAULT_PAGE_SIZE, }; if (filter) { params.filter = filter; } const url = this.buildUrl(path, params); const response = await this.makeApiCall>(url); const pageData = response.data ?? []; if (pageData.length > 0) { allItems.push(...pageData); } total = response.meta?.pagingInfo?.total ?? 0; offset += this.DEFAULT_PAGE_SIZE; if (pageData.length > 0) { console.log(`Veeam VSPC: fetched ${allItems.length}/${total} from ${path}`); } // Safety: if page returned nothing and we haven't hit total, stop if (pageData.length === 0) break; } while (offset < total); return allItems; } // ============================================================================ // Data Fetching Methods // ============================================================================ /** * Fetch all organizations (tenants/companies) */ async getOrganizations(): Promise { console.log('Fetching Veeam VSPC organizations...'); const orgs = await this.fetchAllPages('/organizations'); console.log(`Fetched ${orgs.length} Veeam organizations`); return orgs; } /** * Fetch all backup servers */ async getBackupServers(): Promise { console.log('Fetching Veeam VSPC backup servers...'); const servers = await this.fetchAllPages('/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 { console.log('Fetching Veeam VSPC backup server jobs...'); const jobs = await this.fetchAllPages('/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 { console.log('Fetching Veeam VSPC backup agent jobs...'); const jobs = await this.fetchAllPages('/infrastructure/backupAgents/jobs', filter); console.log(`Fetched ${jobs.length} Veeam backup agent jobs`); return jobs; } /** * Fetch all protected workloads (virtual machines) */ async getProtectedWorkloads(): Promise { console.log('Fetching Veeam VSPC protected workloads...'); const workloads = await this.fetchAllPages('/protectedWorkloads/virtualMachines'); console.log(`Fetched ${workloads.length} Veeam protected workloads`); return workloads; } /** * Fetch all repositories */ async getRepositories(): Promise { console.log('Fetching Veeam VSPC repositories...'); const repos = await this.fetchAllPages('/infrastructure/backupServers/repositories'); console.log(`Fetched ${repos.length} Veeam repositories`); return repos; } /** * Fetch all backup agents (Veeam agent installs on managed machines) */ async getBackupAgents(): Promise { console.log('Fetching Veeam VSPC backup agents...'); const agents = await this.fetchAllPages('/infrastructure/backupAgents'); console.log(`Fetched ${agents.length} Veeam backup agents`); return agents; } /** * Fetch all active VSPC alarms */ async getActiveAlarms(): Promise { console.log('Fetching Veeam VSPC active alarms...'); const alarms = await this.fetchAllPages('/alarms/active'); console.log(`Fetched ${alarms.length} Veeam active alarms`); return alarms; } /** * Test API connectivity by fetching a single organization */ async testConnection(): Promise { try { const url = this.buildUrl('/organizations', { limit: 1 }); const response = await this.makeApiCall>(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; } } }