fix/fixed disk chart information

This commit is contained in:
biersoeckli
2024-12-31 10:34:26 +00:00
parent 2c2e638c08
commit 75710e438d
7 changed files with 553 additions and 267 deletions

View File

@@ -0,0 +1,234 @@
'use client';
import {
Label,
PolarGrid,
PolarRadiusAxis,
RadialBar,
RadialBarChart,
} from 'recharts';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart';
import { NodeResourceModel } from '@/shared/model/node-resource.model';
import { useEffect } from 'react';
import { StringUtils } from '@/shared/utils/string.utils';
export default function ChartDiskRessources({
nodeRessource,
}: {
nodeRessource: NodeResourceModel;
}) {
const chartData = [{
diskUsed: nodeRessource.diskUsageAbsolut, //* 360 / nodeRessource.diskUsageCapacity,
diskReserved: nodeRessource.diskUsageReserved, //* 360 / nodeRessource.diskUsageCapacity,
diskSchedulable: nodeRessource.diskSpaceSchedulable
}];
const chartConfig = {
diskUsed: {
label: "Used",
color: "hsl(var(--chart-1))",
},
diskReserved: {
label: "Reserved (free but not usable)",
color: "hsl(var(--chart-2))",
},
diskSchedulable: {
label: "Schedulable",
color: "hsl(var(--muted))",
},
} satisfies ChartConfig
return (<Card className="flex flex-col">
<CardHeader className="items-center pb-0">
<CardTitle>Disk</CardTitle>
<CardDescription>Usage in %</CardDescription>
</CardHeader>
<CardContent className="flex flex-1 items-center pb-0">
<ChartContainer
config={chartConfig}
className="mx-auto aspect-square w-full max-w-[250px]"
>
<RadialBarChart
data={chartData}
innerRadius={80}
outerRadius={110}
>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent hideLabel formatter={(value, name) => {
// Convert the value from bytes to gigabytes
const formattedValue = StringUtils.convertBytesToReadableSize(value as number);
// Optionally, you can customize the label (name) here if needed
return <div className='flex gap-2'>
<div className='self-center rounded w-2 h-2' style={{ backgroundColor: (chartConfig as any)[name].color }}></div>
<div className='flex-1'>{(chartConfig as any)[name].label}:</div>
<div>{formattedValue}</div>
</div>
}} />}
/>
<PolarRadiusAxis tick={false} tickLine={false} axisLine={false}>
<Label
content={({ viewBox }) => {
if (
viewBox &&
'cx' in viewBox &&
'cy' in viewBox
) {
return (
<text
x={viewBox.cx}
y={viewBox.cy}
textAnchor="middle"
dominantBaseline="middle"
>
<tspan
x={viewBox.cx}
y={viewBox.cy}
className="fill-foreground text-4xl font-bold"
>
{((nodeRessource.diskUsageAbsolut + nodeRessource.diskUsageReserved) / nodeRessource.diskUsageCapacity * 100).toFixed(1)}
</tspan>
<tspan
x={viewBox.cx}
y={(viewBox.cy || 0) + 24}
className="fill-muted-foreground"
>
%
</tspan>
</text>
);
}
}}
/>
</PolarRadiusAxis>
<RadialBar
dataKey="diskUsed"
stackId="a"
cornerRadius={5}
fill="var(--color-diskUsed)"
className="stroke-transparent stroke-2"
/>
<RadialBar
dataKey="diskReserved"
fill="var(--color-diskReserved)"
stackId="a"
cornerRadius={5}
className="stroke-transparent stroke-2"
/>
<RadialBar
dataKey="diskSchedulable"
fill="var(--color-diskSchedulable)"
stackId="a"
cornerRadius={5}
className="stroke-transparent stroke-2"
/>
</RadialBarChart>
</ChartContainer>
</CardContent>
<CardFooter className="flex-col gap-2 text-sm">
<div className="flex items-center gap-2 font-medium leading-none">
Disk Used + Reserved: {StringUtils.convertBytesToReadableSize(nodeRessource.diskUsageAbsolut + nodeRessource.diskUsageReserved)}
</div>
<div className="flex items-center gap-2 font-medium leading-none">
Disk Schedulable: {StringUtils.convertBytesToReadableSize(nodeRessource.diskSpaceSchedulable)}
</div>
<div className="flex items-center gap-2 font-medium leading-none">
Disk Capacity: {StringUtils.convertBytesToReadableSize(nodeRessource.diskUsageCapacity)}
</div>
</CardFooter>
</Card>
);
}
/*
<CardContent className="flex-1 pb-0">
<ChartContainer
config={chartConfig}
className="mx-auto aspect-square max-h-[250px]">
<RadialBarChart
data={chartData}
innerRadius={80}
outerRadius={110} >
<PolarGrid
gridType="circle"
radialLines={false}
stroke="none"
className="first:fill-muted last:fill-background"
polarRadius={[86, 74]}
/>
<RadialBar
dataKey="diskUsed"
stackId="a"
background
fill="var(--color-diskUsed)"
cornerRadius={10}
/>
<RadialBar
dataKey="diskReserved"
stackId="a"
fill="var(--color-diskReserved)"
background
cornerRadius={10}
/>
<PolarRadiusAxis
tick={false}
tickLine={false}
axisLine={false}
>
<Label
content={({ viewBox }) => {
if (
viewBox &&
'cx' in viewBox &&
'cy' in viewBox
) {
return (
<text
x={viewBox.cx}
y={viewBox.cy}
textAnchor="middle"
dominantBaseline="middle"
>
<tspan
x={viewBox.cx}
y={viewBox.cy}
className="fill-foreground text-4xl font-bold"
>
{(nodeRessource.diskUsageAbsolut / nodeRessource.diskUsageCapacity * 100).toFixed(1)}
</tspan>
<tspan
x={viewBox.cx}
y={(viewBox.cy || 0) + 24}
className="fill-muted-foreground"
>
%
</tspan>
</text>
);
}
}}
/>
</PolarRadiusAxis>
</RadialBarChart>
</ChartContainer>
</CardContent>
*/

