452 lines
14 KiB
TypeScript
452 lines
14 KiB
TypeScript
import {
|
||
mockAds,
|
||
mockBvAttachments,
|
||
mockConsoleConns,
|
||
mockImages,
|
||
mockInstances,
|
||
mockShapes,
|
||
mockVolAttachments,
|
||
mockVolumes,
|
||
} from './mock'
|
||
import { reactive } from 'vue'
|
||
|
||
import { ApiError, mockOn, mocked, request } from './request'
|
||
import type {
|
||
AttachVnicRequest,
|
||
BootVolumeAttachment,
|
||
ComputeShape,
|
||
ConsoleConnection,
|
||
CreateInstanceRequest,
|
||
CreateInstancesResponse,
|
||
ImageInfo,
|
||
Instance,
|
||
PowerAction,
|
||
UpdateInstanceRequest,
|
||
Vnic,
|
||
Volume,
|
||
VolumeAttachment,
|
||
} from '@/types/api'
|
||
|
||
// ---- 创建实例前置查询 ----
|
||
export interface ImagesQuery {
|
||
region?: string
|
||
operatingSystem?: string
|
||
shape?: string
|
||
}
|
||
|
||
export function listImages(cfgId: number, query: ImagesQuery = {}): Promise<ImageInfo[]> {
|
||
if (mockOn) return mocked(mockImages)
|
||
return request(`/oci-configs/${cfgId}/images`, { query: { ...query } })
|
||
}
|
||
|
||
export function getImage(cfgId: number, imageId: string, region?: string): Promise<ImageInfo> {
|
||
if (mockOn) {
|
||
const found = mockImages.find((i) => i.id === imageId) ?? {
|
||
...mockImages[0],
|
||
id: imageId,
|
||
displayName: 'Canonical-Ubuntu-24.04-aarch64-2026.05.20-0',
|
||
}
|
||
return mocked(found)
|
||
}
|
||
return request(`/oci-configs/${cfgId}/images/${encodeURIComponent(imageId)}`, {
|
||
query: { region },
|
||
})
|
||
}
|
||
|
||
export function listAvailabilityDomains(cfgId: number, region?: string): Promise<string[]> {
|
||
if (mockOn) return mocked(mockAds)
|
||
return request(`/oci-configs/${cfgId}/availability-domains`, { query: { region } })
|
||
}
|
||
|
||
export function listShapes(
|
||
cfgId: number,
|
||
region?: string,
|
||
availabilityDomain?: string,
|
||
): Promise<ComputeShape[]> {
|
||
if (mockOn) return mocked(mockShapes)
|
||
return request(`/oci-configs/${cfgId}/shapes`, { query: { region, availabilityDomain } })
|
||
}
|
||
|
||
export function listVolumes(
|
||
cfgId: number,
|
||
availabilityDomain?: string,
|
||
compartmentId?: string,
|
||
region?: string,
|
||
): Promise<Volume[]> {
|
||
if (mockOn) return mocked(mockVolumes[cfgId] ?? [])
|
||
return request(`/oci-configs/${cfgId}/volumes`, {
|
||
query: { availabilityDomain, compartmentId, region },
|
||
})
|
||
}
|
||
|
||
export function listInstances(
|
||
cfgId: number,
|
||
region?: string,
|
||
compartmentId?: string,
|
||
): Promise<Instance[]> {
|
||
if (mockOn) return mocked(compartmentId ? [] : (mockInstances[cfgId] ?? []))
|
||
return request(`/oci-configs/${cfgId}/instances`, { query: { region, compartmentId } })
|
||
}
|
||
|
||
export function getInstance(cfgId: number, instanceId: string, region?: string): Promise<Instance> {
|
||
if (mockOn) {
|
||
const all = Object.values(mockInstances).flat()
|
||
const found = all.find((i) => i.id === instanceId)
|
||
return found ? mocked(found) : Promise.reject(new Error('实例不存在'))
|
||
}
|
||
return request(`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}`, {
|
||
query: { region },
|
||
})
|
||
}
|
||
|
||
export function createInstances(
|
||
cfgId: number,
|
||
body: CreateInstanceRequest,
|
||
): Promise<CreateInstancesResponse> {
|
||
if (mockOn) return mocked({ instances: [mockInstances[1][0]], errors: [] }, 1200)
|
||
return request(`/oci-configs/${cfgId}/instances`, { method: 'POST', body })
|
||
}
|
||
|
||
export function updateInstance(
|
||
cfgId: number,
|
||
instanceId: string,
|
||
body: UpdateInstanceRequest,
|
||
region?: string,
|
||
): Promise<Instance> {
|
||
if (mockOn) return mocked({ ...mockInstances[1][0], ...body })
|
||
return request(`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}`, {
|
||
method: 'PUT',
|
||
body: { region: region ?? '', ...body },
|
||
})
|
||
}
|
||
|
||
/** 终止请求飞行中的实例(instance OCID):模块级响应式集合,列表页、
|
||
* 详情页与组件重挂载共用一把锁,防「关闭重开对话框后用相反的
|
||
* preserveBootVolume 并发提交」;跨浏览器页签由服务端同键 guard 兜底 */
|
||
export const terminatingInstances = reactive(new Set<string>())
|
||
|
||
/** Instance OCID 唯一标识物理实例;cfgId 不得拆成两把终止锁 */
|
||
export function terminatingKey(_cfgId: number, instanceId: string): string {
|
||
return instanceId
|
||
}
|
||
|
||
export async function terminateInstance(
|
||
cfgId: number,
|
||
instanceId: string,
|
||
preserveBootVolume: boolean,
|
||
region?: string,
|
||
): Promise<void> {
|
||
const key = terminatingKey(cfgId, instanceId)
|
||
if (terminatingInstances.has(key)) {
|
||
throw new ApiError(409, '该实例的终止请求正在处理中')
|
||
}
|
||
terminatingInstances.add(key)
|
||
try {
|
||
if (mockOn) return await mocked(undefined, 600)
|
||
return await requestTerminate(cfgId, instanceId, preserveBootVolume, region)
|
||
} finally {
|
||
terminatingInstances.delete(key)
|
||
}
|
||
}
|
||
|
||
function requestTerminate(
|
||
cfgId: number,
|
||
instanceId: string,
|
||
preserveBootVolume: boolean,
|
||
region?: string,
|
||
): Promise<void> {
|
||
return request(`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}`, {
|
||
method: 'DELETE',
|
||
query: { preserveBootVolume, region },
|
||
})
|
||
}
|
||
|
||
export function instanceAction(
|
||
cfgId: number,
|
||
instanceId: string,
|
||
action: PowerAction,
|
||
region?: string,
|
||
): Promise<Instance> {
|
||
if (mockOn) {
|
||
const next = action === 'STOP' || action === 'SOFTSTOP' ? 'STOPPING' : 'STARTING'
|
||
return mocked({ ...mockInstances[1][0], lifecycleState: next }, 600)
|
||
}
|
||
return request(`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}/action`, {
|
||
method: 'POST',
|
||
body: { region: region ?? '', action },
|
||
})
|
||
}
|
||
|
||
export function changePublicIp(
|
||
cfgId: number,
|
||
instanceId: string,
|
||
region?: string,
|
||
): Promise<{ publicIp: string }> {
|
||
if (mockOn) return mocked({ publicIp: '130.61.99.201' }, 900)
|
||
return request(
|
||
`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}/change-public-ip`,
|
||
{ method: 'POST', body: { region: region ?? '' } },
|
||
)
|
||
}
|
||
|
||
export function addIpv6(
|
||
cfgId: number,
|
||
instanceId: string,
|
||
address = '',
|
||
region?: string,
|
||
): Promise<{ ipv6Address: string }> {
|
||
if (mockOn) return mocked({ ipv6Address: address || '2603:c020:400d:aa01::2' }, 700)
|
||
return request(`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}/ipv6-addresses`, {
|
||
method: 'POST',
|
||
body: { region: region ?? '', address },
|
||
})
|
||
}
|
||
|
||
export function removeIpv6(
|
||
cfgId: number,
|
||
instanceId: string,
|
||
address: string,
|
||
region?: string,
|
||
): Promise<void> {
|
||
if (mockOn) return mocked(undefined)
|
||
return request(`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}/ipv6-addresses`, {
|
||
method: 'DELETE',
|
||
query: { address, region },
|
||
})
|
||
}
|
||
|
||
// ---- VNIC 管理 ----
|
||
export function listVnics(cfgId: number, instanceId: string, region?: string): Promise<Vnic[]> {
|
||
if (mockOn)
|
||
return mocked([
|
||
{
|
||
attachmentId: 'ocid1.vnicattachment..a1',
|
||
vnicId: 'ocid1.vnic..v1',
|
||
displayName: instanceId.slice(-12),
|
||
isPrimary: true,
|
||
privateIp: '10.0.0.11',
|
||
publicIp: '141.147.60.10',
|
||
ipv6Addresses: ['2603:c020:400d:aa01::1'],
|
||
subnetId: 'ocid1.subnet..pub',
|
||
subnetName: 'public-subnet',
|
||
lifecycleState: 'ATTACHED',
|
||
timeCreated: '2026-05-20T14:32:00Z',
|
||
},
|
||
{
|
||
attachmentId: 'ocid1.vnicattachment..a2',
|
||
vnicId: 'ocid1.vnic..v2',
|
||
displayName: 'vnic-secondary',
|
||
isPrimary: false,
|
||
privateIp: '10.0.0.12',
|
||
publicIp: '',
|
||
ipv6Addresses: [],
|
||
subnetId: 'ocid1.subnet..pub',
|
||
subnetName: 'public-subnet',
|
||
lifecycleState: 'ATTACHED',
|
||
timeCreated: '2026-07-20T08:00:00Z',
|
||
},
|
||
])
|
||
return request(`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}/vnics`, {
|
||
query: { region },
|
||
})
|
||
}
|
||
|
||
export function attachVnic(
|
||
cfgId: number,
|
||
instanceId: string,
|
||
body: AttachVnicRequest,
|
||
): Promise<Vnic> {
|
||
if (mockOn) return Promise.reject(new Error('mock 环境不支持附加 VNIC'))
|
||
return request(`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}/vnics`, {
|
||
method: 'POST',
|
||
body: { ...body, region: body.region ?? '' },
|
||
})
|
||
}
|
||
|
||
export function detachVnic(cfgId: number, attachmentId: string, region?: string): Promise<void> {
|
||
if (mockOn) return mocked(undefined)
|
||
return request(`/oci-configs/${cfgId}/vnic-attachments/${encodeURIComponent(attachmentId)}`, {
|
||
method: 'DELETE',
|
||
query: { region },
|
||
})
|
||
}
|
||
|
||
/** 更换指定 VNIC 的临时公网 IP,返回新地址;绑定保留 IP 的网卡会被拒绝 */
|
||
export function changeVnicPublicIp(
|
||
cfgId: number,
|
||
vnicId: string,
|
||
region?: string,
|
||
): Promise<{ publicIp: string }> {
|
||
if (mockOn) return mocked({ publicIp: '130.61.99.7' }, 800)
|
||
return request(`/oci-configs/${cfgId}/vnics/${encodeURIComponent(vnicId)}/change-public-ip`, {
|
||
method: 'POST',
|
||
body: { region: region ?? '' },
|
||
})
|
||
}
|
||
|
||
/** 为指定 VNIC 添加 IPv6;address 留空自动分配。删除复用实例级 removeIpv6(按地址遍历全部网卡) */
|
||
export function addVnicIpv6(
|
||
cfgId: number,
|
||
vnicId: string,
|
||
address = '',
|
||
region?: string,
|
||
): Promise<{ address: string }> {
|
||
if (mockOn) return mocked({ address: address || '2603:c020:400d:aa01::3' }, 700)
|
||
return request(`/oci-configs/${cfgId}/vnics/${encodeURIComponent(vnicId)}/ipv6-addresses`, {
|
||
method: 'POST',
|
||
body: { region: region ?? '', address },
|
||
})
|
||
}
|
||
|
||
// ---- 网页控制台(串行 / VNC)----
|
||
export type ConsoleSessionType = 'serial' | 'vnc'
|
||
|
||
export function createConsoleSession(
|
||
cfgId: number,
|
||
instanceId: string,
|
||
type: ConsoleSessionType,
|
||
region?: string,
|
||
): Promise<{ sessionId: string; type: ConsoleSessionType }> {
|
||
if (mockOn) return Promise.reject(new Error('mock 环境不支持网页控制台'))
|
||
return request(
|
||
`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}/console-sessions`,
|
||
{ method: 'POST', body: { type, region: region ?? '' } },
|
||
)
|
||
}
|
||
|
||
export function deleteConsoleSession(sessionId: string): Promise<void> {
|
||
if (mockOn) return mocked(undefined)
|
||
return request(`/console-sessions/${encodeURIComponent(sessionId)}`, { method: 'DELETE' })
|
||
}
|
||
|
||
/** 控制台数据面 WS 地址:浏览器 WS 无法带自定义头,token 走 query */
|
||
export function consoleSessionWsUrl(sessionId: string, token: string, extra?: string): string {
|
||
const proto = location.protocol === 'https:' ? 'wss' : 'ws'
|
||
const q = extra ? `&${extra}` : ''
|
||
return `${proto}://${location.host}/api/v1/console-sessions/${encodeURIComponent(sessionId)}/ws?token=${encodeURIComponent(token)}${q}`
|
||
}
|
||
|
||
// ---- 控制台连接 ----
|
||
export function listConsoleConnections(
|
||
cfgId: number,
|
||
instanceId: string,
|
||
region?: string,
|
||
): Promise<ConsoleConnection[]> {
|
||
if (mockOn) return mocked(mockConsoleConns)
|
||
return request(
|
||
`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}/console-connections`,
|
||
{ query: { region } },
|
||
)
|
||
}
|
||
|
||
export function createConsoleConnection(
|
||
cfgId: number,
|
||
instanceId: string,
|
||
sshPublicKey: string,
|
||
region?: string,
|
||
): Promise<ConsoleConnection> {
|
||
if (mockOn) return mocked(mockConsoleConns[0], 1500)
|
||
return request(
|
||
`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}/console-connections`,
|
||
{ method: 'POST', body: { region: region ?? '', sshPublicKey } },
|
||
)
|
||
}
|
||
|
||
export function deleteConsoleConnection(
|
||
cfgId: number,
|
||
connectionId: string,
|
||
region?: string,
|
||
): Promise<void> {
|
||
if (mockOn) return mocked(undefined)
|
||
return request(`/oci-configs/${cfgId}/console-connections/${encodeURIComponent(connectionId)}`, {
|
||
method: 'DELETE',
|
||
query: { region },
|
||
})
|
||
}
|
||
|
||
// ---- 引导卷挂载 ----
|
||
export function listBootVolumeAttachments(
|
||
cfgId: number,
|
||
instanceId: string,
|
||
region?: string,
|
||
): Promise<BootVolumeAttachment[]> {
|
||
if (mockOn) return mocked(mockBvAttachments)
|
||
return request(
|
||
`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}/boot-volume-attachments`,
|
||
{ query: { region } },
|
||
)
|
||
}
|
||
|
||
export function attachBootVolume(
|
||
cfgId: number,
|
||
instanceId: string,
|
||
bootVolumeId: string,
|
||
region?: string,
|
||
): Promise<BootVolumeAttachment> {
|
||
if (mockOn) return mocked(mockBvAttachments[0], 1200)
|
||
return request(
|
||
`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}/boot-volume-attachments`,
|
||
{ method: 'POST', body: { region: region ?? '', bootVolumeId } },
|
||
)
|
||
}
|
||
|
||
export function replaceBootVolume(
|
||
cfgId: number,
|
||
instanceId: string,
|
||
bootVolumeId: string,
|
||
region?: string,
|
||
): Promise<BootVolumeAttachment> {
|
||
if (mockOn) return mocked(mockBvAttachments[0], 1500)
|
||
return request(
|
||
`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}/replace-boot-volume`,
|
||
{ method: 'POST', body: { region: region ?? '', bootVolumeId } },
|
||
)
|
||
}
|
||
|
||
export function detachBootVolume(
|
||
cfgId: number,
|
||
attachmentId: string,
|
||
region?: string,
|
||
): Promise<void> {
|
||
if (mockOn) return mocked(undefined)
|
||
return request(`/oci-configs/${cfgId}/boot-volume-attachments/${encodeURIComponent(attachmentId)}`, {
|
||
method: 'DELETE',
|
||
query: { region },
|
||
})
|
||
}
|
||
|
||
// ---- 块卷挂载 ----
|
||
export function listVolumeAttachments(
|
||
cfgId: number,
|
||
instanceId: string,
|
||
region?: string,
|
||
): Promise<VolumeAttachment[]> {
|
||
if (mockOn) return mocked(mockVolAttachments)
|
||
return request(
|
||
`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}/volume-attachments`,
|
||
{ query: { region } },
|
||
)
|
||
}
|
||
|
||
export function attachVolume(
|
||
cfgId: number,
|
||
instanceId: string,
|
||
volumeId: string,
|
||
readOnly = false,
|
||
region?: string,
|
||
): Promise<VolumeAttachment> {
|
||
if (mockOn) return mocked(mockVolAttachments[0], 1000)
|
||
return request(`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}/volume-attachments`, {
|
||
method: 'POST',
|
||
body: { region: region ?? '', volumeId, readOnly },
|
||
})
|
||
}
|
||
|
||
export function detachVolume(cfgId: number, attachmentId: string, region?: string): Promise<void> {
|
||
if (mockOn) return mocked(undefined)
|
||
return request(`/oci-configs/${cfgId}/volume-attachments/${encodeURIComponent(attachmentId)}`, {
|
||
method: 'DELETE',
|
||
query: { region },
|
||
})
|
||
}
|