128 lines
6.8 KiB
Python
128 lines
6.8 KiB
Python
|
|
"""Read-only Gateway inventory lookup for reviewed asset creation."""
|
||
|
|
import asyncio
|
||
|
|
import json
|
||
|
|
from typing import Literal
|
||
|
|
from urllib.parse import quote
|
||
|
|
|
||
|
|
import aiohttp
|
||
|
|
from fastapi import APIRouter, HTTPException, Query
|
||
|
|
from pydantic import BaseModel, Field
|
||
|
|
from psycopg2.extras import Json
|
||
|
|
from app.core.database import execute_query
|
||
|
|
from app.core.config import settings
|
||
|
|
from app.products.backend.router import _apigw_base_url, _apigw_headers
|
||
|
|
|
||
|
|
router = APIRouter(prefix='/hardware-intake/inventory')
|
||
|
|
|
||
|
|
|
||
|
|
def inventory_item(record):
|
||
|
|
raw = record.get('json_data', record.get('hardware_data')) or {}
|
||
|
|
if isinstance(raw, str):
|
||
|
|
raw = json.loads(raw)
|
||
|
|
hardware = raw.get('hardware') or {}
|
||
|
|
return dict(id=record['id'], name=record.get('computer_name') or '',
|
||
|
|
brand=record.get('manufacturer') or hardware.get('manufacturer') or '',
|
||
|
|
model=record.get('model') or hardware.get('model') or '',
|
||
|
|
serial_number=record.get('serial_number') or '',
|
||
|
|
os=record.get('os_caption') or (raw.get('os') or {}).get('caption') or '',
|
||
|
|
cpu=record.get('cpu_name') or (hardware.get('cpu') or {}).get('name') or '',
|
||
|
|
ram_gb=record.get('ram_gb', hardware.get('ram_gb')),
|
||
|
|
last_seen=record.get('last_seen') or record.get('collected_at'),
|
||
|
|
hardware_specs={'gateway_inventory': {'id':record['id'], 'collected_at':record.get('last_seen') or record.get('collected_at'), 'data':raw, 'name':record.get('computer_name'), 'os':record.get('os_caption') or (raw.get('os') or {}).get('caption'), 'cpu':record.get('cpu_name') or (hardware.get('cpu') or {}).get('name'), 'ram_gb':record.get('ram_gb', hardware.get('ram_gb'))}})
|
||
|
|
|
||
|
|
|
||
|
|
@router.get('')
|
||
|
|
async def search_inventory(q: str = Query('', max_length=160), by: Literal['name', 'serial'] = 'name', page: int = Query(1, ge=1)):
|
||
|
|
path = '/api/inventory'
|
||
|
|
params = {'per_page': 20, 'page': page}
|
||
|
|
if by == 'serial':
|
||
|
|
if not q.strip():
|
||
|
|
raise HTTPException(422, 'Indtast et serienummer')
|
||
|
|
path += '/serial/' + quote(q.strip(), safe='')
|
||
|
|
params = {}
|
||
|
|
elif q.strip():
|
||
|
|
params['computer_name'] = q.strip()
|
||
|
|
headers = _apigw_headers()
|
||
|
|
try:
|
||
|
|
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=settings.APIGW_TIMEOUT_SECONDS)) as session:
|
||
|
|
async with session.get(_apigw_base_url() + path, headers=headers, params=params, allow_redirects=False) as response:
|
||
|
|
if response.status == 404 and by == 'serial':
|
||
|
|
record = await find_inventory_in_list(session, headers, serial=q.strip())
|
||
|
|
return {'items': [inventory_item(record)] if record else [], 'pagination': {}}
|
||
|
|
if response.status != 200:
|
||
|
|
raise HTTPException(502, 'API Gateway kunne ikke levere inventory. Kontrollér adgang og tilgængelighed.')
|
||
|
|
payload = await response.json()
|
||
|
|
records = [payload] if by == 'serial' else payload['data']
|
||
|
|
return {'items': [inventory_item(record) for record in records], 'pagination': payload.get('pagination', {})}
|
||
|
|
except (aiohttp.ClientError, asyncio.TimeoutError, ValueError, KeyError, TypeError, AttributeError):
|
||
|
|
raise HTTPException(502, 'Inventory kunne ikke læses fra API Gateway') from None
|
||
|
|
|
||
|
|
|
||
|
|
class InventoryPairInput(BaseModel):
|
||
|
|
inventory_id: int = Field(gt=0)
|
||
|
|
|
||
|
|
|
||
|
|
async def find_inventory_in_list(session, headers, *, inventory_id=None, serial=None):
|
||
|
|
"""Some deployed Gateways expose the list but lack working detail routes."""
|
||
|
|
page = 1
|
||
|
|
while page <= 1000:
|
||
|
|
async with session.get(f'{_apigw_base_url()}/api/inventory', headers=headers,
|
||
|
|
params={'per_page': 100, 'page': page}, allow_redirects=False) as response:
|
||
|
|
if response.status != 200:
|
||
|
|
raise HTTPException(502, 'Inventory-listen kunne ikke hentes fra API Gateway')
|
||
|
|
payload = await response.json()
|
||
|
|
for record in payload['data']:
|
||
|
|
if inventory_id is not None and str(record['id']) == str(inventory_id):
|
||
|
|
return record
|
||
|
|
if serial is not None and str(record.get('serial_number') or '').strip().casefold() == serial.casefold():
|
||
|
|
return record
|
||
|
|
total_pages = int(payload['pagination']['total_pages'])
|
||
|
|
if page >= total_pages:
|
||
|
|
return None
|
||
|
|
page += 1
|
||
|
|
raise HTTPException(502, 'Inventory-listen er for stor til at gennemføre opslaget')
|
||
|
|
|
||
|
|
|
||
|
|
async def fetch_inventory_record(inventory_id: int):
|
||
|
|
headers = _apigw_headers()
|
||
|
|
try:
|
||
|
|
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=settings.APIGW_TIMEOUT_SECONDS)) as session:
|
||
|
|
async with session.get(f'{_apigw_base_url()}/api/inventory/{inventory_id}', headers=headers, allow_redirects=False) as response:
|
||
|
|
if response.status == 404:
|
||
|
|
record = await find_inventory_in_list(session, headers, inventory_id=inventory_id)
|
||
|
|
if record is None:
|
||
|
|
raise HTTPException(404, 'Inventory-maskinen findes ikke længere. Søg igen for at opdatere listen.')
|
||
|
|
return inventory_item(record)
|
||
|
|
if response.status != 200:
|
||
|
|
raise HTTPException(502, 'Inventory-maskinen kunne ikke hentes fra API Gateway')
|
||
|
|
return inventory_item(await response.json())
|
||
|
|
except (aiohttp.ClientError, asyncio.TimeoutError, ValueError, KeyError, TypeError, AttributeError):
|
||
|
|
raise HTTPException(502, 'Inventory kunne ikke læses fra API Gateway') from None
|
||
|
|
|
||
|
|
|
||
|
|
@router.post('/{hardware_id}/pair')
|
||
|
|
async def pair_inventory(hardware_id: int, payload: InventoryPairInput):
|
||
|
|
if not execute_query('SELECT id FROM hardware_assets WHERE id=%s AND deleted_at IS NULL', (hardware_id,)):
|
||
|
|
raise HTTPException(404, 'Hardware findes ikke')
|
||
|
|
item = await fetch_inventory_record(payload.inventory_id)
|
||
|
|
# Merge in PostgreSQL so concurrent updates and acquisition/ESET/UISP data survive.
|
||
|
|
rows = execute_query("""UPDATE hardware_assets
|
||
|
|
SET hardware_specs = COALESCE(hardware_specs, '{}'::jsonb) || %s::jsonb,
|
||
|
|
updated_at = NOW()
|
||
|
|
WHERE id=%s AND deleted_at IS NULL RETURNING id""",
|
||
|
|
(Json(item['hardware_specs']), hardware_id))
|
||
|
|
if not rows:
|
||
|
|
raise HTTPException(404, 'Hardware findes ikke')
|
||
|
|
return {'hardware_id': hardware_id, 'inventory': item}
|
||
|
|
|
||
|
|
|
||
|
|
@router.delete('/{hardware_id}/pair')
|
||
|
|
async def unpair_inventory(hardware_id: int):
|
||
|
|
rows = execute_query("""UPDATE hardware_assets
|
||
|
|
SET hardware_specs = COALESCE(hardware_specs, '{}'::jsonb) - 'gateway_inventory',
|
||
|
|
updated_at = NOW()
|
||
|
|
WHERE id=%s AND deleted_at IS NULL RETURNING id""", (hardware_id,))
|
||
|
|
if not rows:
|
||
|
|
raise HTTPException(404, 'Hardware findes ikke')
|
||
|
|
return {'hardware_id': hardware_id, 'unpaired': True}
|