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:
3151
api/package-lock.json
generated
3151
api/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,38 @@ import { InfrastructureAdapter, NormalizedResource, ResourceSpec, NormalizedMetr
|
||||
import { ResourceProvider } from '../../types/resource.js'
|
||||
import { logger } from '../../lib/logger'
|
||||
|
||||
interface CloudflareZone {
|
||||
id: string
|
||||
name: string
|
||||
status: string
|
||||
created_on?: string
|
||||
modified_on?: string
|
||||
account?: {
|
||||
id?: string
|
||||
}
|
||||
name_servers?: string[]
|
||||
plan?: {
|
||||
name?: string
|
||||
}
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
interface CloudflareTunnel {
|
||||
id: string
|
||||
name: string
|
||||
status: string
|
||||
created_at?: string | number
|
||||
connections?: unknown[]
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
interface CloudflareAPIResponse<T> {
|
||||
result: T[]
|
||||
success: boolean
|
||||
errors?: unknown[]
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export class CloudflareAdapter implements InfrastructureAdapter {
|
||||
readonly provider: ResourceProvider = 'CLOUDFLARE'
|
||||
|
||||
@@ -58,27 +90,6 @@ export class CloudflareAdapter implements InfrastructureAdapter {
|
||||
return data.result || []
|
||||
}
|
||||
|
||||
interface CloudflareZone {
|
||||
id: string
|
||||
name: string
|
||||
status: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
interface CloudflareTunnel {
|
||||
id: string
|
||||
name: string
|
||||
status: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
interface CloudflareAPIResponse<T> {
|
||||
result: T[]
|
||||
success: boolean
|
||||
errors?: unknown[]
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
private async getZones(): Promise<CloudflareZone[]> {
|
||||
const response = await fetch('https://api.cloudflare.com/client/v4/zones', {
|
||||
method: 'GET',
|
||||
@@ -149,9 +160,9 @@ interface CloudflareAPIResponse<T> {
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
if (data.result) {
|
||||
return this.normalizeTunnel(data.result)
|
||||
const data = (await response.json()) as CloudflareAPIResponse<CloudflareTunnel>
|
||||
if (data.result && data.result.length > 0) {
|
||||
return this.normalizeTunnel(data.result[0])
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -166,9 +177,9 @@ interface CloudflareAPIResponse<T> {
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
if (data.result) {
|
||||
return this.normalizeZone(data.result)
|
||||
const data = (await response.json()) as CloudflareAPIResponse<CloudflareZone>
|
||||
if (data.result && data.result.length > 0) {
|
||||
return this.normalizeZone(data.result[0])
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -200,12 +211,15 @@ interface CloudflareAPIResponse<T> {
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
const error = (await response.json()) as { errors?: Array<{ message?: string }> }
|
||||
throw new Error(`Failed to create tunnel: ${error.errors?.[0]?.message || response.statusText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return this.normalizeTunnel(data.result)
|
||||
const data = (await response.json()) as CloudflareAPIResponse<CloudflareTunnel>
|
||||
if (!data.result || data.result.length === 0) {
|
||||
throw new Error('No tunnel result returned from API')
|
||||
}
|
||||
return this.normalizeTunnel(data.result[0])
|
||||
} else if (spec.type === 'dns_zone') {
|
||||
// Create DNS Zone
|
||||
const response = await fetch('https://api.cloudflare.com/client/v4/zones', {
|
||||
@@ -224,12 +238,15 @@ interface CloudflareAPIResponse<T> {
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
const error = (await response.json()) as { errors?: Array<{ message?: string }> }
|
||||
throw new Error(`Failed to create zone: ${error.errors?.[0]?.message || response.statusText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return this.normalizeZone(data.result)
|
||||
const data = (await response.json()) as CloudflareAPIResponse<CloudflareZone>
|
||||
if (!data.result || data.result.length === 0) {
|
||||
throw new Error('No zone result returned from API')
|
||||
}
|
||||
return this.normalizeZone(data.result[0])
|
||||
} else {
|
||||
throw new Error(`Unsupported resource type: ${spec.type}`)
|
||||
}
|
||||
@@ -262,12 +279,15 @@ interface CloudflareAPIResponse<T> {
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
const error = (await response.json()) as { errors?: Array<{ message?: string }> }
|
||||
throw new Error(`Failed to update tunnel: ${error.errors?.[0]?.message || response.statusText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return this.normalizeTunnel(data.result)
|
||||
const data = (await response.json()) as CloudflareAPIResponse<CloudflareTunnel>
|
||||
if (!data.result || data.result.length === 0) {
|
||||
throw new Error('No tunnel result returned from API')
|
||||
}
|
||||
return this.normalizeTunnel(data.result[0])
|
||||
} else if (existing.type === 'dns_zone') {
|
||||
// Update DNS Zone
|
||||
const response = await fetch(`https://api.cloudflare.com/client/v4/zones/${providerId}`, {
|
||||
@@ -282,12 +302,15 @@ interface CloudflareAPIResponse<T> {
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
const error = (await response.json()) as { errors?: Array<{ message?: string }> }
|
||||
throw new Error(`Failed to update zone: ${error.errors?.[0]?.message || response.statusText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return this.normalizeZone(data.result)
|
||||
const data = (await response.json()) as CloudflareAPIResponse<CloudflareZone>
|
||||
if (!data.result || data.result.length === 0) {
|
||||
throw new Error('No zone result returned from API')
|
||||
}
|
||||
return this.normalizeZone(data.result[0])
|
||||
} else {
|
||||
throw new Error(`Unsupported resource type: ${existing.type}`)
|
||||
}
|
||||
@@ -355,7 +378,7 @@ interface CloudflareAPIResponse<T> {
|
||||
)
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
const data = (await response.json()) as { result?: { totals?: { requests?: { all?: number, cached?: number }, bandwidth?: { all?: number, cached?: number } } } }
|
||||
const result = data.result
|
||||
|
||||
// Network throughput
|
||||
@@ -407,7 +430,7 @@ interface CloudflareAPIResponse<T> {
|
||||
)
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
const data = (await response.json()) as { result?: unknown[] }
|
||||
const connections = data.result || []
|
||||
|
||||
metrics.push({
|
||||
@@ -449,9 +472,6 @@ interface CloudflareAPIResponse<T> {
|
||||
)
|
||||
|
||||
if (tunnelResponse.ok) {
|
||||
const tunnelData = await tunnelResponse.json()
|
||||
const tunnel = tunnelData.result
|
||||
|
||||
// Get DNS routes for this tunnel
|
||||
try {
|
||||
const routesResponse = await fetch(
|
||||
@@ -466,7 +486,7 @@ interface CloudflareAPIResponse<T> {
|
||||
)
|
||||
|
||||
if (routesResponse.ok) {
|
||||
const routesData = await routesResponse.json()
|
||||
const routesData = (await routesResponse.json()) as { result?: Array<{ zone_id?: string }> }
|
||||
const routes = routesData.result || []
|
||||
|
||||
for (const route of routes) {
|
||||
@@ -536,7 +556,7 @@ interface CloudflareAPIResponse<T> {
|
||||
)
|
||||
|
||||
if (routesResponse.ok) {
|
||||
const routesData = await routesResponse.json()
|
||||
const routesData = (await routesResponse.json()) as { result?: Array<{ zone_id?: string }> }
|
||||
const routes = routesData.result || []
|
||||
|
||||
for (const route of routes) {
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user