View File

@@ -1,4 +1,5 @@
'use client';
import {
Label,
PolarGrid,
@@ -21,8 +22,9 @@ import {
useBreadcrumbs,
} from '@/frontend/states/zustand.states';
import { useEffect } from 'react';
import ChartDiskRessources from './disk-chart';
export default async function ResourcesNodes({
export default function ResourcesNodes({
resourcesNodes,
}: {
resourcesNodes: NodeResourceModel[];
@@ -48,265 +50,187 @@ export default async function ResourcesNodes({
);
return (
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{resourcesNodes.map((Node, index) => (
<div key={index} className="space-y-4 rounded-lg border">
<h3
className={'p-4 rounded-t-lg font-semibold text-xl text-center'}
>
Node {index + 1}:<br/>
{Node.name}
</h3>
<div className="space-y-2 px-4 pb-2">
<Card className="flex flex-col">
<CardHeader className="items-center pb-0">
<CardTitle>CPU</CardTitle>
<CardDescription>Usage in %</CardDescription>
</CardHeader>
<CardContent className="flex-1 pb-0">
<ChartContainer
config={chartConfig}
className="mx-auto aspect-square max-h-[250px]"
<CardContent>
<div className="grid grid-cols-1 gap-8">
{resourcesNodes.map((node, index) => (<>
<h3
className={'p-4 rounded-t-lg font-semibold text-xl text-center'}
>
Node {index + 1}:<br />
{node.name}
</h3>
<div key={index} className="grid grid-cols-1 md:grid-cols-3">
<div className="space-y-2 px-4 pb-2">
<Card className="flex flex-col">
<CardHeader className="items-center pb-0">
<CardTitle>CPU</CardTitle>
<CardDescription>Usage in %</CardDescription>
</CardHeader>
<CardContent className="flex-1 pb-0">
<ChartContainer
config={chartConfig}
className="mx-auto aspect-square max-h-[250px]"
>
<RadialBarChart
data={chartData}
startAngle={0}
endAngle={360 * node.cpuUsageAbsolut / node.cpuUsageCapacity}
innerRadius={80}
outerRadius={110}
>
<RadialBarChart
data={chartData}
startAngle={0}
endAngle={360 * Node.cpuUsageAbsolut / Node.cpuUsageCapacity}
innerRadius={80}
outerRadius={110}
<PolarGrid
gridType="circle"
radialLines={false}
stroke="none"
className="first:fill-muted last:fill-background"
polarRadius={[86, 74]}
/>
<RadialBar
dataKey="usage"
background
cornerRadius={10}
/>
<PolarRadiusAxis
tick={false}
tickLine={false}
axisLine={false}
>
<PolarGrid
gridType="circle"
radialLines={false}
stroke="none"
className="first:fill-muted last:fill-background"
polarRadius={[86, 74]}
/>
<RadialBar
dataKey="usage"
background
cornerRadius={10}
/>
<PolarRadiusAxis
tick={false}
tickLine={false}
axisLine={false}
>
<Label
content={({ viewBox }) => {
if (
viewBox &&
'cx' in viewBox &&
'cy' in viewBox
) {
return (
<text
<Label
content={({ viewBox }) => {
if (
viewBox &&
'cx' in viewBox &&
'cy' in viewBox
) {
return (
<text
x={viewBox.cx}
y={viewBox.cy}
textAnchor="middle"
dominantBaseline="middle"
>
<tspan
x={viewBox.cx}
y={viewBox.cy}
textAnchor="middle"
dominantBaseline="middle"
className="fill-foreground text-4xl font-bold"
>
<tspan
x={viewBox.cx}
y={viewBox.cy}
className="fill-foreground text-4xl font-bold"
>
{(Node.cpuUsageAbsolut / Node.cpuUsageCapacity * 100).toFixed(2)}
</tspan>
<tspan
x={viewBox.cx}
y={(viewBox.cy || 0) + 24}
className="fill-muted-foreground"
>
%
</tspan>
</text>
);
}
}}
/>
</PolarRadiusAxis>
</RadialBarChart>
</ChartContainer>
</CardContent>
<CardFooter className="flex-col gap-2 text-sm">
<div className="flex items-center gap-2 font-medium leading-none">
CPU Absolute: {Node.cpuUsageAbsolut.toFixed(2)} cores
</div>
<div className="flex items-center gap-2 font-medium leading-none">
CPU Capacity: {Node.cpuUsageCapacity.toFixed(2)} cores
</div>
</CardFooter>
</Card>
</div>
<div className="space-y-2 px-4 pb-2">
<Card className="flex flex-col">
<CardHeader className="items-center pb-0">
<CardTitle>RAM</CardTitle>
<CardDescription>Usage in %</CardDescription>
</CardHeader>
<CardContent className="flex-1 pb-0">
<ChartContainer
config={chartConfig}
className="mx-auto aspect-square max-h-[250px]"
>
<RadialBarChart
data={chartData}
startAngle={0}
endAngle={360 * Node.ramUsageAbsolut / Node.ramUsageCapacity}
innerRadius={80}
outerRadius={110}
>
<PolarGrid
gridType="circle"
radialLines={false}
stroke="none"
className="first:fill-muted last:fill-background"
polarRadius={[86, 74]}
/>
<RadialBar
dataKey="usage"
background
cornerRadius={10}
/>
<PolarRadiusAxis
tick={false}
tickLine={false}
axisLine={false}
>
<Label
content={({ viewBox }) => {
if (
viewBox &&
'cx' in viewBox &&
'cy' in viewBox
) {
return (
<text
{(node.cpuUsageAbsolut / node.cpuUsageCapacity * 100).toFixed(1)}
</tspan>
<tspan
x={viewBox.cx}
y={viewBox.cy}
textAnchor="middle"
dominantBaseline="middle"
y={(viewBox.cy || 0) + 24}
className="fill-muted-foreground"
>
<tspan
x={viewBox.cx}
y={viewBox.cy}
className="fill-foreground text-4xl font-bold"
>
{(Node.ramUsageAbsolut / Node.ramUsageCapacity * 100).toFixed(2)}
</tspan>
<tspan
x={viewBox.cx}
y={(viewBox.cy || 0) + 24}
className="fill-muted-foreground"
>
%
</tspan>
</text>
);
}
}}
/>
</PolarRadiusAxis>
</RadialBarChart>
</ChartContainer>
</CardContent>
<CardFooter className="flex-col gap-2 text-sm">
<div className="flex items-center gap-2 font-medium leading-none">
RAM Absolute: {(Node.ramUsageAbsolut / (1024 * 1024 * 1024)).toFixed(2)} GB
</div>
<div className="flex items-center gap-2 font-medium leading-none">
RAM Capacity: {(Node.ramUsageCapacity / (1024 * 1024 * 1024)).toFixed(2)} GB
</div>
</CardFooter>
</Card>
</div>
<div className="space-y-2 px-4 pb-2">
<Card className="flex flex-col">
<CardHeader className="items-center pb-0">
<CardTitle>Disk</CardTitle>
<CardDescription>Usage in %</CardDescription>
</CardHeader>
<CardContent className="flex-1 pb-0">
<ChartContainer
config={chartConfig}
className="mx-auto aspect-square max-h-[250px]"
>
<RadialBarChart
data={chartData}
startAngle={0}
endAngle={360 * Node.diskUsageAbsolut / Node.diskUsageCapacity}
innerRadius={80}
outerRadius={110}
>
<PolarGrid
gridType="circle"
radialLines={false}
stroke="none"
className="first:fill-muted last:fill-background"
polarRadius={[86, 74]}
%
</tspan>
</text>
);
}
}}
/>
<RadialBar
dataKey="usage"
background
cornerRadius={10}
/>
<PolarRadiusAxis
tick={false}
tickLine={false}
axisLine={false}
>
<Label
content={({ viewBox }) => {
if (
viewBox &&
'cx' in viewBox &&
'cy' in viewBox
) {
return (
<text
x={viewBox.cx}
y={viewBox.cy}
textAnchor="middle"
dominantBaseline="middle"
>
<tspan
x={viewBox.cx}
y={viewBox.cy}
className="fill-foreground text-4xl font-bold"
>
{(Node.diskUsageAbsolut / Node.diskUsageCapacity * 100).toFixed(2)}
</tspan>
<tspan
x={viewBox.cx}
y={(viewBox.cy || 0) + 24}
className="fill-muted-foreground"
>
%
</tspan>
</text>
);
}
}}
/>
</PolarRadiusAxis>
</RadialBarChart>
</ChartContainer>
</CardContent>
<CardFooter className="flex-col gap-2 text-sm">
<div className="flex items-center gap-2 font-medium leading-none">
Disk Absolute: {(Node.diskUsageAbsolut / (1024 * 1024 * 1024)).toFixed(2)} GB
</div>
<div className="flex items-center gap-2 font-medium leading-none">
Disk Capacity: {(Node.diskUsageCapacity / (1024 * 1024 * 1024)).toFixed(2)} GB
</div>
</CardFooter>
</Card>
</div>
</PolarRadiusAxis>
</RadialBarChart>
</ChartContainer>
</CardContent>
<CardFooter className="flex-col gap-2 text-sm">
<div className="flex items-center gap-2 font-medium leading-none">
CPU Absolute: {node.cpuUsageAbsolut.toFixed(2)} cores
</div>
<div className="flex items-center gap-2 font-medium leading-none">
CPU Capacity: {node.cpuUsageCapacity.toFixed(2)} cores
</div>
</CardFooter>
</Card>
</div>
))}
</div>
</CardContent>
<div className="space-y-2 px-4 pb-2">
<Card className="flex flex-col">
<CardHeader className="items-center pb-0">
<CardTitle>RAM</CardTitle>
<CardDescription>Usage in %</CardDescription>
</CardHeader>
<CardContent className="flex-1 pb-0">
<ChartContainer
config={chartConfig}
className="mx-auto aspect-square max-h-[250px]"
>
<RadialBarChart
data={chartData}
startAngle={0}
endAngle={360 * node.ramUsageAbsolut / node.ramUsageCapacity}
innerRadius={80}
outerRadius={110}
>
<PolarGrid
gridType="circle"
radialLines={false}
stroke="none"
className="first:fill-muted last:fill-background"
polarRadius={[86, 74]}
/>
<RadialBar
dataKey="usage"
background
cornerRadius={10}
/>
<PolarRadiusAxis
tick={false}
tickLine={false}
axisLine={false}
>
<Label
content={({ viewBox }) => {
if (
viewBox &&
'cx' in viewBox &&
'cy' in viewBox
) {
return (
<text
x={viewBox.cx}
y={viewBox.cy}
textAnchor="middle"
dominantBaseline="middle"
>
<tspan
x={viewBox.cx}
y={viewBox.cy}
className="fill-foreground text-4xl font-bold"
>
{(node.ramUsageAbsolut / node.ramUsageCapacity * 100).toFixed(1)}
</tspan>
<tspan
x={viewBox.cx}
y={(viewBox.cy || 0) + 24}
className="fill-muted-foreground"
>
%
</tspan>
</text>
);
}
}}
/>
</PolarRadiusAxis>
</RadialBarChart>
</ChartContainer>
</CardContent>
<CardFooter className="flex-col gap-2 text-sm">
<div className="flex items-center gap-2 font-medium leading-none">
RAM Absolute: {(node.ramUsageAbsolut / (1024 * 1024 * 1024)).toFixed(2)} GB
</div>
<div className="flex items-center gap-2 font-medium leading-none">
RAM Capacity: {(node.ramUsageCapacity / (1024 * 1024 * 1024)).toFixed(2)} GB
</div>
</CardFooter>
</Card>
</div>
<div className="space-y-2 px-4 pb-2">
<ChartDiskRessources nodeRessource={node} />
</div>
</div>
</>))}
</div>
</CardContent>
);
}

View File

@@ -7,13 +7,13 @@ import ResourceNodes from "./monitoring-nodes";
export default async function ResourceNodesInfoPage() {
await getAuthUserSession();
const resourcesNode = await clusterService.getNodeResourceUsage();
const session = await getAuthUserSession();
return (
<div className="flex-1 space-y-4 pt-6">
<PageTitle
title={'Resources Nodes'}
subtitle={`View all resources of the nodes which belongs to the QuickStack Cluster.`}>
subtitle={`View all resources of the nodes which belong to the QuickStack Cluster.`}>
</PageTitle>
<ResourceNodes resourcesNodes={resourcesNode} />
</div>

View File

@@ -1,9 +1,11 @@
class LonghornApiAdapter {
async getLonghornVolume(pvcName: String) {
let longhornApiUrl = process.env.NODE_ENV === 'production' ? 'http://longhorn-frontend.longhorn-system.svc.cluster.local/v1/volumes' : 'http://localhost:4000/v1/volumes';
get longhornBaseUrl() {
return process.env.NODE_ENV === 'production' ? 'http://longhorn-frontend.longhorn-system.svc.cluster.local' : 'http://localhost:4000';
}
const response = await fetch(`${longhornApiUrl}/${pvcName}`, {
async getLonghornVolume(pvcName: String) {
const response = await fetch(`${this.longhornBaseUrl}/v1/volumes/${pvcName}`, {
cache: 'no-cache',
method: 'GET',
headers: {
@@ -26,9 +28,7 @@ class LonghornApiAdapter {
async getNodeStorageInfo(nodeName: String) {
let longhornApiUrl = process.env.NODE_ENV === 'production' ? 'http://longhorn-frontend.longhorn-system.svc.cluster.local/v1/nodes' : 'http://localhost:4000/v1/nodes';
const response = await fetch(`${longhornApiUrl}/${nodeName}`, {
const response = await fetch(`${this.longhornBaseUrl}/v1/nodes/${nodeName}`, {
cache: 'no-cache',
method: 'GET',
headers: {
@@ -46,7 +46,9 @@ class LonghornApiAdapter {
disks: {
[key: string]: {
storageMaximum: number,
storageAvailable: number
storageAvailable: number,
storageReserved: number,
storageScheduled: number
}
}
};
@@ -57,19 +59,93 @@ class LonghornApiAdapter {
let totalStorageMaximum = 0;
let totalStorageAvailable = 0;
let totalStorageReserved = 0;
let totalStorageScheduled = 0;
Object.values(data.disks).forEach(disk => {
totalStorageMaximum += disk.storageMaximum;
totalStorageAvailable += disk.storageAvailable;
totalStorageReserved += disk.storageReserved;
totalStorageScheduled += disk.storageScheduled;
});
// The available Storage is the total storage minus the reserved storage (which is not available for scheduling --> 30% of disk space)
const totalSchedulableStorage = totalStorageAvailable - totalStorageReserved;
return {
totalStorageMaximum,
totalStorageAvailable
totalStorageAvailable,
totalSchedulableStorage,
totalStorageReserved
};
}
async backupPvc(pvcName: string) {
const snapshot = await this.createSnapshot(pvcName);
await this.createBackup(pvcName, snapshot.id);
}
private async createBackup(pvcName: string, snapshotId: string) {
const response = await fetch(`${this.longhornBaseUrl}/v1/volumes/${pvcName}?action=snapshotBackup`, {
cache: 'no-cache',
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
"name": snapshotId
})
});
if (!response.ok) {
throw new Error(`HTTP-Error: ${response.status}`);
}
return await response.json();
}
private async createSnapshot(pvcName: string) {
const response = await fetch(`${this.longhornBaseUrl}/v1/volumes/${pvcName}?action=snapshotCreate`, {
cache: 'no-cache',
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP-Error: ${response.status}`);
}
/*
Response:
{
"actions": {},
"checksum": "",
"children": {
"volume-head": true
},
"created": "2024-12-30T14:20:34Z",
"id": "79973f60-02d5-4100-acd4-9306112d5c91",
"labels": {},
"links": {
"self": "http://10.42.0.100:9500/v1/snapshots/79973f60-02d5-4100-acd4-9306112d5c91"
},
"name": "79973f60-02d5-4100-acd4-9306112d5c91",
"parent": "4beb4c48-53cf-4eaa-98dc-cc3a91f71e44",
"removed": false,
"size": "0",
"type": "snapshot",
"usercreated": true
}
*/
return await response.json();
}
}
const longhornApiAdapter = new LonghornApiAdapter();
export default longhornApiAdapter;

View File

@@ -0,0 +1,47 @@
/*
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-same-namespace-and-external-traffic
namespace: proj-databases-ce53614e
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector: {}
- from:
- podSelector:
matchLabels:
app.kubernetes.io/name: traefik
- from:
- namespaceSelector:
matchLabels:
name: kube-system
- from:
- ipBlock:
cidr: 0.0.0.0/0
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0
- to:
- namespaceSelector: {}
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- port: 53
protocol: UDP
- to:
- podSelector: {}
*/

View File

@@ -65,7 +65,7 @@ class ClusterService {
async getNodeResourceUsage(): Promise<NodeResourceModel[]> {
const topNodes = await k8s.topNodes(k3s.core);
return await Promise.all (topNodes.map(async (node) => {
return await Promise.all(topNodes.map(async (node) => {
const diskInfo = await longhornApiAdapter.getNodeStorageInfo(node.Node.metadata?.name!);
return {
name: node.Node.metadata?.name!,
@@ -74,9 +74,12 @@ class ClusterService {
ramUsageAbsolut: Number(node.Memory?.RequestTotal!),
ramUsageCapacity: Number(node.Memory?.Capacity!),
diskUsageAbsolut: diskInfo.totalStorageMaximum - diskInfo.totalStorageAvailable,
diskUsageReserved: diskInfo.totalStorageReserved,
diskUsageCapacity: diskInfo.totalStorageMaximum,
}}));
}
diskSpaceSchedulable: diskInfo.totalSchedulableStorage
}
}));
}
}
const clusterService = new ClusterService();

View File

@@ -10,6 +10,8 @@ export const nodeResourceZodModel = z.object({
ramUsageCapacity: z.number(),
diskUsageAbsolut: z.number(),
diskUsageCapacity: z.number(),
diskUsageReserved: z.number(),
diskSpaceSchedulable: z.number(),
})
export type NodeResourceModel = z.infer<typeof nodeResourceZodModel>;