Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CNV-49533: Add Memory, CPU and Network usage on vm list #2299

Merged
merged 1 commit into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/views/virtualmachines/list/VirtualMachinesList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import VirtualMachineRow from './components/VirtualMachineRow/VirtualMachineRow'
import VirtualMachinesCreateButton from './components/VirtualMachinesCreateButton/VirtualMachinesCreateButton';
import useSelectedFilters from './hooks/useSelectedFilters';
import useVirtualMachineColumns from './hooks/useVirtualMachineColumns';
import useVMMetrics from './hooks/useVMMetrics';
import { deselectAll, selectAll, selectedVMs } from './selectedVMs';

import '@kubevirt-utils/styles/list-managment-group.scss';
Expand All @@ -63,6 +64,7 @@ const VirtualMachinesList: FC<VirtualMachinesListProps> = ({ kind, namespace })
const isProxyPodAlive = useKubevirtDataPodHealth();

useSignals();
useVMMetrics();

const query = useQuery();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as React from 'react';
import React, { FC, useMemo } from 'react';

import { VirtualMachineModelGroupVersionKind } from '@kubevirt-ui/kubevirt-api/console';
import {
Expand All @@ -17,9 +17,13 @@ import { setSelectedTreeItem, treeDataMap } from '@virtualmachines/tree/utils/ut
import VirtualMachineStatus from '../VirtualMachineStatus/VirtualMachineStatus';
import { VMStatusConditionLabelList } from '../VMStatusConditionLabel';

import CPUPercentage from './components/CPUPercentage';
import MemoryPercentage from './components/MemoryPercentage';
import NetworkUsage from './components/NetworkUsage';

import './virtual-machine-row-layout.scss';

const VirtualMachineRowLayout: React.FC<
const VirtualMachineRowLayout: FC<
RowProps<
V1VirtualMachine,
{
Expand All @@ -32,6 +36,9 @@ const VirtualMachineRowLayout: React.FC<
> = ({ activeColumnIDs, obj, rowData: { ips, isSingleNodeCluster, node, vmim } }) => {
const selected = isVMSelected(obj);

const vmName = useMemo(() => getName(obj), [obj]);
const vmNamespace = useMemo(() => getNamespace(obj), [obj]);

const [actions] = useVirtualMachineActionsProvider(obj, vmim, isSingleNodeCluster);
return (
<>
Expand All @@ -45,19 +52,19 @@ const VirtualMachineRowLayout: React.FC<
<TableData activeColumnIDs={activeColumnIDs} className="vm-column" id="name">
<ResourceLink
onClick={() => {
setSelectedTreeItem(treeDataMap.value[`${getNamespace(obj)}/${getName(obj)}`]);
setSelectedTreeItem(treeDataMap.value[`${vmNamespace}/${vmName}`]);
}}
groupVersionKind={VirtualMachineModelGroupVersionKind}
name={getName(obj)}
namespace={getNamespace(obj)}
name={vmName}
namespace={vmNamespace}
/>
</TableData>
<TableData
activeColumnIDs={activeColumnIDs}
className="pf-m-width-10 vm-column"
id="namespace"
>
<ResourceLink kind="Namespace" name={getNamespace(obj)} />
<ResourceLink kind="Namespace" name={vmNamespace} />
</TableData>
<TableData activeColumnIDs={activeColumnIDs} className="pf-m-width-15 vm-column" id="status">
<VirtualMachineStatus vm={obj} />
Expand All @@ -82,6 +89,15 @@ const VirtualMachineRowLayout: React.FC<
>
{ips}
</TableData>
<TableData activeColumnIDs={activeColumnIDs} className="vm-column" id="memory-usage">
<MemoryPercentage vmName={vmName} vmNamespace={vmNamespace} />
</TableData>
<TableData activeColumnIDs={activeColumnIDs} className="vm-column" id="cpu-usage">
<CPUPercentage vmName={vmName} vmNamespace={vmNamespace} />
</TableData>
<TableData activeColumnIDs={activeColumnIDs} className="vm-column" id="network-usage">
<NetworkUsage vmName={vmName} vmNamespace={vmNamespace} />
</TableData>
<TableData
activeColumnIDs={activeColumnIDs}
className="dropdown-kebab-pf pf-v5-c-table__action"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React, { FC, memo } from 'react';

import { NO_DATA_DASH } from '@kubevirt-utils/resources/vm/utils/constants';
import { isEmpty } from '@kubevirt-utils/utils/utils';
import { getVMMetrics } from '@virtualmachines/list/metrics';

type CPUPercentageProps = {
vmName: string;
vmNamespace: string;
};

const CPUPercentage: FC<CPUPercentageProps> = ({ vmName, vmNamespace }) => {
const { cpuRequested, cpuUsage } = getVMMetrics(vmName, vmNamespace);

if (isEmpty(cpuRequested) || isEmpty(cpuUsage)) return <span>{NO_DATA_DASH}</span>;

const percentage = Math.round((cpuUsage * 10000) / cpuRequested) / 100;
return <span>{percentage}%</span>;
};

export default memo(CPUPercentage);
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React, { FC, memo } from 'react';

import { NO_DATA_DASH } from '@kubevirt-utils/resources/vm/utils/constants';
import { isEmpty } from '@kubevirt-utils/utils/utils';
import { getVMMetrics } from '@virtualmachines/list/metrics';

type MemoryPercentageProps = {
vmName: string;
vmNamespace: string;
};

const MemoryPercentage: FC<MemoryPercentageProps> = ({ vmName, vmNamespace }) => {
const { memoryRequested, memoryUsage } = getVMMetrics(vmName, vmNamespace);

if (isEmpty(memoryRequested) || isEmpty(memoryUsage)) return <span>{NO_DATA_DASH}</span>;

const percentage = Math.round((memoryUsage / memoryRequested) * 10000) / 100;
return <span>{percentage}%</span>;
};

export default memo(MemoryPercentage);
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React, { FC, memo } from 'react';
import xbytes from 'xbytes';

import { NO_DATA_DASH } from '@kubevirt-utils/resources/vm/utils/constants';
import { isEmpty } from '@kubevirt-utils/utils/utils';
import { getVMMetrics } from '@virtualmachines/list/metrics';

type NetworkUsageProps = {
vmName: string;
vmNamespace: string;
};

const NetworkUsage: FC<NetworkUsageProps> = ({ vmName, vmNamespace }) => {
const { networkUsage } = getVMMetrics(vmName, vmNamespace);
if (isEmpty(networkUsage)) return <>{NO_DATA_DASH}</>;

const totalTransferred = xbytes(networkUsage || 0, {
fixed: 0,
iec: true,
});

return <div>{totalTransferred}ps</div>;
};

export default memo(NetworkUsage);
23 changes: 23 additions & 0 deletions src/views/virtualmachines/list/hooks/constants.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,25 @@
import { ALL_NAMESPACES_SESSION_KEY } from '@kubevirt-utils/hooks/constants';

export const TEXT_FILTER_NAME_ID = 'name';
export const TEXT_FILTER_LABELS_ID = 'labels';

export const VMListQueries = {
CPU_REQUESTED: 'CPU_REQUESTED',
CPU_USAGE: 'CPU_USAGE',
MEMORY_REQUESTED: 'MEMORY_REQUESTED',
MEMORY_USAGE: 'MEMORY_USAGE',
NETWORK_TOTAL_USAGE: 'NETWORK_TOTAL_USAGE',
};

export const getVMListQueries = (namespace: string) => {
const namespaceFilter =
namespace === ALL_NAMESPACES_SESSION_KEY ? '' : `namespace='${namespace}'`;

return {
[VMListQueries.CPU_REQUESTED]: `kube_pod_resource_request{resource='cpu',${namespaceFilter}}`,
[VMListQueries.CPU_USAGE]: `rate(kubevirt_vmi_cpu_usage_seconds_total{${namespaceFilter}}[5m])`,
[VMListQueries.MEMORY_REQUESTED]: `kube_pod_resource_request{resource='memory',${namespaceFilter}}`,
[VMListQueries.MEMORY_USAGE]: `kubevirt_vmi_memory_used_bytes{${namespaceFilter}}`,
[VMListQueries.NETWORK_TOTAL_USAGE]: `rate(kubevirt_vmi_network_transmit_bytes_total{${namespaceFilter}}[5m]) + rate(kubevirt_vmi_network_receive_bytes_total{${namespaceFilter}}[5m])`,
};
};
135 changes: 135 additions & 0 deletions src/views/virtualmachines/list/hooks/useVMMetrics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { useEffect, useMemo } from 'react';

import { IoK8sApiCoreV1Pod } from '@kubevirt-ui/kubevirt-api/kubernetes';
import { ALL_NAMESPACES_SESSION_KEY } from '@kubevirt-utils/hooks/constants';
import { modelToGroupVersionKind, PodModel } from '@kubevirt-utils/models';
import { isEmpty } from '@kubevirt-utils/utils/utils';
import {
PrometheusEndpoint,
useActiveNamespace,
useK8sWatchResource,
usePrometheusPoll,
} from '@openshift-console/dynamic-plugin-sdk';

import {
setVMCPURequested,
setVMCPUUsage,
setVMMemoryRequested,
setVMMemoryUsage,
setVMNetworkUsage,
} from '../metrics';

import { getVMListQueries, VMListQueries } from './constants';
import { getVMNamesFromPodsNames } from './utils';

const useVMMetrics = () => {
const [activeNamespace] = useActiveNamespace();
const allNamespace = useMemo(
() => activeNamespace === ALL_NAMESPACES_SESSION_KEY,
[activeNamespace],
);
const currentTime = useMemo<number>(() => Date.now(), []);

const [pods] = useK8sWatchResource<IoK8sApiCoreV1Pod[]>({
groupVersionKind: modelToGroupVersionKind(PodModel),
isList: true,
namespace: allNamespace ? undefined : activeNamespace,
});

const launcherNameToVMName = useMemo(() => getVMNamesFromPodsNames(pods), [pods]);

const queries = useMemo(() => getVMListQueries(activeNamespace), [activeNamespace]);

const [memoryUsageResponse] = usePrometheusPoll({
endpoint: PrometheusEndpoint?.QUERY,
endTime: currentTime,
namespace: allNamespace ? undefined : activeNamespace,
query: queries?.[VMListQueries.MEMORY_USAGE],
});

const [networkTotalResponse] = usePrometheusPoll({
endpoint: PrometheusEndpoint?.QUERY,
endTime: currentTime,
namespace: allNamespace ? undefined : activeNamespace,
query: queries?.NETWORK_TOTAL_USAGE,
});

const [memoryRequestedResponse] = usePrometheusPoll({
endpoint: PrometheusEndpoint?.QUERY,
endTime: currentTime,
namespace: allNamespace ? undefined : activeNamespace,
query: queries?.[VMListQueries.MEMORY_REQUESTED],
});

const [cpuUsageResponse] = usePrometheusPoll({
endpoint: PrometheusEndpoint?.QUERY,
endTime: currentTime,
namespace: allNamespace ? undefined : activeNamespace,
query: queries?.[VMListQueries.CPU_USAGE],
});

const [cpuRequestedResponse] = usePrometheusPoll({
endpoint: PrometheusEndpoint?.QUERY,
endTime: currentTime,
namespace: allNamespace ? undefined : activeNamespace,
query: queries?.[VMListQueries.CPU_REQUESTED],
});

useEffect(() => {
networkTotalResponse?.data?.result?.forEach((result) => {
const vmName = result?.metric?.name;
const vmNamespace = result?.metric?.namespace;
const memoryUsage = parseFloat(result?.value?.[1]);

setVMNetworkUsage(vmName, vmNamespace, memoryUsage);
});
}, [networkTotalResponse]);

useEffect(() => {
memoryUsageResponse?.data?.result?.forEach((result) => {
const vmName = result?.metric?.name;
const vmNamespace = result?.metric?.namespace;
const memoryUsage = parseFloat(result?.value?.[1]);

setVMMemoryUsage(vmName, vmNamespace, memoryUsage);
});
}, [memoryUsageResponse]);

useEffect(() => {
memoryRequestedResponse?.data?.result?.forEach((result) => {
const vmName = launcherNameToVMName?.[`${result?.metric?.namespace}-${result?.metric?.pod}`];

if (isEmpty(vmName)) return;
const vmNamespace = result?.metric?.namespace;

const memoryRequested = parseFloat(result?.value?.[1]);

setVMMemoryRequested(vmName, vmNamespace, memoryRequested);
});
}, [memoryRequestedResponse, launcherNameToVMName]);

useEffect(() => {
cpuUsageResponse?.data?.result?.forEach((result) => {
const vmName = result?.metric?.name;
const vmNamespace = result?.metric?.namespace;
const cpuUsage = parseFloat(result?.value?.[1]);

setVMCPUUsage(vmName, vmNamespace, cpuUsage);
});
}, [cpuUsageResponse]);

useEffect(() => {
cpuRequestedResponse?.data?.result?.forEach((result) => {
const vmName = launcherNameToVMName?.[`${result?.metric?.namespace}-${result?.metric?.pod}`];

if (isEmpty(vmName)) return;

const vmNamespace = result?.metric?.namespace;
const cpuRequested = parseFloat(result?.value?.[1]);

setVMCPURequested(vmName, vmNamespace, cpuRequested);
});
}, [cpuRequestedResponse, launcherNameToVMName]);
};

export default useVMMetrics;
15 changes: 15 additions & 0 deletions src/views/virtualmachines/list/hooks/useVirtualMachineColumns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,21 @@ const useVirtualMachineColumns = (
props: { className: 'pf-m-width-10' },
title: t('IP address'),
},
{
additional: true,
id: 'memory-usage',
title: t('Memory'),
},
{
additional: true,
id: 'cpu-usage',
title: t('CPU'),
},
{
additional: true,
id: 'network-usage',
title: t('Network'),
},
{
id: '',
props: { className: 'dropdown-kebab-pf pf-v5-c-table__action' },
Expand Down
21 changes: 21 additions & 0 deletions src/views/virtualmachines/list/hooks/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import VirtualMachineInstanceModel from '@kubevirt-ui/kubevirt-api/console/models/VirtualMachineInstanceModel';
import { IoK8sApiCoreV1Pod } from '@kubevirt-ui/kubevirt-api/kubernetes';
import { getName, getNamespace } from '@kubevirt-utils/resources/shared';
import { K8sResourceCommon } from '@openshift-console/dynamic-plugin-sdk';

const getVMIOwner = (resource: K8sResourceCommon) =>
resource?.metadata?.ownerReferences?.find(
(owner) => owner.kind === VirtualMachineInstanceModel.kind,
);

export const getVMNamesFromPodsNames = (pods: IoK8sApiCoreV1Pod[]) => {
return pods?.reduce((acc, pod) => {
const vmiOwner = getVMIOwner(pod);

if (!vmiOwner) return acc;

acc[`${getNamespace(pod)}-${getName(pod)}`] = vmiOwner.name;

return acc;
}, {});
};
Loading
Loading