- 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
226 lines
7.1 KiB
TypeScript
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;
|
|
}
|
|
}
|
|
}
|