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 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,
|
||||
})),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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={[
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}[]
|
||||
}
|
||||
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"
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user