Apply Composer changes: comprehensive API updates, migrations, middleware, and infrastructure improvements
- Add comprehensive database migrations (001-024) for schema evolution - Enhance API schema with expanded type definitions and resolvers - Add new middleware: audit logging, rate limiting, MFA enforcement, security, tenant auth - Implement new services: AI optimization, billing, blockchain, compliance, marketplace - Add adapter layer for cloud integrations (Cloudflare, Kubernetes, Proxmox, storage) - Update Crossplane provider with enhanced VM management capabilities - Add comprehensive test suite for API endpoints and services - Update frontend components with improved GraphQL subscriptions and real-time updates - Enhance security configurations and headers (CSP, CORS, etc.) - Update documentation and configuration files - Add new CI/CD workflows and validation scripts - Implement design system improvements and UI enhancements
This commit is contained in:
347
api/src/adapters/proxmox/adapter.ts
Normal file
347
api/src/adapters/proxmox/adapter.ts
Normal file
@@ -0,0 +1,347 @@
|
||||
/**
|
||||
* 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<NormalizedResource[]> {
|
||||
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<any[]> {
|
||||
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<any[]> {
|
||||
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<NormalizedResource | null> {
|
||||
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<NormalizedResource> {
|
||||
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<NormalizedResource>
|
||||
} catch (error) {
|
||||
logger.error('Error creating Proxmox resource', { error })
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async updateResource(providerId: string, spec: Partial<ResourceSpec>): Promise<NormalizedResource> {
|
||||
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<NormalizedResource>
|
||||
}
|
||||
|
||||
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<NormalizedResource>
|
||||
} catch (error) {
|
||||
logger.error(`Error updating Proxmox resource ${providerId}`, { error, providerId })
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async deleteResource(providerId: string): Promise<boolean> {
|
||||
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<NormalizedMetrics[]> {
|
||||
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<NormalizedRelationship[]> {
|
||||
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<HealthStatus> {
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user