Update .gitignore, remove package-lock.json, and enhance Cloudflare and Proxmox adapters

- Added lock file exclusions for pnpm in .gitignore.
- Removed obsolete package-lock.json from the api and portal directories.
- Enhanced Cloudflare adapter with additional interfaces for zones and tunnels.
- Improved Proxmox adapter error handling and logging for API requests.
- Updated Proxmox VM parameters with validation rules in the API schema.
- Enhanced documentation for Proxmox VM specifications and examples.
This commit is contained in:
defiQUG
2025-12-12 19:29:01 -08:00
parent 9daf1fd378
commit 7cd7022f6e
66 changed files with 5892 additions and 14502 deletions

View File

@@ -43,61 +43,129 @@ export class ProxmoxAdapter implements InfrastructureAdapter {
}
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}`, {
const response = await fetch(`${this.apiUrl}/api2/json/nodes`, {
method: 'GET',
headers: {
'Authorization': `PVEAPIToken=${this.apiToken}`,
'Authorization': `PVEAPIToken ${this.apiToken}`, // Note: space after PVEAPIToken for Proxmox API
'Content-Type': 'application/json',
},
})
if (!response.ok) {
if (response.status === 404) return null
throw new Error(`Proxmox API error: ${response.status} ${response.statusText}`)
const errorBody = await response.text().catch(() => '')
logger.error('Failed to get Proxmox nodes', {
status: response.status,
statusText: response.statusText,
body: errorBody,
url: `${this.apiUrl}/api2/json/nodes`,
})
throw new Error(`Proxmox API error: ${response.status} ${response.statusText} - ${errorBody}`)
}
const data = await response.json()
if (!data.data) return null
if (!data || !Array.isArray(data.data)) {
logger.warn('Unexpected response format from Proxmox nodes API', { data })
return []
}
return data.data
} catch (error) {
logger.error('Error getting Proxmox nodes', { error, apiUrl: this.apiUrl })
throw error
}
}
private async getVMs(node: string): Promise<any[]> {
if (!node || typeof node !== 'string') {
throw new Error(`Invalid node name: ${node}`)
}
try {
const url = `${this.apiUrl}/api2/json/nodes/${encodeURIComponent(node)}/qemu`
const response = await fetch(url, {
method: 'GET',
headers: {
'Authorization': `PVEAPIToken ${this.apiToken}`, // Note: space after PVEAPIToken for Proxmox API
'Content-Type': 'application/json',
},
})
if (!response.ok) {
const errorBody = await response.text().catch(() => '')
logger.error('Failed to get VMs from Proxmox node', {
node,
status: response.status,
statusText: response.statusText,
body: errorBody,
url,
})
throw new Error(`Proxmox API error getting VMs from node ${node}: ${response.status} ${response.statusText}`)
}
const data = await response.json()
if (!data || !Array.isArray(data.data)) {
logger.warn('Unexpected response format from Proxmox VMs API', { node, data })
return []
}
return data.data
} catch (error) {
logger.error('Error getting VMs from Proxmox node', { error, node, apiUrl: this.apiUrl })
throw error
}
}
async getResource(providerId: string): Promise<NormalizedResource | null> {
if (!providerId || typeof providerId !== 'string') {
logger.warn('Invalid providerId provided to getResource', { providerId })
return null
}
try {
const [node, vmid] = providerId.split(':')
if (!node || !vmid) {
logger.warn('Invalid providerId format, expected "node:vmid"', { providerId })
return null
}
// Validate vmid is numeric
const vmidNum = parseInt(vmid, 10)
if (isNaN(vmidNum) || vmidNum < 100 || vmidNum > 999999999) {
logger.warn('Invalid VMID in providerId', { providerId, vmid, vmidNum })
return null
}
const url = `${this.apiUrl}/api2/json/nodes/${encodeURIComponent(node)}/qemu/${encodeURIComponent(vmid)}`
const response = await fetch(url, {
method: 'GET',
headers: {
'Authorization': `PVEAPIToken ${this.apiToken}`, // Note: space after PVEAPIToken for Proxmox API
'Content-Type': 'application/json',
},
})
if (!response.ok) {
if (response.status === 404) {
logger.debug('VM not found', { providerId, node, vmid })
return null
}
const errorBody = await response.text().catch(() => '')
logger.error('Failed to get Proxmox resource', {
providerId,
node,
vmid,
status: response.status,
statusText: response.statusText,
body: errorBody,
url,
})
throw new Error(`Proxmox API error getting resource ${providerId}: ${response.status} ${response.statusText}`)
}
const data = await response.json()
if (!data || !data.data) {
logger.warn('Empty response from Proxmox API', { providerId, data })
return null
}
return this.normalizeVM(data.data, node)
} catch (error) {
@@ -107,40 +175,89 @@ export class ProxmoxAdapter implements InfrastructureAdapter {
}
async createResource(spec: ResourceSpec): Promise<NormalizedResource> {
if (!spec || !spec.name) {
throw new Error('Invalid resource spec: name is required')
}
try {
const [node] = await this.getNodes()
if (!node) {
const nodes = await this.getNodes()
if (!nodes || nodes.length === 0) {
throw new Error('No Proxmox nodes available')
}
// Find first online node, or use first node if status unknown
const node = nodes.find((n: any) => n.status === 'online') || nodes[0]
if (!node || !node.node) {
throw new Error('No valid Proxmox node found')
}
const targetNode = node.node
// Validate config
if (spec.config?.vmid && (spec.config.vmid < 100 || spec.config.vmid > 999999999)) {
throw new Error(`Invalid VMID: ${spec.config.vmid} (must be between 100 and 999999999)`)
}
const config: any = {
vmid: spec.config.vmid || undefined, // Auto-assign if not specified
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',
cores: spec.config?.cores || 2,
memory: spec.config?.memory || 2048,
net0: spec.config?.net0 || 'virtio,bridge=vmbr0',
ostype: spec.config?.ostype || 'l26',
}
// Validate memory is positive
if (config.memory <= 0) {
throw new Error(`Invalid memory value: ${config.memory} (must be positive)`)
}
// Create VM
const response = await fetch(`${this.apiUrl}/api2/json/nodes/${node.node}/qemu`, {
const url = `${this.apiUrl}/api2/json/nodes/${encodeURIComponent(targetNode)}/qemu`
const response = await fetch(url, {
method: 'POST',
headers: {
'Authorization': `PVEAPIToken=${this.apiToken}`,
'Authorization': `PVEAPIToken ${this.apiToken}`, // Note: space after PVEAPIToken for Proxmox API
'Content-Type': 'application/json',
},
body: JSON.stringify(config),
})
if (!response.ok) {
throw new Error(`Failed to create VM: ${response.statusText}`)
const errorBody = await response.text().catch(() => '')
logger.error('Failed to create Proxmox VM', {
spec,
node: targetNode,
status: response.status,
statusText: response.statusText,
body: errorBody,
url,
})
throw new Error(`Failed to create VM: ${response.status} ${response.statusText} - ${errorBody}`)
}
const data = await response.json()
// VMID can be returned as string or number from Proxmox API
const vmid = data.data || config.vmid
// Get created VM
return this.getResource(`${node.node}:${vmid}`) as Promise<NormalizedResource>
if (!vmid) {
throw new Error('VM creation succeeded but no VMID returned')
}
const vmidStr = String(vmid) // Ensure it's a string for providerId format
// Get created VM with retry logic (VM may not be immediately available)
let retries = 3
while (retries > 0) {
const vm = await this.getResource(`${targetNode}:${vmidStr}`)
if (vm) {
return vm
}
await new Promise(resolve => setTimeout(resolve, 1000)) // Wait 1 second
retries--
}
throw new Error(`VM ${vmidStr} created but not found after retries`)
} catch (error) {
logger.error('Error creating Proxmox resource', { error })
throw error
@@ -148,31 +265,64 @@ export class ProxmoxAdapter implements InfrastructureAdapter {
}
async updateResource(providerId: string, spec: Partial<ResourceSpec>): Promise<NormalizedResource> {
if (!providerId || typeof providerId !== 'string') {
throw new Error(`Invalid providerId: ${providerId}`)
}
try {
const [node, vmid] = providerId.split(':')
if (!node || !vmid) {
throw new Error('Invalid provider ID format')
throw new Error(`Invalid provider ID format, expected "node:vmid", got: ${providerId}`)
}
// Validate vmid is numeric
const vmidNum = parseInt(vmid, 10)
if (isNaN(vmidNum) || vmidNum < 100 || vmidNum > 999999999) {
throw new Error(`Invalid VMID in providerId: ${vmid}`)
}
const updates: any = {}
if (spec.config?.cores) updates.cores = spec.config.cores
if (spec.config?.memory) updates.memory = spec.config.memory
if (spec.config?.cores !== undefined) {
if (spec.config.cores < 1) {
throw new Error(`Invalid CPU cores: ${spec.config.cores} (must be at least 1)`)
}
updates.cores = spec.config.cores
}
if (spec.config?.memory !== undefined) {
if (spec.config.memory <= 0) {
throw new Error(`Invalid memory: ${spec.config.memory} (must be positive)`)
}
updates.memory = spec.config.memory
}
if (Object.keys(updates).length === 0) {
logger.debug('No updates to apply', { providerId })
return this.getResource(providerId) as Promise<NormalizedResource>
}
const response = await fetch(`${this.apiUrl}/api2/json/nodes/${node}/qemu/${vmid}/config`, {
const url = `${this.apiUrl}/api2/json/nodes/${encodeURIComponent(node)}/qemu/${encodeURIComponent(vmid)}/config`
const response = await fetch(url, {
method: 'PUT',
headers: {
'Authorization': `PVEAPIToken=${this.apiToken}`,
'Authorization': `PVEAPIToken ${this.apiToken}`, // Note: space after PVEAPIToken for Proxmox API
'Content-Type': 'application/json',
},
body: JSON.stringify(updates),
})
if (!response.ok) {
throw new Error(`Failed to update VM: ${response.statusText}`)
const errorBody = await response.text().catch(() => '')
logger.error('Failed to update Proxmox VM', {
providerId,
node,
vmid,
updates,
status: response.status,
statusText: response.statusText,
body: errorBody,
url,
})
throw new Error(`Failed to update VM ${providerId}: ${response.status} ${response.statusText} - ${errorBody}`)
}
return this.getResource(providerId) as Promise<NormalizedResource>
@@ -183,21 +333,49 @@ export class ProxmoxAdapter implements InfrastructureAdapter {
}
async deleteResource(providerId: string): Promise<boolean> {
if (!providerId || typeof providerId !== 'string') {
logger.warn('Invalid providerId provided to deleteResource', { providerId })
return false
}
try {
const [node, vmid] = providerId.split(':')
if (!node || !vmid) {
throw new Error('Invalid provider ID format')
logger.warn('Invalid provider ID format, expected "node:vmid"', { providerId })
return false
}
const response = await fetch(`${this.apiUrl}/api2/json/nodes/${node}/qemu/${vmid}`, {
// Validate vmid is numeric
const vmidNum = parseInt(vmid, 10)
if (isNaN(vmidNum) || vmidNum < 100 || vmidNum > 999999999) {
logger.warn('Invalid VMID in providerId', { providerId, vmid })
return false
}
const url = `${this.apiUrl}/api2/json/nodes/${encodeURIComponent(node)}/qemu/${encodeURIComponent(vmid)}`
const response = await fetch(url, {
method: 'DELETE',
headers: {
'Authorization': `PVEAPIToken=${this.apiToken}`,
'Authorization': `PVEAPIToken ${this.apiToken}`, // Note: space after PVEAPIToken for Proxmox API
'Content-Type': 'application/json',
},
})
return response.ok
if (!response.ok) {
const errorBody = await response.text().catch(() => '')
logger.error('Failed to delete Proxmox VM', {
providerId,
node,
vmid,
status: response.status,
statusText: response.statusText,
body: errorBody,
url,
})
return false
}
return true
} catch (error) {
logger.error(`Error deleting Proxmox resource ${providerId}`, { error, providerId })
return false
@@ -214,7 +392,7 @@ export class ProxmoxAdapter implements InfrastructureAdapter {
{
method: 'GET',
headers: {
'Authorization': `PVEAPIToken=${this.apiToken}`,
'Authorization': `PVEAPIToken ${this.apiToken}`, // Note: space after PVEAPIToken for Proxmox API
'Content-Type': 'application/json',
},
}
@@ -292,7 +470,7 @@ export class ProxmoxAdapter implements InfrastructureAdapter {
const response = await fetch(`${this.apiUrl}/api2/json/version`, {
method: 'GET',
headers: {
'Authorization': `PVEAPIToken=${this.apiToken}`,
'Authorization': `PVEAPIToken ${this.apiToken}`, // Note: space after PVEAPIToken for Proxmox API
'Content-Type': 'application/json',
},
})