Browse Source

feat(collector): tambah endpoint PUT /assets/{id}/owner dan GET /assets/by-owner/{ldap_username}

master
Budi Setiawan 3 weeks ago
parent
commit
5aa12a5f7b
  1. 26
      GeoNetAgent/collector/app/api/v1/assets.py
  2. 98
      GeoNetAgent/collector/app/schemas/assets.py
  3. 239
      GeoNetAgent/collector/app/services/asset_read_service.py

26
GeoNetAgent/collector/app/api/v1/assets.py

@ -6,7 +6,7 @@ from sqlalchemy.orm import Session @@ -6,7 +6,7 @@ from sqlalchemy.orm import Session
from app.core.auth import verify_api_bearer
from app.db.session import get_db
from app.schemas.agent_report import ErrorBody, ErrorResponse
from app.schemas.assets import AssetDetail, PaginatedAssets, PaginatedSnapshots, PaginatedSoftware, SnapshotSummary, StatsSummary
from app.schemas.assets import AssetDetail, AssetOwnerIn, AssetOwnerOut, AssetsByOwnerItem, PaginatedAssets, PaginatedSnapshots, PaginatedSoftware, SnapshotSummary, StatsSummary
from app.services import asset_read_service
router = APIRouter()
@ -51,6 +51,15 @@ def list_assets( @@ -51,6 +51,15 @@ def list_assets(
)
@router.get("/assets/by-owner/{ldap_username}", response_model=list[AssetsByOwnerItem])
def get_assets_by_owner(
ldap_username: str,
db: Session = Depends(get_db),
_: str = Depends(verify_api_bearer),
) -> list[AssetsByOwnerItem]:
return asset_read_service.get_assets_by_ldap_username(db, ldap_username)
@router.get("/assets/{asset_id}", response_model=AssetDetail)
def get_asset(
asset_id: uuid.UUID,
@ -109,3 +118,18 @@ def list_asset_software( @@ -109,3 +118,18 @@ def list_asset_software(
if result is None:
raise _not_found()
return result
@router.put("/assets/{asset_id}/owner", response_model=AssetOwnerOut)
def set_asset_owner(
asset_id: uuid.UUID,
body: AssetOwnerIn,
db: Session = Depends(get_db),
_: str = Depends(verify_api_bearer),
) -> AssetOwnerOut:
result = asset_read_service.set_asset_owner(db, asset_id, body.ldap_username)
if result is None:
raise _not_found()
return result

98
GeoNetAgent/collector/app/schemas/assets.py

@ -7,6 +7,8 @@ from pydantic import BaseModel, Field @@ -7,6 +7,8 @@ from pydantic import BaseModel, Field
class AssetListItem(BaseModel):
id: str
hostname: str
display_name: str | None = None
form_factor: str | None = None
asset_type: str
status: str
manufacturer: str | None = None
@ -37,9 +39,76 @@ class DiskOut(BaseModel): @@ -37,9 +39,76 @@ class DiskOut(BaseModel):
model: str | None = None
class CpuOut(BaseModel):
model: str | None = None
manufacturer: str | None = None
cores: int | None = None
logical_processors: int | None = None
max_clock_mhz: int | None = None
current_clock_mhz: int | None = None
socket: str | None = None
socket_count: int | None = None
l2_cache_kb: int | None = None
l3_cache_kb: int | None = None
architecture: str | None = None
class GpuOut(BaseModel):
name: str
dedicated_memory_mb: int | None = None
adapter_compatibility: str | None = None
video_processor: str | None = None
driver_version: str | None = None
driver_date: str | None = None
pnp_device_id: str | None = None
status: str | None = None
current_resolution: str | None = None
video_mode_description: str | None = None
class PhysicalDiskOut(BaseModel):
disk_number: int | None = None
device_id: str | None = None
model: str | None = None
friendly_name: str | None = None
media_type: str | None = None
operational_status: str | None = None
health_status: str | None = None
total_gb: float | None = None
partition_style: str | None = None
bus_type: str | None = None
serial_number: str | None = None
class MemoryModuleOut(BaseModel):
slot: str | None = None
bank_label: str | None = None
capacity_gb: float | None = None
speed_mhz: int | None = None
configured_speed_mhz: int | None = None
manufacturer: str | None = None
part_number: str | None = None
serial_number: str | None = None
form_factor: str | None = None
memory_type_label: str | None = None
rank: int | None = None
class MemorySummaryOut(BaseModel):
total_gb: float | None = None
module_count: int | None = None
slot_count: int | None = None
empty_slots: int | None = None
layout: str | None = None
configuration: str | None = None
upgradeable: bool | None = None
ddr_generation: str | None = None
class SystemSnapshotOut(BaseModel):
form_factor: str | None = None
chassis_label: str | None = None
pc_system_label: str | None = None
class NetworkAdapterOut(BaseModel):
@ -76,6 +145,7 @@ class SnapshotSummary(BaseModel): @@ -76,6 +145,7 @@ class SnapshotSummary(BaseModel):
os_name: str | None = None
os_version: str | None = None
cpu_model: str | None = None
cpu: CpuOut | None = None
ram_gb: float | None = None
gpu_model: str | None = None
bios_version: str | None = None
@ -85,7 +155,11 @@ class SnapshotSummary(BaseModel): @@ -85,7 +155,11 @@ class SnapshotSummary(BaseModel):
collected_at: datetime
created_at: datetime
gpus: list[GpuOut] = Field(default_factory=list)
memory_modules: list[MemoryModuleOut] = Field(default_factory=list)
memory_summary: MemorySummaryOut | None = None
disks: list[DiskOut] = Field(default_factory=list)
physical_disks: list[PhysicalDiskOut] = Field(default_factory=list)
system: SystemSnapshotOut | None = None
network: list[NetworkAdapterOut] = Field(default_factory=list)
peripherals: list[PeripheralOut] = Field(default_factory=list)
@ -153,3 +227,27 @@ class StatsSummary(BaseModel): @@ -153,3 +227,27 @@ class StatsSummary(BaseModel):
stale_assets: int
total_snapshots: int
total_software_rows: int
class AssetOwnerIn(BaseModel):
ldap_username: str = Field(..., min_length=1, max_length=100)
class AssetOwnerOut(BaseModel):
asset_id: str
ldap_username: str
assigned_at: datetime
assigned_by: str
class AssetsByOwnerItem(BaseModel):
id: str
hostname: str
display_name: str | None = None
asset_type: str
status: str
manufacturer: str | None = None
model: str | None = None
serial_number: str | None = None
last_seen_at: datetime | None = None
latest_snapshot_at: datetime | None = None

239
GeoNetAgent/collector/app/services/asset_read_service.py

@ -5,21 +5,28 @@ from datetime import datetime, timedelta, timezone @@ -5,21 +5,28 @@ from datetime import datetime, timedelta, timezone
from sqlalchemy import Select, exists, func, or_, select
from sqlalchemy.orm import Session
from app.db.models import Asset, AssetDisk, AssetNetworkAdapter, AssetSnapshot, SoftwareInstallation
from app.db.models import Asset, AssetDisk, AssetNetworkAdapter, AssetSnapshot, AssetUserAssignment, SoftwareInstallation, User
from app.schemas.assets import (
AssetDetail,
AssetListItem,
AssetOwnerOut,
AssetsByOwnerItem,
CpuOut,
DiskOut,
GpuOut,
MemoryModuleOut,
MemorySummaryOut,
NetworkAdapterOut,
PaginatedAssets,
PaginatedSnapshots,
PaginatedSoftware,
PeripheralOut,
PhysicalDiskOut,
SnapshotListItem,
SnapshotSummary,
SoftwareOut,
StatsSummary,
SystemSnapshotOut,
)
@ -151,6 +158,9 @@ def list_assets( @@ -151,6 +158,9 @@ def list_assets(
AssetListItem(
id=str(asset.id),
hostname=asset.hostname,
display_name=asset.display_name,
form_factor=_form_factor_from_payload(snap.payload if snap else None)
or (asset.metadata_ or {}).get("form_factor"),
asset_type=asset.asset_type,
status=asset.status,
manufacturer=asset.manufacturer,
@ -240,6 +250,85 @@ def _peripherals_from_payload(payload: dict | None) -> list[PeripheralOut]: @@ -240,6 +250,85 @@ def _peripherals_from_payload(payload: dict | None) -> list[PeripheralOut]:
return items
def _form_factor_from_payload(payload: dict | None) -> str | None:
if not payload:
return None
system = payload.get("system")
if isinstance(system, dict):
value = system.get("form_factor")
if value:
return str(value)
return None
def _system_from_payload(payload: dict | None) -> SystemSnapshotOut | None:
if not payload:
return None
system = payload.get("system")
if not isinstance(system, dict):
return None
return SystemSnapshotOut(
form_factor=system.get("form_factor"),
chassis_label=system.get("chassis_label"),
pc_system_label=system.get("pc_system_label"),
)
def _physical_disks_from_payload(payload: dict | None) -> list[PhysicalDiskOut]:
if not payload:
return []
raw = payload.get("physical_disks")
if not isinstance(raw, list):
return []
items: list[PhysicalDiskOut] = []
for entry in raw:
if not isinstance(entry, dict):
continue
items.append(
PhysicalDiskOut(
disk_number=entry.get("disk_number"),
device_id=entry.get("device_id"),
model=entry.get("model"),
friendly_name=entry.get("friendly_name"),
media_type=entry.get("media_type"),
operational_status=entry.get("operational_status"),
health_status=entry.get("health_status"),
total_gb=entry.get("total_gb"),
partition_style=entry.get("partition_style"),
bus_type=entry.get("bus_type"),
serial_number=entry.get("serial_number"),
)
)
return items
def _cpu_from_payload(payload: dict | None) -> CpuOut | None:
if not payload:
return None
raw = payload.get("cpu")
if isinstance(raw, dict):
return CpuOut(
model=raw.get("model"),
manufacturer=raw.get("manufacturer"),
cores=raw.get("cores"),
logical_processors=raw.get("logical_processors"),
max_clock_mhz=raw.get("max_clock_mhz"),
current_clock_mhz=raw.get("current_clock_mhz"),
socket=raw.get("socket"),
socket_count=raw.get("socket_count"),
l2_cache_kb=raw.get("l2_cache_kb"),
l3_cache_kb=raw.get("l3_cache_kb"),
architecture=raw.get("architecture"),
)
system = payload.get("system")
if isinstance(system, dict) and system.get("cpu_model"):
return CpuOut(model=str(system["cpu_model"]))
return None
def _gpus_from_payload(payload: dict | None) -> list[GpuOut]:
if not payload:
return []
@ -263,21 +352,77 @@ def _gpus_from_payload(payload: dict | None) -> list[GpuOut]: @@ -263,21 +352,77 @@ def _gpus_from_payload(payload: dict | None) -> list[GpuOut]:
GpuOut(
name=str(name),
dedicated_memory_mb=int(dedicated) if dedicated is not None else None,
adapter_compatibility=entry.get("adapter_compatibility"),
video_processor=entry.get("video_processor"),
driver_version=entry.get("driver_version"),
driver_date=str(entry.get("driver_date")) if entry.get("driver_date") else None,
pnp_device_id=entry.get("pnp_device_id"),
status=entry.get("status"),
current_resolution=entry.get("current_resolution"),
video_mode_description=entry.get("video_mode_description"),
)
)
return items
def _memory_from_payload(payload: dict | None) -> tuple[list[MemoryModuleOut], MemorySummaryOut | None]:
if not payload:
return [], None
modules: list[MemoryModuleOut] = []
raw_modules = payload.get("memory_modules")
if isinstance(raw_modules, list):
for entry in raw_modules:
if not isinstance(entry, dict):
continue
modules.append(
MemoryModuleOut(
slot=entry.get("slot"),
bank_label=entry.get("bank_label"),
capacity_gb=entry.get("capacity_gb"),
speed_mhz=entry.get("speed_mhz"),
configured_speed_mhz=entry.get("configured_speed_mhz"),
manufacturer=entry.get("manufacturer"),
part_number=entry.get("part_number"),
serial_number=entry.get("serial_number"),
form_factor=entry.get("form_factor"),
memory_type_label=entry.get("memory_type_label"),
rank=entry.get("rank"),
)
)
summary = None
raw_summary = payload.get("memory_summary")
if isinstance(raw_summary, dict):
summary = MemorySummaryOut(
total_gb=raw_summary.get("total_gb"),
module_count=raw_summary.get("module_count"),
slot_count=raw_summary.get("slot_count"),
empty_slots=raw_summary.get("empty_slots"),
layout=raw_summary.get("layout"),
configuration=raw_summary.get("configuration"),
upgradeable=raw_summary.get("upgradeable"),
ddr_generation=raw_summary.get("ddr_generation"),
)
return modules, summary
def _snapshot_to_summary(db: Session, snapshot: AssetSnapshot) -> SnapshotSummary:
disks, network = _load_snapshot_children(db, snapshot)
peripherals = _peripherals_from_payload(snapshot.payload)
gpus = _gpus_from_payload(snapshot.payload)
cpu = _cpu_from_payload(snapshot.payload)
memory_modules, memory_summary = _memory_from_payload(snapshot.payload)
physical_disks = _physical_disks_from_payload(snapshot.payload)
system = _system_from_payload(snapshot.payload)
return SnapshotSummary(
id=str(snapshot.id),
agent_version=snapshot.agent_version,
os_name=snapshot.os_name,
os_version=snapshot.os_version,
cpu_model=snapshot.cpu_model,
cpu_model=snapshot.cpu_model or (cpu.model if cpu else None),
cpu=cpu,
ram_gb=float(snapshot.ram_gb) if snapshot.ram_gb is not None else None,
gpu_model=snapshot.gpu_model,
bios_version=snapshot.bios_version,
@ -287,7 +432,11 @@ def _snapshot_to_summary(db: Session, snapshot: AssetSnapshot) -> SnapshotSummar @@ -287,7 +432,11 @@ def _snapshot_to_summary(db: Session, snapshot: AssetSnapshot) -> SnapshotSummar
collected_at=snapshot.collected_at,
created_at=snapshot.created_at,
gpus=gpus,
memory_modules=memory_modules,
memory_summary=memory_summary,
disks=disks,
physical_disks=physical_disks,
system=system,
network=network,
peripherals=peripherals,
)
@ -417,3 +566,89 @@ def list_software( @@ -417,3 +566,89 @@ def list_software(
snapshot_id=str(resolved_snapshot_id) if resolved_snapshot_id else None,
**meta,
)
def set_asset_owner(db: Session, asset_id: uuid.UUID, ldap_username: str) -> AssetOwnerOut | None:
asset = db.scalar(select(Asset).where(Asset.id == asset_id, Asset.deleted_at.is_(None)))
if not asset:
return None
user = db.scalar(select(User).where(User.username == ldap_username))
if not user:
user = User(username=ldap_username)
db.add(user)
db.flush()
existing = db.scalar(
select(AssetUserAssignment).where(
AssetUserAssignment.asset_id == asset_id,
AssetUserAssignment.is_primary == True, # noqa: E712
AssetUserAssignment.ended_at.is_(None),
)
)
if existing:
existing.ended_at = _utcnow()
db.flush()
assignment = AssetUserAssignment(
asset_id=asset_id,
user_id=user.id,
assigned_by="manual",
is_primary=True,
)
db.add(assignment)
db.commit()
db.refresh(assignment)
return AssetOwnerOut(
asset_id=str(asset_id),
ldap_username=user.username,
assigned_at=assignment.assigned_at,
assigned_by=assignment.assigned_by,
)
def get_assets_by_ldap_username(db: Session, ldap_username: str) -> list[AssetsByOwnerItem]:
user = db.scalar(select(User).where(User.username == ldap_username))
if not user:
return []
rows = db.execute(
select(Asset, AssetUserAssignment)
.join(AssetUserAssignment, AssetUserAssignment.asset_id == Asset.id)
.where(
AssetUserAssignment.user_id == user.id,
AssetUserAssignment.is_primary == True, # noqa: E712
AssetUserAssignment.ended_at.is_(None),
Asset.deleted_at.is_(None),
)
.order_by(Asset.last_seen_at.desc().nullslast())
).all()
latest = _latest_snapshot_subquery()
items: list[AssetsByOwnerItem] = []
for asset, _ in rows:
snap = db.scalar(
select(AssetSnapshot)
.join(
latest,
(AssetSnapshot.asset_id == latest.c.asset_id)
& (AssetSnapshot.collected_at == latest.c.max_collected_at),
)
.where(AssetSnapshot.asset_id == asset.id)
)
items.append(
AssetsByOwnerItem(
id=str(asset.id),
hostname=asset.hostname,
display_name=asset.display_name,
asset_type=asset.asset_type,
status=asset.status,
manufacturer=asset.manufacturer,
model=asset.model,
serial_number=asset.serial_number,
last_seen_at=asset.last_seen_at,
latest_snapshot_at=snap.collected_at if snap else None,
)
)
return items

Loading…
Cancel
Save