@@ -34,6 +34,7 @@
< el-dropdown-item command = "rescue" > 救援模式 < / el-dropdown-item >
< el-dropdown-item command = "exitRescue" > 退出救援 < / el-dropdown-item >
< el-dropdown-item divided command = "migrateVm" > 迁移虚拟机 < / el-dropdown-item >
< el-dropdown-item command = "dataMigrateVm" > 数据迁移 < / el-dropdown-item >
< / el-dropdown-menu >
< / template >
< / el-dropdown >
@@ -225,7 +226,7 @@
< / el-table-column > -- >
< el-table-column label = "状态" width = "80" >
< template # default = "{ row }" >
< el-tag : type = "row.status === 'ready' ? 'success' : 'info'" size = "small" > { { row . status === 'ready' ? '就绪' : ( row . status || '-' ) } } < / el-tag >
< el-tag :type = "volumeStatusType(row.status)" size = "small" > { { volumeStatusLabel ( row . status ) } } < / el-tag >
< / template >
< / el-table-column >
< el-table-column prop = "path" label = "路径" min -width = " 160 " show -overflow -tooltip >
@@ -259,7 +260,7 @@
< div style = "display: flex; gap: 8px" >
< el-button size = "small" type = "primary" @click ="handleSgCreate" > < el -icon > < Plus / > < / el-icon > 创建安全组 < / el-button >
< el-button size = "small" @click ="handleSgBind" > 绑定安全组 < / el -button >
< el-button size = "small" :icon = "Refresh" @click ="loadDetail " > 刷新 < / el -button >
< el-button size = "small" :icon = "Refresh" @click ="async () => { await loadDetail(); loadSgLockInfo() } " > 刷新 < / el -button >
< / div >
< / div >
< el-table :data = "pagedSecurityGroups" size = "small" stripe >
@@ -789,8 +790,8 @@
< el-form-item label = "上行带宽(Mbps)" >
< el-input-number v-model = "trafficForm.tx_bandwidth" :min="0" controls-position="right" style="width: 100%" / >
< / el-form-item >
< el-form-item label = "每月最大流量(M B)" >
< el-input-number v-model = "trafficForm.traffic_max " :min="0" controls-position="right" style="width: 100%" / >
< el-form-item label = "流量上限(G B)" >
< el-input-number v-model = "trafficForm._ trafficGB " :min="0" :precision="2" controls-position="right" style="width: 100%" / >
< / el-form-item >
< / el-form >
< template # footer >
@@ -841,6 +842,66 @@
< / template >
< / el-dialog >
<!-- 数据迁移弹窗 -- >
< el-dialog v-model = "dataMigrateVisible" title="发起虚拟机数据迁移" width="580px" destroy -on -close >
< el-alert type = "info" :closable = "false" style = "margin-bottom:16px" >
源服务的虚拟机数据将通过 rsync 传输到目标服务的宿主机上 , 完成后在目标宿主机上校验并恢复虚拟机 。
< / el-alert >
< el-form :model = "dataMigrateForm" label -width = " 130px " v-loading = "dataMigrateLoading" >
< el-form-item label = "源主控服务" >
< el-input : model -value = " ` $ { serviceName } ( ID : $ { serviceId } ) ` " disabled / >
< / el-form-item >
< el-form-item label = "源虚拟机" >
< el-input : model -value = " ` $ { detail ? .name | | ' ' } ( ID : $ { vmId } ) ` " disabled / >
< / el-form-item >
< el-form-item label = "目标主控服务" required >
< el-select v-model = "dataMigrateForm.target_service_id" placeholder="选择目标主控服务" filterable style = "width:100%"
@ change = "dataMigrateForm.target_host_id = null; loadDataMigrateHosts()" >
< el-option v-for = "s in dataMigrateServiceOptions" :key="s.id" :label="`${s.name} (ID:${s.id})`" :value="s.id" / >
< / el-select >
< / el-form-item >
< el-form-item label = "目标宿主机" required >
< el-select v-model = "dataMigrateForm.target_host_id" placeholder="选择目标宿主机" filterable style = "width:100%"
: disabled = "!dataMigrateForm.target_service_id" : loading = "dataMigrateHostsLoading" >
< el-option v-for = "h in dataMigrateHostOptions" :key="h.id" :label="`${h.name} (${h.ip || h.id})`" :value="h.id" / >
< / el-select >
< / el-form-item >
< el-row :gutter = "16" >
< el-col :span = "12" >
< el-form-item label = "IPv4数量" >
< el-input-number v-model = "dataMigrateForm.ipv4_num" :min="0" controls-position="right" style="width:100%" / >
< / el-form-item >
< / el-col >
< el-col :span = "12" >
< el-form-item label = "IPv6数量" >
< el-input-number v-model = "dataMigrateForm.ipv6_num" :min="0" controls-position="right" style="width:100%" / >
< / el-form-item >
< / el-col >
< / el-row >
< / el-form >
< template # footer >
< el-button @click ="dataMigrateVisible = false" > 取消 < / el -button >
< el-button type = "primary" :loading = "actionLoading" @click ="submitDataMigrate" > 发起迁移 < / el -button >
< / template >
< / el-dialog >
<!-- 数据迁移进度弹窗 -- >
< el-dialog v-model = "dataMigrateProgressVisible" title="数据迁移进度" width="480px" destroy -on -close >
< div v-loading = "dataMigrateProgressLoading" >
< el-descriptions :column = "1" border size = "small" v-if = "dataMigrateProgressData" >
< el-descriptions-item label = "迁移ID" > < span style = "font-family:monospace;font-size:12px" > { { dataMigrateProgressData . migration _id || '-' } } < / span > < / el-descriptions-item >
< el-descriptions-item label = "状态" > < el-tag :type = "taskStatusType(dataMigrateProgressData.status)" size = "small" > { { dataMigrateProgressData . status || '-' } } < / el-tag > < / el-descriptions-item >
< el-descriptions-item label = "进度" v-if = "dataMigrateProgressData.progress != null" > {{ dataMigrateProgressData.progress }} % < / el -descriptions -item >
< el-descriptions-item label = "信息" v-if = "dataMigrateProgressData.message" > {{ dataMigrateProgressData.message }} < / el -descriptions -item >
< / el-descriptions >
< el-empty v-else-if = "!dataMigrateProgressLoading" description="暂无进度信息" :image-size="60" / >
< / div >
< template # footer >
< el-button @click ="dataMigrateProgressVisible = false" > 关闭 < / el -button >
< el-button :icon = "Refresh" @click ="loadDataMigrateProgress" :loading = "dataMigrateProgressLoading" > 刷新进度 < / el-button >
< / template >
< / el-dialog >
<!-- 绑定外网选择器 ( bridge ) -- >
< NetworkSelectorPopup v-model = "showNetBindBridgeSelector" :service-id="serviceId" :host-id="vmHostId" filter-type="bridge" filter-used="false" @confirm="handleNetBindBridgeConfirm" @create="() => handleNetCreate('bindBridge')" / >
<!-- 绑定内网选择器 ( nat ) -- >
@@ -1175,10 +1236,13 @@ import {
getVmList , bindSecurityGroup , unbindSecurityGroup ,
getSnapshotList , createSnapshot , restoreSnapshot , deleteSnapshot , getSnapshotProgress , getSnapshotCount , setSnapshotLimit ,
getBackupList , createBackup , restoreBackup , deleteBackup , getBackupProgress , getBackupCount , setBackupLimit ,
migrateVm , getRemoteHostGroupList , getRemoteHostDetail
migrateVm , getRemoteHostGroupList , getRemoteHostDetail ,
dataMigrateVm , getDataMigrateProgress ,
getKvmServiceList
} from '@/api/admin/kvmService'
import { getUserInfo } from '@/api/admin/user'
import { extractApiError } from '@/utils/kvmErrorUtil'
import { vmStatusLabel as vmStatusLabelUtil , vmStatusType as vmStatusTypeUtil , volumeStatusLabel , volumeStatusType } from '@/utils/tool'
import * as echarts from 'echarts'
import ImageSelectorPopup from '@/components/admin/ImageSelectorPopup.vue'
import NetworkSelectorPopup from '@/components/admin/NetworkSelectorPopup.vue'
@@ -1264,10 +1328,11 @@ const handleMoreCommand = (cmd) => {
migrateVm : handleMigrateVm
}
if ( actionMap [ cmd ] ) actionMap [ cmd ] ( )
if ( cmd === 'dataMigrateVm' ) handleDataMigrateVm ( )
}
const vmStatusType = ( s ) => ( { running : 'success' , ready : 'success' , creating : 'warning' , pending : 'info' , stopped : 'danger' , stop : 'danger' , shutoff : 'danger' , error : 'danger' , paused : 'warning' , reboot : 'warning' , poweroff : 'info' , unknown : 'info' } [ s ] || 'info' )
const vmStatusLabel = ( s ) => ( { running : '运行中' , ready : '就绪' , creating : '创建中' , pending : '等待中' , stopped : '已停止' , stop : '已停止' , shutoff : '已关闭' , error : '错误' , paused : '已暂停' , reboot : '重启中' , poweroff : '已关机' , unknown : '未知' } [ s ] || s || '-' )
const vmStatusType = ( s ) => vmStatusTypeUtil ( s )
const vmStatusLabel = ( s ) => vmStatusLabelUtil ( s )
const imgStatusType = ( s ) => ( { ready : 'success' , downloading : 'warning' , pending : 'info' , error : 'danger' } [ s ] || 'info' )
const imgStatusLabel = ( s ) => ( { ready : '就绪' , downloading : '下载中' , pending : '等待中' , error : '错误' } [ s ] || s || '-' )
@@ -1696,14 +1761,14 @@ const submitRefactorVm = async () => {
/ / - - - - 修 改 带 宽 - - - -
const trafficDialogVisible = ref ( false )
const trafficForm = reactive ( { rx _bandwidth : 0 , tx _bandwidth : 0 , traffic _max : 0 } )
const trafficForm = reactive ( { rx _bandwidth : 0 , tx _bandwidth : 0 , _trafficGB : 0 } )
const handleUpdateTraffic = ( ) => {
if ( ! detail . value ) return
Object . assign ( trafficForm , {
rx _bandwidth : detail . value . rx _bandwidth || 0 ,
tx _bandwidth : detail . value . tx _bandwidth || 0 ,
traffic _max : detail . value . traffic _max || 0
_trafficGB : ( ( detail . value . traffic _max || 0 ) / 1024 ) . toFixed ( 2 ) * 1
} )
trafficDialogVisible . value = true
}
@@ -1716,7 +1781,7 @@ const submitUpdateTraffic = async () => {
fd . append ( 'vm_id' , vmId . value )
fd . append ( 'rx_bandwidth' , trafficForm . rx _bandwidth )
fd . append ( 'tx_bandwidth' , trafficForm . tx _bandwidth )
if ( trafficForm . traffic _max ) fd . append ( 'traffic_max' , trafficForm . traffic _max )
if ( trafficForm . _trafficGB ) fd . append ( 'traffic_max' , Math . round ( trafficForm . _trafficGB * 1024 ) ) / / G B → M b
const res = await updateVmTraffic ( fd )
if ( res ? . data ? . code === 200 ) { ElMessage . success ( '带宽修改成功' ) ; trafficDialogVisible . value = false ; loadDetail ( ) }
else ElMessage . error ( extractApiError ( res ? . data , '修改失败' ) )
@@ -1770,6 +1835,78 @@ const submitMigrateVm = async () => {
} catch ( e ) { ElMessage . error ( extractApiError ( e ? . response ? . data , '迁移失败' ) ) } finally { actionLoading . value = false }
}
/ / - - - - 数 据 迁 移 - - - -
const dataMigrateVisible = ref ( false )
const dataMigrateLoading = ref ( false )
const dataMigrateHostsLoading = ref ( false )
const dataMigrateServiceOptions = ref ( [ ] )
const dataMigrateHostOptions = ref ( [ ] )
const dataMigrateProgressVisible = ref ( false )
const dataMigrateProgressLoading = ref ( false )
const dataMigrateProgressData = ref ( null )
const dataMigrationId = ref ( '' )
const dataMigrateForm = reactive ( { target _service _id : null , target _host _id : null , ipv4 _num : 0 , ipv6 _num : 0 } )
const handleDataMigrateVm = async ( ) => {
Object . assign ( dataMigrateForm , { target _service _id : null , target _host _id : null , ipv4 _num : 0 , ipv6 _num : 0 } )
dataMigrateVisible . value = true
dataMigrateLoading . value = true
try {
const res = await getKvmServiceList ( { page : 1 , count : 10 } )
if ( res ? . data ? . code === 200 && res ? . data ? . data ) {
const inner = res . data . data
const raw = inner . data || inner . list || ( Array . isArray ( inner ) ? inner : [ ] )
dataMigrateServiceOptions . value = raw . map ( s => ( { id : s . id ? ? s . Id , name : s . name ? ? s . Name } ) )
}
} catch { /* */ } finally { dataMigrateLoading . value = false }
}
const loadDataMigrateHosts = async ( ) => {
if ( ! dataMigrateForm . target _service _id ) return
dataMigrateHostsLoading . value = true
try {
const res = await getRemoteHostList ( { service _id : dataMigrateForm . target _service _id , page : 1 , page _size : 10 } )
if ( res ? . data ? . code === 200 && res ? . data ? . data ) {
const inner = res . data . data
dataMigrateHostOptions . value = Array . isArray ( inner ) ? inner : ( inner . hosts || inner . data || [ ] )
}
} catch { /* */ } finally { dataMigrateHostsLoading . value = false }
}
const submitDataMigrate = async ( ) => {
if ( ! dataMigrateForm . target _service _id ) { ElMessage . warning ( '请选择目标主控服务' ) ; return }
if ( ! dataMigrateForm . target _host _id ) { ElMessage . warning ( '请选择目标宿主机' ) ; return }
actionLoading . value = true
try {
const fd = new FormData ( )
fd . append ( 'source_service_id' , serviceId . value )
fd . append ( 'source_vm_id' , vmId . value )
fd . append ( 'target_service_id' , dataMigrateForm . target _service _id )
fd . append ( 'target_host_id' , dataMigrateForm . target _host _id )
if ( dataMigrateForm . ipv4 _num ) fd . append ( 'ipv4_num' , dataMigrateForm . ipv4 _num )
if ( dataMigrateForm . ipv6 _num ) fd . append ( 'ipv6_num' , dataMigrateForm . ipv6 _num )
const res = await dataMigrateVm ( fd )
if ( res ? . data ? . code === 200 ) {
ElMessage . success ( '数据迁移已发起' )
dataMigrationId . value = res . data . data ? . migration _id || ''
dataMigrateVisible . value = false
/ / 自 动 打 开 进 度 弹 窗
dataMigrateProgressData . value = res . data . data
dataMigrateProgressVisible . value = true
} else ElMessage . error ( extractApiError ( res ? . data , '发起迁移失败' ) )
} catch ( e ) { ElMessage . error ( extractApiError ( e ? . response ? . data , '发起迁移失败' ) ) } finally { actionLoading . value = false }
}
const loadDataMigrateProgress = async ( ) => {
if ( ! dataMigrationId . value ) return
dataMigrateProgressLoading . value = true
try {
const res = await getDataMigrateProgress ( { migration _id : dataMigrationId . value , service _id : serviceId . value } )
if ( res ? . data ? . code === 200 ) dataMigrateProgressData . value = res . data . data
else ElMessage . warning ( '暂无进度信息' )
} catch { /* */ } finally { dataMigrateProgressLoading . value = false }
}
/ / - - - - V N C 连 接 - - - -
const vncDialogVisible = ref ( false )
const vncNodeId = ref ( null )
@@ -2048,7 +2185,7 @@ const handleVolUnmount = (row) => {
}
const loadVmListOptions = async ( ) => {
try {
const res = await getVmList ( { service _id : serviceId . value , page : 1 , page _size : 200 } )
const res = await getVmList ( { service _id : serviceId . value , page : 1 , page _size : 10 } )
if ( res ? . data ? . code === 200 && res ? . data ? . data ) {
const inner = res . data . data
vmListOptions . value = inner . vms || inner . data || ( Array . isArray ( inner ) ? inner : [ ] )
@@ -2790,6 +2927,22 @@ const triggerTabLoad = (tab) => {
if ( tab === 'snapshot' ) { loadSnapshots ( ) ; loadSnapshotQuota ( ) }
if ( tab === 'backup' ) { loadBackups ( ) ; loadBackupQuota ( ) }
if ( tab === 'userNetworking' ) loadVmNetworkingList ( )
if ( tab === 'security' ) loadSgLockInfo ( )
}
/ / 请 求 安 全 组 详 情 补 充 l o c k 字 段
const loadSgLockInfo = async ( ) => {
const groups = [ vmPortGroup . value , vmOutPortGroup . value ] . filter ( Boolean )
for ( const sg of groups ) {
try {
const res = await getSecurityGroupDetail ( { service _id : serviceId . value , id : sg . id } )
if ( res ? . data ? . code === 200 && res ? . data ? . data ) {
const d = res . data . data . group || res . data . data . data || res . data . data
if ( vmPortGroup . value ? . id === sg . id ) vmPortGroup . value = { ... vmPortGroup . value , lock : d . lock ? ? sg . lock }
if ( vmOutPortGroup . value ? . id === sg . id ) vmOutPortGroup . value = { ... vmOutPortGroup . value , lock : d . lock ? ? sg . lock }
}
} catch { /* */ }
}
}
watch ( vmId , ( ) => { if ( isPageActive ) initPage ( ) } )