You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

531 lines
14 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div>
<back-header>
节点管理 > {{ detail.name }}
<!-- <template #extra>-->
<!-- <el-form-item-->
<!-- label="节点调度"-->
<!-- style="margin-bottom: 0; margin-right: 20px"-->
<!-- >-->
<!-- <el-radio-group-->
<!-- :disabled="detail.isExternal"-->
<!-- v-model="tempSchedulable"-->
<!-- size="small"-->
<!-- @change="onChangeSchedulable"-->
<!-- >-->
<!-- <el-radio-button label="启用" :value="true" />-->
<!-- <el-radio-button label="禁用" :value="false" />-->
<!-- </el-radio-group>-->
<!-- </el-form-item>-->
<!-- </template>-->
</back-header>
<block-box class="node-block">
<div class="node-detail">
<div class="node-detail-left">
<div class="title">详细信息</div>
<ul class="node-detail-info">
<li v-for="{ label, value, render } in detailColumns" :key="label">
<span class="label">{{ label }}</span>
<component v-if="render" :is="render(detail)" />
<span v-else class="value">{{ detail[value] }}</span>
</li>
</ul>
</div>
</div>
</block-box>
<block-box>
<ul class="card-gauges">
<li v-for="(item, index) in gaugeConfig.slice(0, 4)" :key="index">
<template v-if="!detail.isExternal || item.title.includes('使用率')">
<Gauge v-bind="item" />
</template>
<template v-else-if="detail.isExternal && item.title.includes('分配率')">
<el-empty description="暂无资源分配数据" :image-size="90" />
</template>
</li>
</ul>
<ul class="card-gauges" style="margin-top: 20px;">
<li v-for="(item, index) in gaugeConfig.slice(4, 7)" :key="index">
<template v-if="!detail.isExternal || item.title.includes('使用率')">
<Gauge v-bind="item" />
</template>
<template v-else-if="detail.isExternal && item.title.includes('分配率')">
<el-empty description="暂无资源分配数据" :image-size="90" />
</template>
</li>
</ul>
</block-box>
<div class="line-box">
<block-box title="资源分配趋势(%">
<template #extra>
<time-picker v-model="times" type="datetimerange" size="small" />
</template>
<div style="height: 200px">
<echarts-plus :options="getRangeOptions({
cpu: gaugeConfig[7].data,
internal: gaugeConfig[8].data,
core: gaugeConfig[4].data,
memory: gaugeConfig[5].data,
})
" />
</div>
</block-box>
<block-box title="资源使用趋势(%">
<template #extra>
<time-picker v-model="times" type="datetimerange" size="small" />
</template>
<div style="height: 200px">
<echarts-plus :options="getRangeOptions({
cpu: gaugeConfig[0].data,
internal: gaugeConfig[1].data,
core: gaugeConfig[6].data,
memory: gaugeConfig[3].data,
})
" />
</div>
</block-box>
</div>
<block-box title="显卡列表">
<CardList :hideTitle="true" :filters="{ nodeUid: detail.uid }" />
</block-box>
<block-box title="任务列表">
<template v-if="detail.isExternal">
<el-alert title="由于节点未纳管,无法获取到任务数据" show-icon type="warning" :closable="false" />
<el-empty description="暂无任务数据" :image-size="100" />
</template>
<template v-else>
<TaskList :hideTitle="true" :filters="{ nodeUid: detail.uid }" />
</template>
</block-box>
</div>
</template>
<script setup lang="jsx">
import BackHeader from '@/components/BackHeader.vue';
import { useRoute, useRouter } from 'vue-router';
import BlockBox from '@/components/BlockBox.vue';
import { computed, onMounted, ref, watch } from 'vue';
import { Tools } from '@element-plus/icons-vue';
import CardList from '~/vgpu/views/card/admin/index.vue';
import TaskList from '~/vgpu/views/task/admin/index.vue';
import Gauge from '~/vgpu/components/gauge.vue';
import useInstantVector from '~/vgpu/hooks/useInstantVector';
import EchartsPlus from '@/components/Echarts-plus.vue';
import TimeSelect from '~/vgpu/components/timeSelect.vue';
import nodeApi from '~/vgpu/api/node';
import { getLineOptions } from '~/vgpu/views/monitor/overview/getOptions';
import { ElMessage, ElMessageBox } from 'element-plus';
import api from '~/vgpu/api/task';
import { getRangeOptions } from './getOptions';
import { bytesToGB } from "@/utils";
import useParentAction from '~/vgpu/hooks/useParentAction';
const { sendRouteChange } = useParentAction();
const route = useRoute();
const detail = ref({});
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000);
const times = ref([start, end]);
const isSchedulable = ref(true);
const tempSchedulable = ref(isSchedulable.value);
const cp = useInstantVector(
[
{
label: 'vGPU 超配',
count: '0',
query: `avg(hami_vgpu_count{node=~"$node"})`,
},
{
label: '算力超配',
count: '0',
query: `avg(hami_vcore_scaling{node=~"$node"})`,
},
{
label: '显存超配',
count: '1.5',
query: `avg(hami_vmemory_scaling{node=~"$node"})`,
},
],
(query) => query.replaceAll('$node', detail.value.name),
);
const gaugeConfig = useInstantVector(
[
{
title: 'CPU 使用率',
percent: 0,
query: `count(node_cpu_seconds_total{mode="idle", instance=~"$node"}) by (instance)*(1 - avg(rate(node_cpu_seconds_total{mode="idle", instance=~"$node"}[5m])) by (instance))`,
totalQuery: `count(node_cpu_seconds_total{mode="idle", instance=~"$node"}) by (instance)`,
percentQuery: `100 * (1 - avg by(instance)(irate(node_cpu_seconds_total{mode="idle", instance=~"$node"}[1m])))`,
total: 0,
used: 0,
unit: '核',
},
{
title: '内存 使用率',
percent: 0,
query: `avg(node_memory_MemTotal_bytes{instance=~"$node"} - node_memory_MemAvailable_bytes{instance=~"$node"}) by (instance) / 1024 / 1024 / 1024`,
totalQuery: `avg(node_memory_MemTotal_bytes{instance=~"$node"}) by (instance) / 1024 / 1024 / 1024`,
percentQuery: `100 * (1 - node_memory_MemAvailable_bytes{instance=~"$node"} / node_memory_MemTotal_bytes{instance=~"$node"})`,
total: 0,
used: 0,
unit: 'GiB',
},
{
title: '磁盘 使用率',
percent: 0,
query: `sum(node_filesystem_size_bytes{instance=~"$node", fstype=~"ext4|xfs", mountpoint!~"/var/lib/kubelet/pods.*"} - node_filesystem_free_bytes{instance=~"$node", fstype=~"ext4|xfs", mountpoint!~"/var/lib/kubelet/pods.*"}) by (instance) / 1024 / 1024 / 1024`,
totalQuery: `sum(node_filesystem_size_bytes{instance=~"$node", fstype=~"ext4|xfs", mountpoint!~"/var/lib/kubelet/pods.*"}) by (instance) / 1024 / 1024 / 1024`,
percentQuery: ``,
total: 0,
used: 0,
unit: 'GiB',
},
{
title: '显存使用率',
percent: 0,
query: `avg(sum(hami_memory_used{node=~"$node"}) by (instance)) / 1024`,
totalQuery: `avg(sum(hami_memory_size{node=~"$node"}) by (instance))/1024`,
percentQuery: `(avg(sum(hami_memory_used{node=~"$node"}) by (instance)) / 1024)/(avg(sum(hami_memory_size{node=~"$node"}) by (instance))/1024)*100`,
total: 0,
used: 0,
unit: 'GiB',
},
{
title: '算力分配率',
percent: 0,
query: `avg(sum(hami_container_vcore_allocated{node=~"$node"}) by (instance))`,
totalQuery: `avg(sum(hami_core_size{node=~"$node"}) by (instance))`,
percentQuery: `avg(sum(hami_container_vcore_allocated{node=~"$node"}) by (instance)) / avg(sum(hami_core_size{node=~"$node"}) by (instance)) *100`,
total: 0,
used: 0,
unit: ' ',
},
{
title: '显存分配率',
percent: 0,
query: `avg(sum(hami_container_vmemory_allocated{node=~"$node"}) by (instance)) / 1024`,
totalQuery: `avg(sum(hami_memory_size{node=~"$node"}) by (instance)) / 1024`,
percentQuery: `(avg(sum(hami_container_vmemory_allocated{node=~"$node"}) by (instance)) / 1024) /(avg(sum(hami_memory_size{node=~"$node"}) by (instance)) / 1024) *100`,
total: 0,
used: 0,
unit: 'GiB',
},
{
title: '算力使用率',
percent: 0,
query: `avg(sum(hami_core_util{node=~"$node"}) by (instance))`,
percentQuery: `avg(sum(hami_core_util_avg{node=~"$node"}) by (instance))`,
totalQuery: `avg(sum(hami_core_size{node=~"$node"}) by (instance))`,
total: 100,
used: 0,
unit: ' ',
},
{
title: 'CPU 分配率',
percent: 0,
query: ``,
totalQuery: ``,
percentQuery: `avg(sum(hami_core_used{node=~"$node"}) by (instance) / sum(hami_core_size{node=~"$node"}) by (instance) * 100)`,
total: 0,
used: 0,
unit: '核',
},
{
title: '内存 分配率',
percent: 0,
query: ``,
totalQuery: ``,
percentQuery: `avg(sum(hami_memory_used{node=~"$node"}) by (instance) / sum(hami_memory_size{node=~"$node"}) by (instance) * 100)`,
total: 0,
used: 0,
unit: 'GiB',
},
],
(query) => query.replaceAll(`$node`, detail.value.name),
times,
);
const detailColumns = [
{
label: '节点状态',
value: 'status',
width: 100,
render: ({ isSchedulable, isExternal }) => {
if (detail.value && detail.value.isSchedulable !== undefined) {
return (
<el-tag disable-transitions type={isExternal ? 'warning' : (isSchedulable ? 'success' : 'danger')}>
{isExternal ? '未纳管' : (isSchedulable ? '可调度' : '禁止调度')}
</el-tag>
);
} else {
return <el-tag disable-transitions size="small" type="info">加载中...</el-tag>;
}
},
},
{
label: '节点 IP 地址',
value: 'ip',
render: ({ ip }) => <text-plus text={ip} copy />,
},
{
label: '节点 UUID',
value: 'uid',
render: ({ uid }) => <text-plus text={uid} copy />,
},
{
label: '操作系统类型',
value: 'operatingSystem',
render: ({ operatingSystem }) => (
<span>
{operatingSystem === '' ? '--' : operatingSystem}
</span>
),
},
{
label: '系统架构',
value: 'architecture',
render: ({ architecture }) => (
<span>
{architecture === '' ? '--' : architecture}
</span>
),
},
{
label: 'kubelet 版本',
value: 'kubeletVersion',
render: ({ kubeletVersion }) => (
<span>
{kubeletVersion === '' ? '--' : kubeletVersion}
</span>
),
},
{
label: '操作系统版本',
value: 'osImage',
render: ({ osImage }) => (
<span>
{osImage === '' ? '--' : osImage}
</span>
),
},
{
label: '内核版本',
value: 'kernelVersion',
render: ({ kernelVersion }) => (
<span>
{kernelVersion === '' ? '--' : kernelVersion}
</span>
),
},
{
label: 'kube-proxy 版本',
value: 'kubeProxyVersion',
render: ({ kubeProxyVersion }) => (
<span>
{kubeProxyVersion === '' ? '--' : kubeProxyVersion}
</span>
),
},
{
label: 'CPU大小',
value: 'cpuCores',
render: ({ cpuCores }) => (
<span>
{`${cpuCores}核`}
</span>
),
},
{
label: '内存大小',
value: 'totalMemory',
render: ({ totalMemory }) => (
<span>
{`${bytesToGB(totalMemory)}GiB`}
</span>
),
},
{
label: '磁盘大小',
value: 'diskSize',
render: ({ diskSize }) => (
<span>
{`${bytesToGB(diskSize)}GiB`}
</span>
),
},
{
label: '容器运行时',
value: 'containerRuntimeVersion',
render: ({ containerRuntimeVersion }) => (
<span>
{containerRuntimeVersion === '' ? '--' : containerRuntimeVersion}
</span>
),
},
{
label: '显卡数量',
value: 'cardCnt',
render: ({ cardCnt }) => (
<span>
{cardCnt === '' ? '--' : cardCnt}
</span>
),
},
{
label: '创建时间',
value: 'creationTimestamp',
render: ({ creationTimestamp }) => (
<span>
{creationTimestamp === '' ? '--' : creationTimestamp}
</span>
),
},
];
const onChangeSchedulable = (val) => {
ElMessageBox.confirm(
`确认对该节点进行${val ? '启用' : '禁用'}操作?`,
'操作确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
},
)
.then(async () => {
try {
await nodeApi
.stop({
nodeName: detail.value.name,
switch: val ? 'off' : 'on',
})
.then(() => {
setTimeout(() => {
refresh();
ElMessage.success(`${val ? '启用' : '禁用'}成功`);
}, 500);
});
} catch (error) {
ElMessage.error(error.message);
tempSchedulable.value = isSchedulable.value;
}
})
.catch(() => {
tempSchedulable.value = isSchedulable.value;
});
};
const refresh = async () => {
detail.value = await nodeApi.getNodeDetail({ uid: route.params.uid });
isSchedulable.value = detail.value.isSchedulable;
};
onMounted(async () => {
await refresh();
});
</script>
<style lang="scss">
.node-detail {
display: flex;
height: 100%;
//gap: 50px;
ul {
margin: 0;
padding: 0;
list-style: none;
}
.title {
color: #1d2b3a;
font-family: 'PingFang SC';
font-size: 14px;
font-style: normal;
font-weight: 500;
//line-height: 20px;
margin-bottom: 20px;
}
//.node-detail-left {
// min-width: 800px;
//}
.node-detail-info {
gap: 15px;
font-size: 12px;
display: grid;
grid-template-columns: repeat(3, 1fr);
.label {
display: inline-block;
width: 100px;
height: 20px;
color: #939ea9;
}
.set {
:hover {
cursor: pointer;
color: #324558;
}
}
.cp {
display: flex;
gap: 25px;
}
}
.gauges {
flex: 1;
display: flex;
li {
flex: 1;
}
}
}
.card-gauges {
margin: 0;
padding: 0;
list-style: none;
display: flex;
height: 200px;
li {
flex: 0.25;
}
}
.line-box {
display: grid;
grid-template-columns: repeat(2, 1fr);
column-gap: 20px;
}
.node-block {
display: flex;
flex-direction: column;
.home-block-content {
flex: 1;
}
}
</style>