This commit is contained in:
2025-12-14 10:40:54 +01:00
parent 5d483b0df5
commit 8428bf9c82
55 changed files with 9763 additions and 391 deletions

View File

@@ -49,71 +49,88 @@ async def submit_benchmark(
# Update device timestamp
device.updated_at = datetime.utcnow()
# 2. Create hardware snapshot
# 2. Get or create hardware snapshot
hw = payload.hardware
snapshot = HardwareSnapshot(
device_id=device.id,
captured_at=datetime.utcnow(),
# CPU
cpu_vendor=hw.cpu.vendor if hw.cpu else None,
cpu_model=hw.cpu.model if hw.cpu else None,
cpu_microarchitecture=hw.cpu.microarchitecture if hw.cpu else None,
cpu_cores=hw.cpu.cores if hw.cpu else None,
cpu_threads=hw.cpu.threads if hw.cpu else None,
cpu_base_freq_ghz=hw.cpu.base_freq_ghz if hw.cpu else None,
cpu_max_freq_ghz=hw.cpu.max_freq_ghz if hw.cpu else None,
cpu_cache_l1_kb=hw.cpu.cache_l1_kb if hw.cpu else None,
cpu_cache_l2_kb=hw.cpu.cache_l2_kb if hw.cpu else None,
cpu_cache_l3_kb=hw.cpu.cache_l3_kb if hw.cpu else None,
cpu_flags=json.dumps(hw.cpu.flags) if hw.cpu and hw.cpu.flags else None,
cpu_tdp_w=hw.cpu.tdp_w if hw.cpu else None,
# Check if we have an existing snapshot for this device
existing_snapshot = db.query(HardwareSnapshot).filter(
HardwareSnapshot.device_id == device.id
).order_by(HardwareSnapshot.captured_at.desc()).first()
# RAM
ram_total_mb=hw.ram.total_mb if hw.ram else None,
ram_used_mb=hw.ram.used_mb if hw.ram else None, # NEW
ram_free_mb=hw.ram.free_mb if hw.ram else None, # NEW
ram_shared_mb=hw.ram.shared_mb if hw.ram else None, # NEW
ram_slots_total=hw.ram.slots_total if hw.ram else None,
ram_slots_used=hw.ram.slots_used if hw.ram else None,
ram_ecc=hw.ram.ecc if hw.ram else None,
ram_layout_json=json.dumps([slot.dict() for slot in hw.ram.layout]) if hw.ram and hw.ram.layout else None,
# If we have an existing snapshot, update it instead of creating a new one
if existing_snapshot:
snapshot = existing_snapshot
snapshot.captured_at = datetime.utcnow() # Update timestamp
else:
# Create new snapshot if none exists
snapshot = HardwareSnapshot(
device_id=device.id,
captured_at=datetime.utcnow()
)
# GPU
gpu_summary=f"{hw.gpu.vendor} {hw.gpu.model}" if hw.gpu and hw.gpu.model else None,
gpu_vendor=hw.gpu.vendor if hw.gpu else None,
gpu_model=hw.gpu.model if hw.gpu else None,
gpu_driver_version=hw.gpu.driver_version if hw.gpu else None,
gpu_memory_dedicated_mb=hw.gpu.memory_dedicated_mb if hw.gpu else None,
gpu_memory_shared_mb=hw.gpu.memory_shared_mb if hw.gpu else None,
gpu_api_support=json.dumps(hw.gpu.api_support) if hw.gpu and hw.gpu.api_support else None,
# Update all fields (whether new or existing snapshot)
# CPU
snapshot.cpu_vendor = hw.cpu.vendor if hw.cpu else None
snapshot.cpu_model = hw.cpu.model if hw.cpu else None
snapshot.cpu_microarchitecture = hw.cpu.microarchitecture if hw.cpu else None
snapshot.cpu_cores = hw.cpu.cores if hw.cpu else None
snapshot.cpu_threads = hw.cpu.threads if hw.cpu else None
snapshot.cpu_base_freq_ghz = hw.cpu.base_freq_ghz if hw.cpu else None
snapshot.cpu_max_freq_ghz = hw.cpu.max_freq_ghz if hw.cpu else None
snapshot.cpu_cache_l1_kb = hw.cpu.cache_l1_kb if hw.cpu else None
snapshot.cpu_cache_l2_kb = hw.cpu.cache_l2_kb if hw.cpu else None
snapshot.cpu_cache_l3_kb = hw.cpu.cache_l3_kb if hw.cpu else None
snapshot.cpu_flags = json.dumps(hw.cpu.flags) if hw.cpu and hw.cpu.flags else None
snapshot.cpu_tdp_w = hw.cpu.tdp_w if hw.cpu else None
# Storage
storage_summary=f"{len(hw.storage.devices)} device(s)" if hw.storage and hw.storage.devices else None,
storage_devices_json=json.dumps([d.dict() for d in hw.storage.devices]) if hw.storage and hw.storage.devices else None,
partitions_json=json.dumps([p.dict() for p in hw.storage.partitions]) if hw.storage and hw.storage.partitions else None,
# RAM
snapshot.ram_total_mb = hw.ram.total_mb if hw.ram else None
snapshot.ram_used_mb = hw.ram.used_mb if hw.ram else None
snapshot.ram_free_mb = hw.ram.free_mb if hw.ram else None
snapshot.ram_shared_mb = hw.ram.shared_mb if hw.ram else None
snapshot.ram_slots_total = hw.ram.slots_total if hw.ram else None
snapshot.ram_slots_used = hw.ram.slots_used if hw.ram else None
snapshot.ram_ecc = hw.ram.ecc if hw.ram else None
snapshot.ram_layout_json = json.dumps([slot.dict() for slot in hw.ram.layout]) if hw.ram and hw.ram.layout else None
# Network
network_interfaces_json=json.dumps([i.dict() for i in hw.network.interfaces]) if hw.network and hw.network.interfaces else None,
# GPU
snapshot.gpu_summary = f"{hw.gpu.vendor} {hw.gpu.model}" if hw.gpu and hw.gpu.model else None
snapshot.gpu_vendor = hw.gpu.vendor if hw.gpu else None
snapshot.gpu_model = hw.gpu.model if hw.gpu else None
snapshot.gpu_driver_version = hw.gpu.driver_version if hw.gpu else None
snapshot.gpu_memory_dedicated_mb = hw.gpu.memory_dedicated_mb if hw.gpu else None
snapshot.gpu_memory_shared_mb = hw.gpu.memory_shared_mb if hw.gpu else None
snapshot.gpu_api_support = json.dumps(hw.gpu.api_support) if hw.gpu and hw.gpu.api_support else None
# OS / Motherboard
os_name=hw.os.name if hw.os else None,
os_version=hw.os.version if hw.os else None,
kernel_version=hw.os.kernel_version if hw.os else None,
architecture=hw.os.architecture if hw.os else None,
virtualization_type=hw.os.virtualization_type if hw.os else None,
motherboard_vendor=hw.motherboard.vendor if hw.motherboard else None,
motherboard_model=hw.motherboard.model if hw.motherboard else None,
bios_version=hw.motherboard.bios_version if hw.motherboard else None,
bios_date=hw.motherboard.bios_date if hw.motherboard else None,
# Storage
snapshot.storage_summary = f"{len(hw.storage.devices)} device(s)" if hw.storage and hw.storage.devices else None
snapshot.storage_devices_json = json.dumps([d.dict() for d in hw.storage.devices]) if hw.storage and hw.storage.devices else None
snapshot.partitions_json = json.dumps([p.dict() for p in hw.storage.partitions]) if hw.storage and hw.storage.partitions else None
# Misc
sensors_json=json.dumps(hw.sensors.dict()) if hw.sensors else None,
raw_info_json=json.dumps(hw.raw_info.dict()) if hw.raw_info else None
)
# Network
snapshot.network_interfaces_json = json.dumps([i.dict() for i in hw.network.interfaces]) if hw.network and hw.network.interfaces else None
db.add(snapshot)
db.flush() # Get snapshot.id
# OS / Motherboard
snapshot.os_name = hw.os.name if hw.os else None
snapshot.os_version = hw.os.version if hw.os else None
snapshot.kernel_version = hw.os.kernel_version if hw.os else None
snapshot.architecture = hw.os.architecture if hw.os else None
snapshot.virtualization_type = hw.os.virtualization_type if hw.os else None
snapshot.motherboard_vendor = hw.motherboard.vendor if hw.motherboard else None
snapshot.motherboard_model = hw.motherboard.model if hw.motherboard else None
snapshot.bios_vendor = hw.motherboard.bios_vendor if hw.motherboard and hasattr(hw.motherboard, 'bios_vendor') else None
snapshot.bios_version = hw.motherboard.bios_version if hw.motherboard else None
snapshot.bios_date = hw.motherboard.bios_date if hw.motherboard else None
# Misc
snapshot.sensors_json = json.dumps(hw.sensors.dict()) if hw.sensors else None
snapshot.raw_info_json = json.dumps(hw.raw_info.dict()) if hw.raw_info else None
# Add to session only if it's a new snapshot
if not existing_snapshot:
db.add(snapshot)
db.flush() # Get snapshot.id for new snapshots
# 3. Create benchmark
results = payload.results

View File

@@ -11,9 +11,11 @@ from app.db.session import get_db
from app.schemas.device import DeviceListResponse, DeviceDetail, DeviceSummary, DeviceUpdate
from app.schemas.benchmark import BenchmarkSummary
from app.schemas.hardware import HardwareSnapshotResponse
from app.schemas.document import DocumentResponse
from app.models.device import Device
from app.models.benchmark import Benchmark
from app.models.hardware_snapshot import HardwareSnapshot
from app.models.document import Document
router = APIRouter()
@@ -160,6 +162,24 @@ async def get_device(
motherboard_model=last_snapshot.motherboard_model
)
# Get documents for this device
documents = db.query(Document).filter(
Document.device_id == device.id
).all()
documents_list = [
DocumentResponse(
id=doc.id,
device_id=doc.device_id,
doc_type=doc.doc_type,
filename=doc.filename,
mime_type=doc.mime_type,
size_bytes=doc.size_bytes,
uploaded_at=doc.uploaded_at.isoformat()
)
for doc in documents
]
return DeviceDetail(
id=device.id,
hostname=device.hostname,
@@ -172,7 +192,8 @@ async def get_device(
created_at=device.created_at.isoformat(),
updated_at=device.updated_at.isoformat(),
last_benchmark=last_bench_summary,
last_hardware_snapshot=last_snapshot_data
last_hardware_snapshot=last_snapshot_data,
documents=documents_list
)
@@ -246,7 +267,9 @@ async def update_device(
for key, value in update_dict.items():
setattr(device, key, value)
device.updated_at = db.query(Device).filter(Device.id == device_id).first().updated_at
# Update timestamp
from datetime import datetime
device.updated_at = datetime.utcnow()
db.commit()
db.refresh(device)

View File

@@ -2,12 +2,14 @@
Linux BenchTools - Main Application
"""
from fastapi import FastAPI
from fastapi import FastAPI, Depends
from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager
from sqlalchemy.orm import Session
from app.core.config import settings
from app.db.init_db import init_db
from app.db.session import get_db
from app.api import benchmark, devices, links, docs
@@ -68,37 +70,28 @@ async def health_check():
# Stats endpoint (for dashboard)
@app.get(f"{settings.API_PREFIX}/stats")
async def get_stats():
async def get_stats(db: Session = Depends(get_db)):
"""Get global statistics"""
from sqlalchemy.orm import Session
from app.db.session import get_db
from app.models.device import Device
from app.models.benchmark import Benchmark
from sqlalchemy import func
db: Session = next(get_db())
total_devices = db.query(Device).count()
total_benchmarks = db.query(Benchmark).count()
try:
total_devices = db.query(Device).count()
total_benchmarks = db.query(Benchmark).count()
# Get average score
avg_score = db.query(func.avg(Benchmark.global_score)).scalar()
# Get average score
avg_score = db.query(Benchmark).with_entities(
db.func.avg(Benchmark.global_score)
).scalar()
# Get last benchmark date
last_bench = db.query(Benchmark).order_by(Benchmark.run_at.desc()).first()
last_bench_date = last_bench.run_at.isoformat() if last_bench else None
# Get last benchmark date
last_bench = db.query(Benchmark).order_by(Benchmark.run_at.desc()).first()
last_bench_date = last_bench.run_at.isoformat() if last_bench else None
return {
"total_devices": total_devices,
"total_benchmarks": total_benchmarks,
"avg_global_score": round(avg_score, 2) if avg_score else 0,
"last_benchmark_at": last_bench_date
}
finally:
db.close()
return {
"total_devices": total_devices,
"total_benchmarks": total_benchmarks,
"avg_global_score": round(avg_score, 2) if avg_score else 0,
"last_benchmark_at": last_bench_date
}
if __name__ == "__main__":

View File

@@ -67,6 +67,7 @@ class HardwareSnapshot(Base):
virtualization_type = Column(String(50), nullable=True)
motherboard_vendor = Column(String(100), nullable=True)
motherboard_model = Column(String(255), nullable=True)
bios_vendor = Column(String(100), nullable=True)
bios_version = Column(String(100), nullable=True)
bios_date = Column(String(50), nullable=True)

View File

@@ -9,41 +9,41 @@ from app.schemas.hardware import HardwareData
class CPUResults(BaseModel):
"""CPU benchmark results"""
events_per_sec: Optional[float] = None
duration_s: Optional[float] = None
score: Optional[float] = None
events_per_sec: Optional[float] = Field(None, ge=0)
duration_s: Optional[float] = Field(None, ge=0)
score: Optional[float] = Field(None, ge=0, le=10000)
class MemoryResults(BaseModel):
"""Memory benchmark results"""
throughput_mib_s: Optional[float] = None
score: Optional[float] = None
throughput_mib_s: Optional[float] = Field(None, ge=0)
score: Optional[float] = Field(None, ge=0, le=10000)
class DiskResults(BaseModel):
"""Disk benchmark results"""
read_mb_s: Optional[float] = None
write_mb_s: Optional[float] = None
iops_read: Optional[int] = None
iops_write: Optional[int] = None
latency_ms: Optional[float] = None
score: Optional[float] = None
read_mb_s: Optional[float] = Field(None, ge=0)
write_mb_s: Optional[float] = Field(None, ge=0)
iops_read: Optional[int] = Field(None, ge=0)
iops_write: Optional[int] = Field(None, ge=0)
latency_ms: Optional[float] = Field(None, ge=0)
score: Optional[float] = Field(None, ge=0, le=10000)
class NetworkResults(BaseModel):
"""Network benchmark results"""
upload_mbps: Optional[float] = None
download_mbps: Optional[float] = None
ping_ms: Optional[float] = None
jitter_ms: Optional[float] = None
packet_loss_percent: Optional[float] = None
score: Optional[float] = None
upload_mbps: Optional[float] = Field(None, ge=0)
download_mbps: Optional[float] = Field(None, ge=0)
ping_ms: Optional[float] = Field(None, ge=0)
jitter_ms: Optional[float] = Field(None, ge=0)
packet_loss_percent: Optional[float] = Field(None, ge=0, le=100)
score: Optional[float] = Field(None, ge=0, le=10000)
class GPUResults(BaseModel):
"""GPU benchmark results"""
glmark2_score: Optional[int] = None
score: Optional[float] = None
glmark2_score: Optional[int] = Field(None, ge=0)
score: Optional[float] = Field(None, ge=0, le=10000)
class BenchmarkResults(BaseModel):
@@ -53,7 +53,7 @@ class BenchmarkResults(BaseModel):
disk: Optional[DiskResults] = None
network: Optional[NetworkResults] = None
gpu: Optional[GPUResults] = None
global_score: float = Field(..., ge=0, le=100, description="Global score (0-100)")
global_score: float = Field(..., ge=0, le=10000, description="Global score (0-10000)")
class BenchmarkPayload(BaseModel):

View File

@@ -6,6 +6,7 @@ from pydantic import BaseModel
from typing import Optional, List
from app.schemas.benchmark import BenchmarkSummary
from app.schemas.hardware import HardwareSnapshotResponse
from app.schemas.document import DocumentResponse
class DeviceBase(BaseModel):
@@ -53,6 +54,7 @@ class DeviceDetail(DeviceBase):
updated_at: str
last_benchmark: Optional[BenchmarkSummary] = None
last_hardware_snapshot: Optional[HardwareSnapshotResponse] = None
documents: List[DocumentResponse] = []
class Config:
from_attributes = True

View File

@@ -89,6 +89,7 @@ class NetworkInterface(BaseModel):
ip: Optional[str] = None
speed_mbps: Optional[int] = None
driver: Optional[str] = None
wake_on_lan: Optional[bool] = None
class NetworkInfo(BaseModel):
@@ -100,6 +101,7 @@ class MotherboardInfo(BaseModel):
"""Motherboard information schema"""
vendor: Optional[str] = None
model: Optional[str] = None
bios_vendor: Optional[str] = None
bios_version: Optional[str] = None
bios_date: Optional[str] = None

View File

@@ -0,0 +1,9 @@
-- Migration: Add bios_vendor column to hardware_snapshots
-- Date: 2025-12-14
-- Description: Add missing bios_vendor field to store BIOS manufacturer information
-- Add bios_vendor column (nullable)
ALTER TABLE hardware_snapshots ADD COLUMN bios_vendor VARCHAR(100);
-- Note: No need to populate existing rows since the field was not collected before
-- Future benchmarks will populate this field automatically