feat: disk info collection

This commit is contained in:
Jake Turner 2025-12-07 19:13:43 -08:00
parent 2ff7b055b5
commit 5205d5909d
No known key found for this signature in database
GPG Key ID: 694BC38EF2ED4844
8 changed files with 227 additions and 31 deletions

View File

@ -4,13 +4,15 @@ import { DockerService } from '#services/docker_service'
import { ServiceSlim } from '../../types/services.js'
import logger from '@adonisjs/core/services/logger'
import si from 'systeminformation'
import { SystemInformationResponse } from '../../types/system.js'
import { NomadDiskInfo, NomadDiskInfoRaw, SystemInformationResponse } from '../../types/system.js'
import { readFileSync } from 'fs'
import { join } from 'path'
import path, { join } from 'path'
import { getAllFilesystems, getFile } from '../utils/fs.js'
@inject()
export class SystemService {
private static appVersion: string | null = null
private static diskInfoFile = '/storage/nomad-disk-info.json'
constructor(private dockerService: DockerService) {}
@ -75,16 +77,28 @@ export class SystemService {
async getSystemInfo(): Promise<SystemInformationResponse | undefined> {
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.mem(),
si.osInfo(),
si.diskLayout(),
si.currentLoad(),
si.fsSize(),
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 {
cpu,
mem,
@ -99,4 +113,42 @@ export class SystemService {
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,
})),
}
})
}
}

View File

@ -2,6 +2,7 @@ import { mkdir, readdir, readFile, stat, unlink } from 'fs/promises'
import { join } from 'path'
import { FileEntry } from '../../types/files.js'
import { createReadStream } from 'fs'
import { LSBlockDevice, NomadDiskInfoRaw } from '../../types/system.js'
export async function listDirectoryContents(path: string): Promise<FileEntry[]> {
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
}

View File

@ -32,19 +32,6 @@ export default function SettingsPage(props: {
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 (
<SettingsLayout>
<Head title="System Information" />
@ -194,9 +181,17 @@ export default function SettingsPage(props: {
</h2>
<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
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}
statuses={[
{

View File

@ -1,18 +1,62 @@
import { Systeminformation } from "systeminformation"
import { Systeminformation } from 'systeminformation'
export type SystemInformationResponse = {
cpu: Systeminformation.CpuData
mem: Systeminformation.MemData
os: Systeminformation.OsData
disk: Systeminformation.DiskLayoutData[]
currentLoad: Systeminformation.CurrentLoadData
fsSize: Systeminformation.FsSizeData[]
uptime: Systeminformation.TimeData
cpu: Systeminformation.CpuData
mem: Systeminformation.MemData
os: Systeminformation.OsData
disk: NomadDiskInfo[]
currentLoad: Systeminformation.CurrentLoadData
fsSize: Systeminformation.FsSizeData[]
uptime: Systeminformation.TimeData
}
// Type inferrence is not working properly with usePage and shared props, so we define this type manually
export type UsePageProps = {
appVersion: string
environment: string
appVersion: 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
}[]
}

View 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

View File

@ -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"
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"
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'
accepted_terms='false'
@ -286,6 +287,24 @@ download_entrypoint_script() {
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() {
local start_script_path="${NOMAD_DIR}/start_nomad.sh"
local stop_script_path="${NOMAD_DIR}/stop_nomad.sh"
@ -358,6 +377,7 @@ create_nomad_directory
download_wait_for_it_script
download_entrypoint_script
download_helper_scripts
download_and_start_collect_disk_info_script
download_management_compose_file
start_management_containers
success_message

View File

@ -7,6 +7,7 @@ services:
- "8080:8080"
volumes:
- /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
- ./entrypoint.sh:/usr/local/bin/entrypoint.sh
- ./wait-for-it.sh:/usr/local/bin/wait-for-it.sh

View File

@ -17,6 +17,7 @@
NOMAD_DIR="/opt/project-nomad"
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
}
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() {
echo "Stopping and removing Project N.O.M.A.D. management containers..."
docker compose -f "${MANAGEMENT_COMPOSE_FILE}" down
@ -90,6 +101,9 @@ uninstall_nomad() {
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..."
rm -rf "${NOMAD_DIR}"