|
|
<template>
|
|
|
<list-header description="节点管理用于管理和监控计算节点的状态。它可以启用或禁用节点,查看节点上的物理GPU卡,以及监控节点上运行的所有任务。">
|
|
|
<template #actions>
|
|
|
<el-button @click="handleAdd" style="margin-right: 24px;" type="primary" round>发现节点</el-button>
|
|
|
</template>
|
|
|
</list-header>
|
|
|
|
|
|
<preview-bar :handle-click=handleClick :key="componentKey" />
|
|
|
|
|
|
<table-plus :api="nodeApi.getNodeList()" :columns="columns" :rowAction="rowAction" :searchSchema="searchSchema"
|
|
|
:hasPagination="false" style="height: auto" hideTag ref="table" staticPage>
|
|
|
</table-plus>
|
|
|
|
|
|
<el-dialog @close="nodeSelect = []" v-model="dialogVisible" title="添加节点" width="600" :before-close="handleClose">
|
|
|
<div v-loading="loading">
|
|
|
<template v-if="nodeList && nodeList.length > 0">
|
|
|
<div style="display: flex; align-items: center;" v-for="{ nodeIp }, index in nodeList" :key="nodeIp">
|
|
|
<el-checkbox :model-value="nodeSelect.includes(nodeIp)" @change="handleCheckboxChange(nodeIp)" />
|
|
|
<span style="color: #0B1524; margin-left: 8px;">{{ nodeIp }}</span>
|
|
|
</div>
|
|
|
</template>
|
|
|
<template v-else>
|
|
|
<div>
|
|
|
<el-empty description="暂无节点数据" />
|
|
|
</div>
|
|
|
</template>
|
|
|
</div>
|
|
|
<template v-if="nodeList && nodeList.length > 0" #footer>
|
|
|
<el-button @click="dialogVisible = false">取消</el-button>
|
|
|
<el-button :loading="btnLoading" type="primary" @click="handleOk">确认</el-button>
|
|
|
</template>
|
|
|
</el-dialog>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="jsx">
|
|
|
import nodeApi from '~/vgpu/api/node';
|
|
|
import searchSchema from '~/vgpu/views/node/admin/searchSchema';
|
|
|
import PreviewBar from '~/vgpu/components/previewBar.vue';
|
|
|
import { bytesToGB, roundToDecimal } from '@/utils';
|
|
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
|
|
import { ref } from 'vue';
|
|
|
import useParentAction from '~/vgpu/hooks/useParentAction';
|
|
|
|
|
|
|
|
|
const { sendRouteChange } = useParentAction();
|
|
|
|
|
|
const table = ref();
|
|
|
|
|
|
const componentKey = ref(0);
|
|
|
|
|
|
// 节点选择相关
|
|
|
const dialogVisible = ref(false)
|
|
|
const nodeList = ref([])
|
|
|
const nodeSelect = ref([])
|
|
|
const loading = ref(true)
|
|
|
const btnLoading = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
const handleClick = async (params) => {
|
|
|
const name = params.data.name;
|
|
|
const { list } = await nodeApi.getNodes({ filters: {} })
|
|
|
const node = list.find(node => node.name === name);
|
|
|
if (node) {
|
|
|
const uuid = node.uid;
|
|
|
sendRouteChange(`/admin/vgpu/node/admin/${uuid}?nodeName=${name}`);
|
|
|
} else {
|
|
|
ElMessage.error('节点未找到');
|
|
|
}
|
|
|
};
|
|
|
|
|
|
// 确认操作
|
|
|
const handleOk = async () => {
|
|
|
if (!nodeSelect.value.length) {
|
|
|
ElMessage({
|
|
|
message: '请选择节点',
|
|
|
type: 'warning',
|
|
|
})
|
|
|
return;
|
|
|
}
|
|
|
btnLoading.value = true;
|
|
|
try {
|
|
|
const node_names = nodeList.value.filter(e => nodeSelect.value.includes(e.nodeIp)).map(e => e.nodeName)
|
|
|
const res = await nodeApi.joinNodes({
|
|
|
node_names
|
|
|
})
|
|
|
if (res?.code === 200) {
|
|
|
table.value.fetchData();
|
|
|
componentKey.value += 1;
|
|
|
dialogVisible.value = false;
|
|
|
}
|
|
|
} finally {
|
|
|
btnLoading.value = false;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 复选框变化
|
|
|
const handleCheckboxChange = (ip) => {
|
|
|
const index = nodeSelect.value.indexOf(ip);
|
|
|
if (index > -1) {
|
|
|
nodeSelect.value.splice(index, 1);
|
|
|
} else {
|
|
|
nodeSelect.value.push(ip);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 添加节点
|
|
|
const handleAdd = async () => {
|
|
|
dialogVisible.value = true
|
|
|
loading.value = true
|
|
|
const res = await nodeApi.discoveredNodes({})
|
|
|
nodeList.value = res?.list || []
|
|
|
loading.value = false
|
|
|
}
|
|
|
|
|
|
const columns = [
|
|
|
{
|
|
|
title: '节点名称',
|
|
|
dataIndex: 'name',
|
|
|
render: ({ uid, name }) => (
|
|
|
<text-plus text={name} to={`/admin/vgpu/node/admin/${uid}?nodeName=${name}`} />
|
|
|
),
|
|
|
},
|
|
|
{
|
|
|
title: '节点 IP',
|
|
|
dataIndex: 'ip',
|
|
|
},
|
|
|
{
|
|
|
title: '节点状态',
|
|
|
width: 100,
|
|
|
dataIndex: 'isSchedulable',
|
|
|
render: ({ isSchedulable, isExternal }) => (
|
|
|
<el-tag disable-transitions type={isExternal ? 'warning' : (isSchedulable ? 'success' : 'danger')}>
|
|
|
{isExternal ? '未纳管' : (isSchedulable ? '可调度' : '禁止调度')}
|
|
|
</el-tag>
|
|
|
)
|
|
|
// filters: [
|
|
|
// {
|
|
|
// text: '可调度',
|
|
|
// value: 'true',
|
|
|
// },
|
|
|
// {
|
|
|
// text: '禁止调度',
|
|
|
// value: 'false',
|
|
|
// },
|
|
|
// ],
|
|
|
},
|
|
|
{
|
|
|
title: '显卡型号',
|
|
|
dataIndex: 'type',
|
|
|
// filters: (data) => {
|
|
|
// const r = data.reduce((all, item) => {
|
|
|
// return uniq([...all, ...item.type]);
|
|
|
// }, []);
|
|
|
//
|
|
|
// return r.map((item) => ({ text: item, value: item }));
|
|
|
// },
|
|
|
},
|
|
|
{
|
|
|
title: 'CPU',
|
|
|
dataIndex: 'cpuCores',
|
|
|
render: ({ cpuCores }) => `${cpuCores}核`,
|
|
|
},
|
|
|
{
|
|
|
title: '内存',
|
|
|
dataIndex: 'totalMemory',
|
|
|
render: ({ totalMemory }) => `${bytesToGB(totalMemory)}GiB`,
|
|
|
},
|
|
|
{
|
|
|
title: '磁盘',
|
|
|
dataIndex: 'diskSize',
|
|
|
render: ({ diskSize }) => `${bytesToGB(diskSize)}GiB`,
|
|
|
},
|
|
|
{
|
|
|
title: '所属资源池',
|
|
|
width: 100,
|
|
|
dataIndex: 'resourcePools',
|
|
|
render: ({ resourcePools }) => `${resourcePools.join('、')}`,
|
|
|
},
|
|
|
{
|
|
|
title: '显卡数量',
|
|
|
dataIndex: 'cardCnt',
|
|
|
},
|
|
|
{
|
|
|
title: 'vGPU',
|
|
|
dataIndex: 'used',
|
|
|
render: ({ vgpuTotal, vgpuUsed, isExternal }) => (
|
|
|
<span>
|
|
|
{isExternal ? '--' : vgpuUsed}/{isExternal ? '--' : vgpuTotal}
|
|
|
</span>
|
|
|
),
|
|
|
},
|
|
|
{
|
|
|
title: '算力(已分配/总量)',
|
|
|
width: 120,
|
|
|
dataIndex: 'used',
|
|
|
render: ({ coreTotal, coreUsed, isExternal }) => (
|
|
|
<span>
|
|
|
{isExternal ? '--' : coreUsed}/{coreTotal}
|
|
|
</span>
|
|
|
),
|
|
|
},
|
|
|
{
|
|
|
title: '显存(已分配/总量)',
|
|
|
dataIndex: 'w',
|
|
|
width: 120,
|
|
|
render: ({ memoryTotal, memoryUsed, isExternal }) => (
|
|
|
<span>
|
|
|
{isExternal ? '--' : roundToDecimal(memoryUsed / 1024, 1)}/
|
|
|
{roundToDecimal(memoryTotal / 1024, 1)} GiB
|
|
|
</span>
|
|
|
),
|
|
|
},
|
|
|
];
|
|
|
|
|
|
const rowAction = [
|
|
|
{
|
|
|
title: '查看详情',
|
|
|
onClick: (row) => {
|
|
|
sendRouteChange(`/admin/vgpu/node/admin/${row.uid}?nodeName=${row.name}`);
|
|
|
},
|
|
|
},
|
|
|
{
|
|
|
title: '禁用',
|
|
|
hidden: (row) => !row.isSchedulable,
|
|
|
onClick: async (row) => {
|
|
|
ElMessageBox.confirm(`确认对该节点进行禁用操作?`, '操作确认', {
|
|
|
confirmButtonText: '确定',
|
|
|
cancelButtonText: '取消',
|
|
|
type: 'warning',
|
|
|
})
|
|
|
.then(async () => {
|
|
|
try {
|
|
|
await nodeApi.stop(
|
|
|
{
|
|
|
nodeName: row.name,
|
|
|
status: 'DISABLED'
|
|
|
}
|
|
|
).then(
|
|
|
() => {
|
|
|
setTimeout(() => {
|
|
|
ElMessage.success('节点禁用成功');
|
|
|
table.value.fetchData();
|
|
|
}, 500);
|
|
|
}
|
|
|
)
|
|
|
} catch (error) {
|
|
|
ElMessage.error(error.message);
|
|
|
}
|
|
|
})
|
|
|
.catch(() => { });
|
|
|
},
|
|
|
},
|
|
|
{
|
|
|
title: '开启',
|
|
|
hidden: (row) => row.isSchedulable,
|
|
|
disabled: (row) => row.isExternal,
|
|
|
onClick: async (row) => {
|
|
|
ElMessageBox.confirm(`确认对该节点进行开启调度操作?`, '操作确认', {
|
|
|
confirmButtonText: '确定',
|
|
|
cancelButtonText: '取消',
|
|
|
type: 'warning',
|
|
|
})
|
|
|
.then(async () => {
|
|
|
try {
|
|
|
await nodeApi.stop(
|
|
|
{
|
|
|
nodeName: row.name,
|
|
|
status: 'ENABLE'
|
|
|
}
|
|
|
).then(
|
|
|
() => {
|
|
|
setTimeout(() => {
|
|
|
ElMessage.success('节点开启调度成功');
|
|
|
table.value.fetchData();
|
|
|
}, 500);
|
|
|
}
|
|
|
)
|
|
|
} catch (error) {
|
|
|
ElMessage.error(error.message);
|
|
|
}
|
|
|
})
|
|
|
.catch(() => { });
|
|
|
},
|
|
|
},
|
|
|
];
|
|
|
</script>
|
|
|
|
|
|
<style></style>
|