Files
oci-portal-dash/src/api/instances.ts
T
2026-07-30 12:23:16 +08:00

452 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 },
})
}