/** * Proxmox VE Adapter * Implements the InfrastructureAdapter interface for Proxmox */ import { InfrastructureAdapter, NormalizedResource, ResourceSpec, NormalizedMetrics, TimeRange, HealthStatus, NormalizedRelationship } from '../types.js' import { ResourceProvider } from '../../types/resource.js' import { logger } from '../../lib/logger.js' import type { ProxmoxCluster, ProxmoxVM, ProxmoxVMConfig } from './types.js' export class ProxmoxAdapter implements InfrastructureAdapter { readonly provider: ResourceProvider = 'PROXMOX' private apiUrl: string private apiToken: string constructor(config: { apiUrl: string; apiToken: string }) { this.apiUrl = config.apiUrl this.apiToken = config.apiToken } async discoverResources(): Promise { try { const nodes = await this.getNodes() const allResources: NormalizedResource[] = [] for (const node of nodes) { try { const vms = await this.getVMs(node.node) for (const vm of vms) { allResources.push(this.normalizeVM(vm, node.node)) } } catch (error) { logger.error(`Error discovering VMs on node ${node.node}`, { error, node: node.node }) } } return allResources } catch (error) { logger.error('Error discovering Proxmox resources', { error }) throw error } } private async getNodes(): Promise { const response = await fetch(`${this.apiUrl}/api2/json/nodes`, { method: 'GET', headers: { 'Authorization': `PVEAPIToken=${this.apiToken}`, 'Content-Type': 'application/json', }, }) if (!response.ok) { throw new Error(`Proxmox API error: ${response.status} ${response.statusText}`) } const data = await response.json() return data.data || [] } private async getVMs(node: string): Promise { const response = await fetch(`${this.apiUrl}/api2/json/nodes/${node}/qemu`, { method: 'GET', headers: { 'Authorization': `PVEAPIToken=${this.apiToken}`, 'Content-Type': 'application/json', }, }) if (!response.ok) { throw new Error(`Proxmox API error: ${response.status} ${response.statusText}`) } const data = await response.json() return data.data || [] } async getResource(providerId: string): Promise { try { const [node, vmid] = providerId.split(':') if (!node || !vmid) { return null } const response = await fetch(`${this.apiUrl}/api2/json/nodes/${node}/qemu/${vmid}`, { method: 'GET', headers: { 'Authorization': `PVEAPIToken=${this.apiToken}`, 'Content-Type': 'application/json', }, }) if (!response.ok) { if (response.status === 404) return null throw new Error(`Proxmox API error: ${response.status} ${response.statusText}`) } const data = await response.json() if (!data.data) return null return this.normalizeVM(data.data, node) } catch (error) { logger.error(`Error getting Proxmox resource ${providerId}`, { error, providerId }) return null } } async createResource(spec: ResourceSpec): Promise { try { const [node] = await this.getNodes() if (!node) { throw new Error('No Proxmox nodes available') } const config: any = { vmid: spec.config.vmid || undefined, // Auto-assign if not specified name: spec.name, cores: spec.config.cores || 2, memory: spec.config.memory || 2048, net0: spec.config.net0 || 'virtio,bridge=vmbr0', ostype: spec.config.ostype || 'l26', } // Create VM const response = await fetch(`${this.apiUrl}/api2/json/nodes/${node.node}/qemu`, { method: 'POST', headers: { 'Authorization': `PVEAPIToken=${this.apiToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify(config), }) if (!response.ok) { throw new Error(`Failed to create VM: ${response.statusText}`) } const data = await response.json() const vmid = data.data || config.vmid // Get created VM return this.getResource(`${node.node}:${vmid}`) as Promise } catch (error) { logger.error('Error creating Proxmox resource', { error }) throw error } } async updateResource(providerId: string, spec: Partial): Promise { try { const [node, vmid] = providerId.split(':') if (!node || !vmid) { throw new Error('Invalid provider ID format') } const updates: any = {} if (spec.config?.cores) updates.cores = spec.config.cores if (spec.config?.memory) updates.memory = spec.config.memory if (Object.keys(updates).length === 0) { return this.getResource(providerId) as Promise } const response = await fetch(`${this.apiUrl}/api2/json/nodes/${node}/qemu/${vmid}/config`, { method: 'PUT', headers: { 'Authorization': `PVEAPIToken=${this.apiToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify(updates), }) if (!response.ok) { throw new Error(`Failed to update VM: ${response.statusText}`) } return this.getResource(providerId) as Promise } catch (error) { logger.error(`Error updating Proxmox resource ${providerId}`, { error, providerId }) throw error } } async deleteResource(providerId: string): Promise { try { const [node, vmid] = providerId.split(':') if (!node || !vmid) { throw new Error('Invalid provider ID format') } const response = await fetch(`${this.apiUrl}/api2/json/nodes/${node}/qemu/${vmid}`, { method: 'DELETE', headers: { 'Authorization': `PVEAPIToken=${this.apiToken}`, 'Content-Type': 'application/json', }, }) return response.ok } catch (error) { logger.error(`Error deleting Proxmox resource ${providerId}`, { error, providerId }) return false } } async getMetrics(providerId: string, timeRange: TimeRange): Promise { try { const [node, vmid] = providerId.split(':') if (!node || !vmid) return [] const response = await fetch( `${this.apiUrl}/api2/json/nodes/${node}/qemu/${vmid}/rrddata?timeframe=hour&cf=AVERAGE`, { method: 'GET', headers: { 'Authorization': `PVEAPIToken=${this.apiToken}`, 'Content-Type': 'application/json', }, } ) if (!response.ok) return [] const data = await response.json() const metrics: NormalizedMetrics[] = [] if (data.data && Array.isArray(data.data)) { for (const point of data.data) { if (point.cpu) { metrics.push({ resourceId: providerId, metricType: 'CPU_USAGE', value: parseFloat(point.cpu) * 100, timestamp: new Date(point.time * 1000), }) } if (point.mem) { metrics.push({ resourceId: providerId, metricType: 'MEMORY_USAGE', value: parseFloat(point.mem), timestamp: new Date(point.time * 1000), }) } } } return metrics } catch (error) { logger.error(`Error getting Proxmox metrics for ${providerId}`, { error, providerId }) return [] } } async getRelationships(providerId: string): Promise { try { const [node, vmid] = providerId.split(':') if (!node || !vmid) return [] const vm = await this.getResource(providerId) if (!vm) return [] const relationships: NormalizedRelationship[] = [ { sourceId: providerId, targetId: `proxmox-node-${node}`, type: 'HOSTED_ON', metadata: { node }, }, ] // Add storage relationships if available if (vm.metadata?.storage) { relationships.push({ sourceId: providerId, targetId: `proxmox-storage-${vm.metadata.storage}`, type: 'USES_STORAGE', metadata: { storage: vm.metadata.storage }, }) } return relationships } catch (error) { logger.error(`Error getting Proxmox relationships for ${providerId}`, { error, providerId }) return [] } } async healthCheck(): Promise { try { const response = await fetch(`${this.apiUrl}/api2/json/version`, { method: 'GET', headers: { 'Authorization': `PVEAPIToken=${this.apiToken}`, 'Content-Type': 'application/json', }, }) if (response.ok) { return { status: 'healthy', lastChecked: new Date(), } } return { status: 'unhealthy', message: `API returned status ${response.status}`, lastChecked: new Date(), } } catch (error) { return { status: 'unhealthy', message: error instanceof Error ? error.message : 'Unknown error', lastChecked: new Date(), } } } /** * Helper method to normalize Proxmox VM to unified resource format */ private normalizeVM(vm: ProxmoxVM, node: string): NormalizedResource { return { id: `proxmox-${node}-${vm.vmid}`, name: vm.name || `VM ${vm.vmid}`, type: 'virtual_machine', provider: 'PROXMOX', providerId: `${node}:${vm.vmid}`, providerResourceId: `proxmox://${node}/vm/${vm.vmid}`, status: vm.status, metadata: { node, vmid: vm.vmid, cpu: vm.cpu, memory: vm.mem, disk: vm.disk, uptime: vm.uptime, }, tags: [], createdAt: new Date(Date.now() - vm.uptime * 1000), updatedAt: new Date(), } } }