mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-03-28 03:29:25 +01:00
feat: disk info collection
This commit is contained in:
parent
2ff7b055b5
commit
5205d5909d
|
|
@ -4,13 +4,15 @@ import { DockerService } from '#services/docker_service'
|
||||||
import { ServiceSlim } from '../../types/services.js'
|
import { ServiceSlim } from '../../types/services.js'
|
||||||
import logger from '@adonisjs/core/services/logger'
|
import logger from '@adonisjs/core/services/logger'
|
||||||
import si from 'systeminformation'
|
import si from 'systeminformation'
|
||||||
import { SystemInformationResponse } from '../../types/system.js'
|
import { NomadDiskInfo, NomadDiskInfoRaw, SystemInformationResponse } from '../../types/system.js'
|
||||||
import { readFileSync } from 'fs'
|
import { readFileSync } from 'fs'
|
||||||
import { join } from 'path'
|
import path, { join } from 'path'
|
||||||
|
import { getAllFilesystems, getFile } from '../utils/fs.js'
|
||||||
|
|
||||||
@inject()
|
@inject()
|
||||||
export class SystemService {
|
export class SystemService {
|
||||||
private static appVersion: string | null = null
|
private static appVersion: string | null = null
|
||||||
|
private static diskInfoFile = '/storage/nomad-disk-info.json'
|
||||||
|
|
||||||
constructor(private dockerService: DockerService) {}
|
constructor(private dockerService: DockerService) {}
|
||||||
|
|
||||||
|
|
@ -75,16 +77,28 @@ export class SystemService {
|
||||||
|
|
||||||
async getSystemInfo(): Promise<SystemInformationResponse | undefined> {
|
async getSystemInfo(): Promise<SystemInformationResponse | undefined> {
|
||||||
try {
|
try {
|
||||||
const [cpu, mem, os, disk, currentLoad, fsSize, uptime] = await Promise.all([
|
const [cpu, mem, os, currentLoad, fsSize, uptime] = await Promise.all([
|
||||||
si.cpu(),
|
si.cpu(),
|
||||||
si.mem(),
|
si.mem(),
|
||||||
si.osInfo(),
|
si.osInfo(),
|
||||||
si.diskLayout(),
|
|
||||||
si.currentLoad(),
|
si.currentLoad(),
|
||||||
si.fsSize(),
|
si.fsSize(),
|
||||||
si.time(),
|
si.time(),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const diskInfoRawString = await getFile(
|
||||||
|
path.join(process.cwd(), SystemService.diskInfoFile),
|
||||||
|
'string'
|
||||||
|
)
|
||||||
|
|
||||||
|
const diskInfo = (
|
||||||
|
diskInfoRawString
|
||||||
|
? JSON.parse(diskInfoRawString.toString())
|
||||||
|
: { diskLayout: { blockdevices: [] }, fsSize: [] }
|
||||||
|
) as NomadDiskInfoRaw
|
||||||
|
|
||||||
|
const disk = this.calculateDiskUsage(diskInfo)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
cpu,
|
cpu,
|
||||||
mem,
|
mem,
|
||||||
|
|
@ -99,4 +113,42 @@ export class SystemService {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private calculateDiskUsage(diskInfo: NomadDiskInfoRaw): NomadDiskInfo[] {
|
||||||
|
const { diskLayout, fsSize } = diskInfo
|
||||||
|
|
||||||
|
if (!diskLayout?.blockdevices || !fsSize) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return diskLayout.blockdevices
|
||||||
|
.filter((disk) => disk.type === 'disk') // Only physical disks
|
||||||
|
.map((disk) => {
|
||||||
|
const filesystems = getAllFilesystems(disk, fsSize)
|
||||||
|
|
||||||
|
// Across all partitions
|
||||||
|
const totalUsed = filesystems.reduce((sum, p) => sum + (p.used || 0), 0)
|
||||||
|
const totalSize = filesystems.reduce((sum, p) => sum + (p.size || 0), 0)
|
||||||
|
const percentUsed = totalSize > 0 ? (totalUsed / totalSize) * 100 : 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: disk.name,
|
||||||
|
model: disk.model || 'Unknown',
|
||||||
|
vendor: disk.vendor || '',
|
||||||
|
rota: disk.rota || false,
|
||||||
|
tran: disk.tran || '',
|
||||||
|
size: disk.size,
|
||||||
|
totalUsed,
|
||||||
|
totalSize,
|
||||||
|
percentUsed: Math.round(percentUsed * 100) / 100,
|
||||||
|
filesystems: filesystems.map((p) => ({
|
||||||
|
fs: p.fs,
|
||||||
|
mount: p.mount,
|
||||||
|
used: p.used,
|
||||||
|
size: p.size,
|
||||||
|
percentUsed: p.use,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { mkdir, readdir, readFile, stat, unlink } from 'fs/promises'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { FileEntry } from '../../types/files.js'
|
import { FileEntry } from '../../types/files.js'
|
||||||
import { createReadStream } from 'fs'
|
import { createReadStream } from 'fs'
|
||||||
|
import { LSBlockDevice, NomadDiskInfoRaw } from '../../types/system.js'
|
||||||
|
|
||||||
export async function listDirectoryContents(path: string): Promise<FileEntry[]> {
|
export async function listDirectoryContents(path: string): Promise<FileEntry[]> {
|
||||||
const entries = await readdir(path, { withFileTypes: true })
|
const entries = await readdir(path, { withFileTypes: true })
|
||||||
|
|
@ -104,3 +105,47 @@ export async function deleteFileIfExists(path: string): Promise<void> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getAllFilesystems(
|
||||||
|
device: LSBlockDevice,
|
||||||
|
fsSize: NomadDiskInfoRaw['fsSize']
|
||||||
|
): NomadDiskInfoRaw['fsSize'] {
|
||||||
|
const filesystems: NomadDiskInfoRaw['fsSize'] = []
|
||||||
|
const seen = new Set()
|
||||||
|
|
||||||
|
function traverse(dev: LSBlockDevice) {
|
||||||
|
// Try to find matching filesystem
|
||||||
|
const fs = fsSize.find((f) => matchesDevice(f.fs, dev.name))
|
||||||
|
|
||||||
|
if (fs && !seen.has(fs.fs)) {
|
||||||
|
filesystems.push(fs)
|
||||||
|
seen.add(fs.fs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Traverse children recursively
|
||||||
|
if (dev.children) {
|
||||||
|
dev.children.forEach((child) => traverse(child))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
traverse(device)
|
||||||
|
return filesystems
|
||||||
|
}
|
||||||
|
|
||||||
|
export function matchesDevice(fsPath: string, deviceName: string): boolean {
|
||||||
|
// Remove /dev/ and /dev/mapper/ prefixes
|
||||||
|
const normalized = fsPath.replace('/dev/mapper/', '').replace('/dev/', '')
|
||||||
|
|
||||||
|
// Direct match
|
||||||
|
if (normalized === deviceName) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// LVM volumes use dashes instead of slashes
|
||||||
|
// e.g., ubuntu--vg-ubuntu--lv matches the device name
|
||||||
|
if (fsPath.includes(deviceName)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,19 +32,6 @@ export default function SettingsPage(props: {
|
||||||
|
|
||||||
const uptimeMinutes = info?.uptime.uptime ? Math.floor(info.uptime.uptime / 60) : 0
|
const uptimeMinutes = info?.uptime.uptime ? Math.floor(info.uptime.uptime / 60) : 0
|
||||||
|
|
||||||
const diskData = info?.disk.map((disk) => {
|
|
||||||
const usedBytes = (disk.size || 0) * 0.65 // Estimate - you'd get this from mount points
|
|
||||||
const usedPercent = disk.size ? (usedBytes / disk.size) * 100 : 0
|
|
||||||
|
|
||||||
return {
|
|
||||||
label: disk.name || 'Unknown',
|
|
||||||
value: usedPercent,
|
|
||||||
total: formatBytes(disk.size || 0),
|
|
||||||
used: formatBytes(usedBytes),
|
|
||||||
type: disk.type,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsLayout>
|
<SettingsLayout>
|
||||||
<Head title="System Information" />
|
<Head title="System Information" />
|
||||||
|
|
@ -194,9 +181,17 @@ export default function SettingsPage(props: {
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div className="bg-desert-white rounded-lg p-8 border border-desert-stone-light shadow-sm hover:shadow-lg transition-shadow">
|
<div className="bg-desert-white rounded-lg p-8 border border-desert-stone-light shadow-sm hover:shadow-lg transition-shadow">
|
||||||
{diskData && diskData.length > 0 ? (
|
{info?.disk && info.disk.length > 0 ? (
|
||||||
<HorizontalBarChart
|
<HorizontalBarChart
|
||||||
items={diskData}
|
items={info.disk.map((disk) => ({
|
||||||
|
label: disk.name || 'Unknown',
|
||||||
|
value: disk.percentUsed || 0,
|
||||||
|
total: disk.totalSize ? formatBytes(disk.totalSize) : 'N/A',
|
||||||
|
used: disk.totalUsed ? formatBytes(disk.totalUsed) : 'N/A',
|
||||||
|
subtext: `${formatBytes(disk.totalUsed || 0)} / ${formatBytes(
|
||||||
|
disk.totalSize || 0
|
||||||
|
)}`,
|
||||||
|
}))}
|
||||||
progressiveBarColor={true}
|
progressiveBarColor={true}
|
||||||
statuses={[
|
statuses={[
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,62 @@
|
||||||
import { Systeminformation } from "systeminformation"
|
import { Systeminformation } from 'systeminformation'
|
||||||
|
|
||||||
|
|
||||||
export type SystemInformationResponse = {
|
export type SystemInformationResponse = {
|
||||||
cpu: Systeminformation.CpuData
|
cpu: Systeminformation.CpuData
|
||||||
mem: Systeminformation.MemData
|
mem: Systeminformation.MemData
|
||||||
os: Systeminformation.OsData
|
os: Systeminformation.OsData
|
||||||
disk: Systeminformation.DiskLayoutData[]
|
disk: NomadDiskInfo[]
|
||||||
currentLoad: Systeminformation.CurrentLoadData
|
currentLoad: Systeminformation.CurrentLoadData
|
||||||
fsSize: Systeminformation.FsSizeData[]
|
fsSize: Systeminformation.FsSizeData[]
|
||||||
uptime: Systeminformation.TimeData
|
uptime: Systeminformation.TimeData
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type inferrence is not working properly with usePage and shared props, so we define this type manually
|
// Type inferrence is not working properly with usePage and shared props, so we define this type manually
|
||||||
export type UsePageProps = {
|
export type UsePageProps = {
|
||||||
appVersion: string
|
appVersion: string
|
||||||
environment: string
|
environment: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type LSBlockDevice = {
|
||||||
|
name: string
|
||||||
|
size: string
|
||||||
|
type: string
|
||||||
|
model: string | null
|
||||||
|
serial: string | null
|
||||||
|
vendor: string | null
|
||||||
|
rota: boolean | null
|
||||||
|
tran: string | null
|
||||||
|
children?: LSBlockDevice[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NomadDiskInfoRaw = {
|
||||||
|
diskLayout: {
|
||||||
|
blockdevices: LSBlockDevice[]
|
||||||
|
}
|
||||||
|
fsSize: {
|
||||||
|
fs: string
|
||||||
|
size: number
|
||||||
|
used: number
|
||||||
|
available: number
|
||||||
|
use: number
|
||||||
|
mount: string
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NomadDiskInfo = {
|
||||||
|
name: string
|
||||||
|
model: string
|
||||||
|
vendor: string
|
||||||
|
rota: boolean
|
||||||
|
tran: string
|
||||||
|
size: string
|
||||||
|
totalUsed: number
|
||||||
|
totalSize: number
|
||||||
|
percentUsed: number
|
||||||
|
filesystems: {
|
||||||
|
fs: string
|
||||||
|
mount: string
|
||||||
|
used: number
|
||||||
|
size: number
|
||||||
|
percentUsed: number
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
|
||||||
25
install/collect_disk_info.sh
Normal file
25
install/collect_disk_info.sh
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
DISK_LAYOUT=$(lsblk --json -o NAME,SIZE,TYPE,MODEL,SERIAL,VENDOR,ROTA,TRAN)
|
||||||
|
|
||||||
|
# Get filesystem usage excluding pseudo filesystems
|
||||||
|
FS_SIZE=$(df -B1 -x tmpfs -x devtmpfs -x squashfs | tail -n +2 | \
|
||||||
|
awk 'BEGIN {print "["}
|
||||||
|
{
|
||||||
|
if (NR > 1) printf ","
|
||||||
|
gsub(/%/, "", $5)
|
||||||
|
printf "{\"fs\":\"%s\",\"size\":%s,\"used\":%s,\"available\":%s,\"use\":%s,\"mount\":\"%s\"}",
|
||||||
|
$1, $2, $3, $4, $5, $6
|
||||||
|
}
|
||||||
|
END {print "]"}')
|
||||||
|
|
||||||
|
cat > /tmp/nomad-disk-info.json << EOF
|
||||||
|
{
|
||||||
|
"diskLayout": $DISK_LAYOUT,
|
||||||
|
"fsSize": $FS_SIZE
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
sleep 300
|
||||||
|
done
|
||||||
|
|
@ -36,6 +36,7 @@ START_SCRIPT_URL="https://raw.githubusercontent.com/Crosstalk-Solutions/project-
|
||||||
STOP_SCRIPT_URL="https://raw.githubusercontent.com/Crosstalk-Solutions/project-nomad/refs/heads/master/install/stop_nomad.sh"
|
STOP_SCRIPT_URL="https://raw.githubusercontent.com/Crosstalk-Solutions/project-nomad/refs/heads/master/install/stop_nomad.sh"
|
||||||
UPDATE_SCRIPT_URL="https://raw.githubusercontent.com/Crosstalk-Solutions/project-nomad/refs/heads/master/install/update_nomad.sh"
|
UPDATE_SCRIPT_URL="https://raw.githubusercontent.com/Crosstalk-Solutions/project-nomad/refs/heads/master/install/update_nomad.sh"
|
||||||
WAIT_FOR_IT_SCRIPT_URL="https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh"
|
WAIT_FOR_IT_SCRIPT_URL="https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh"
|
||||||
|
COLLECT_DISK_INFO_SCRIPT_URL="https://raw.githubusercontent.com/Crosstalk-Solutions/project-nomad/refs/heads/master/install/collect_disk_info.sh"
|
||||||
|
|
||||||
script_option_debug='true'
|
script_option_debug='true'
|
||||||
accepted_terms='false'
|
accepted_terms='false'
|
||||||
|
|
@ -286,6 +287,24 @@ download_entrypoint_script() {
|
||||||
echo -e "${GREEN}#${RESET} entrypoint script downloaded successfully to $entrypoint_script_path.\\n"
|
echo -e "${GREEN}#${RESET} entrypoint script downloaded successfully to $entrypoint_script_path.\\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
download_and_start_collect_disk_info_script() {
|
||||||
|
local collect_disk_info_script_path="${NOMAD_DIR}/collect_disk_info.sh"
|
||||||
|
|
||||||
|
echo -e "${YELLOW}#${RESET} Downloading collect_disk_info script...\\n"
|
||||||
|
if ! curl -fsSL "$COLLECT_DISK_INFO_SCRIPT_URL" -o "$collect_disk_info_script_path"; then
|
||||||
|
echo -e "${RED}#${RESET} Failed to download the collect_disk_info script. Please check the URL and try again."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
chmod +x "$collect_disk_info_script_path"
|
||||||
|
echo -e "${GREEN}#${RESET} collect_disk_info script downloaded successfully to $collect_disk_info_script_path.\\n"
|
||||||
|
|
||||||
|
# Start script in background and store PID for easy removal on uninstall
|
||||||
|
echo -e "${YELLOW}#${RESET} Starting collect_disk_info script in the background...\\n"
|
||||||
|
nohup bash "$collect_disk_info_script_path" > /dev/null 2>&1 &
|
||||||
|
echo $! > "${NOMAD_DIR}/nomad-collect-disk-info.pid"
|
||||||
|
echo -e "${GREEN}#${RESET} collect_disk_info script started successfully.\\n"
|
||||||
|
}
|
||||||
|
|
||||||
download_helper_scripts() {
|
download_helper_scripts() {
|
||||||
local start_script_path="${NOMAD_DIR}/start_nomad.sh"
|
local start_script_path="${NOMAD_DIR}/start_nomad.sh"
|
||||||
local stop_script_path="${NOMAD_DIR}/stop_nomad.sh"
|
local stop_script_path="${NOMAD_DIR}/stop_nomad.sh"
|
||||||
|
|
@ -358,6 +377,7 @@ create_nomad_directory
|
||||||
download_wait_for_it_script
|
download_wait_for_it_script
|
||||||
download_entrypoint_script
|
download_entrypoint_script
|
||||||
download_helper_scripts
|
download_helper_scripts
|
||||||
|
download_and_start_collect_disk_info_script
|
||||||
download_management_compose_file
|
download_management_compose_file
|
||||||
start_management_containers
|
start_management_containers
|
||||||
success_message
|
success_message
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ services:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
volumes:
|
volumes:
|
||||||
- /opt/project-nomad/storage:/app/storage
|
- /opt/project-nomad/storage:/app/storage
|
||||||
|
- /tmp/nomad-disk-info.json:/app/storage/disk-info.json
|
||||||
- /var/run/docker.sock:/var/run/docker.sock # Allows the admin service to communicate with the Host's Docker daemon
|
- /var/run/docker.sock:/var/run/docker.sock # Allows the admin service to communicate with the Host's Docker daemon
|
||||||
- ./entrypoint.sh:/usr/local/bin/entrypoint.sh
|
- ./entrypoint.sh:/usr/local/bin/entrypoint.sh
|
||||||
- ./wait-for-it.sh:/usr/local/bin/wait-for-it.sh
|
- ./wait-for-it.sh:/usr/local/bin/wait-for-it.sh
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
NOMAD_DIR="/opt/project-nomad"
|
NOMAD_DIR="/opt/project-nomad"
|
||||||
MANAGEMENT_COMPOSE_FILE="${NOMAD_DIR}/compose.yml"
|
MANAGEMENT_COMPOSE_FILE="${NOMAD_DIR}/compose.yml"
|
||||||
|
COLLECT_DISK_INFO_PID="/var/run/nomad-collect-disk-info.pid"
|
||||||
|
|
||||||
###################################################################################################################################################################################################
|
###################################################################################################################################################################################################
|
||||||
# #
|
# #
|
||||||
|
|
@ -75,6 +76,16 @@ ensure_docker_installed() {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try_remove_disk_info_script() {
|
||||||
|
echo "Checking for running collect-disk-info script..."
|
||||||
|
if [ -f "$COLLECT_DISK_INFO_PID" ]; then
|
||||||
|
echo "Stopping collect-disk-info script..."
|
||||||
|
kill "$(cat "$COLLECT_DISK_INFO_PID")"
|
||||||
|
rm -f "$COLLECT_DISK_INFO_PID"
|
||||||
|
echo "collect-disk-info script stopped."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
uninstall_nomad() {
|
uninstall_nomad() {
|
||||||
echo "Stopping and removing Project N.O.M.A.D. management containers..."
|
echo "Stopping and removing Project N.O.M.A.D. management containers..."
|
||||||
docker compose -f "${MANAGEMENT_COMPOSE_FILE}" down
|
docker compose -f "${MANAGEMENT_COMPOSE_FILE}" down
|
||||||
|
|
@ -90,6 +101,9 @@ uninstall_nomad() {
|
||||||
|
|
||||||
echo "Containers should be stopped now."
|
echo "Containers should be stopped now."
|
||||||
|
|
||||||
|
# Try to stop the collect-disk-info script if it's running
|
||||||
|
try_remove_disk_info_script
|
||||||
|
|
||||||
echo "Removing Project N.O.M.A.D. files..."
|
echo "Removing Project N.O.M.A.D. files..."
|
||||||
rm -rf "${NOMAD_DIR}"
|
rm -rf "${NOMAD_DIR}"
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user