Collector: ingest GE-Enforce/enrollment data with configurable pc-type mapping
Extends the computers collector so it can replace the classic api.asp updateCompleteAsset path that the shopfloor PC fleet uses to auto-update data. Collector schema (project naming) now accepts the GE-Enforce/enrollment shape: machinenumber, pctype, pcsubtype, serialnumber, loggedinuser, lastboottime, lastcheckin, ipaddress, vendorname, modelnumber, osname, installedsoftware. - machinenumber -> Asset.assetnumber (skips the 9999 imaging placeholder, falls back to hostname), on create and update. - pctype -> ComputerType via a configurable mapping (see below). - vendor/model created if missing (free vocab); OS looked up (controlled, warns if unknown); pcsubtype accepted but not yet stored (warning). - Dropped per scope: VNC/WinRM flags, warranty, DNC config, multi-NIC. Configurable pc-type mapping (the gea-shopfloor-* imaging taxonomy -> ComputerType): defaults + resolution live in plugins/computers/pctypemap.py (plugin domain, contract-pure - reads Setting via shopdb.api); overrides stored as pctypemap_<pxetype> settings, seeded on plugin install, edited in Settings > System > "Collector PC Type Mapping" (new UI section). Migration doc: docs/COLLECTOR-INTEGRATION.md maps classic api.asp fields + GE-Enforce status fields to the collector schema, documents machine-number sourcing (registry MachineNo first, then C:\Enrollment\machine-number.txt) and that the transport is interim. Tests: complete-asset payload maps machinenumber/pctype/vendor/model/os; 9999 placeholder falls back to hostname. 186 tests pass, naming green, app boots, mapping UI verified. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -67,15 +67,27 @@ class ComputersPlugin(BasePlugin):
|
||||
# -- ADR-006 collector contract -----------------------------------------
|
||||
|
||||
def get_collector_schema(self) -> Optional[Dict]:
|
||||
"""Schema for the PC collector payload (matched by hostname)."""
|
||||
"""Schema for the PC collector payload (matched by hostname).
|
||||
|
||||
Aligns with the GE-Enforce status shape (transport may change), using
|
||||
the project naming convention (lowercase concatenated). The caller maps
|
||||
its own field names to these.
|
||||
"""
|
||||
return {
|
||||
'identityfield': 'hostname',
|
||||
'fields': {
|
||||
'hostname': {'type': 'string', 'required': True},
|
||||
'machinenumber': {'type': 'string'},
|
||||
'pctype': {'type': 'string'},
|
||||
'pcsubtype': {'type': 'string'},
|
||||
'serialnumber': {'type': 'string'},
|
||||
'currentuser': {'type': 'string'},
|
||||
'loggedinuser': {'type': 'string'},
|
||||
'lastboottime': {'type': 'string', 'format': 'date-time'},
|
||||
'lastcheckin': {'type': 'string', 'format': 'date-time'},
|
||||
'ipaddress': {'type': 'string'},
|
||||
'vendorname': {'type': 'string'},
|
||||
'modelnumber': {'type': 'string'},
|
||||
'osname': {'type': 'string'},
|
||||
'installedsoftware': {
|
||||
'type': 'array',
|
||||
'items': {'name': 'string', 'version': 'string'},
|
||||
@@ -86,13 +98,23 @@ class ComputersPlugin(BasePlugin):
|
||||
def apply_collector_payload(self, payload: Dict) -> Dict:
|
||||
"""Idempotent upsert of a PC from a collector payload (by hostname)."""
|
||||
from datetime import datetime
|
||||
from shopdb.api import Asset, Application, Communication, CommunicationType
|
||||
from shopdb.api import (
|
||||
Asset, Application, Communication, CommunicationType,
|
||||
Vendor, Model, OperatingSystem,
|
||||
)
|
||||
from .pctypemap import pctype_mapping
|
||||
|
||||
warnings = []
|
||||
hostname = (payload.get('hostname') or '').strip()
|
||||
if not hostname:
|
||||
raise ValueError('hostname is required')
|
||||
|
||||
# Machine number is the business identifier (Asset.assetnumber). Skip
|
||||
# the imaging-time placeholder '9999' and fall back to hostname.
|
||||
machinenumber = (payload.get('machinenumber') or '').strip()
|
||||
if machinenumber in ('', '9999'):
|
||||
machinenumber = None
|
||||
|
||||
comp = Computer.query.filter(Computer.hostname.ilike(hostname)).first()
|
||||
if not comp:
|
||||
comp = (Computer.query.join(Asset, Asset.assetid == Computer.assetid)
|
||||
@@ -103,14 +125,16 @@ class ComputersPlugin(BasePlugin):
|
||||
atype = AssetType.query.filter_by(assettype='computer').first()
|
||||
# statusid=1 is the first seeded asset status ("In Use"); a
|
||||
# collector-discovered PC is by definition in use.
|
||||
asset = Asset(assetnumber=hostname, assettypeid=atype.assettypeid,
|
||||
statusid=1)
|
||||
asset = Asset(assetnumber=machinenumber or hostname,
|
||||
assettypeid=atype.assettypeid, statusid=1)
|
||||
db.session.add(asset)
|
||||
db.session.flush()
|
||||
comp = Computer(assetid=asset.assetid, hostname=hostname)
|
||||
db.session.add(comp)
|
||||
db.session.flush()
|
||||
action = 'created'
|
||||
elif machinenumber and comp.asset:
|
||||
comp.asset.assetnumber = machinenumber
|
||||
|
||||
comp.lastreporteddate = datetime.utcnow()
|
||||
if payload.get('lastboottime'):
|
||||
@@ -119,11 +143,63 @@ class ComputersPlugin(BasePlugin):
|
||||
payload['lastboottime'].replace('Z', '+00:00'))
|
||||
except (ValueError, AttributeError):
|
||||
warnings.append('lastboottime not parseable')
|
||||
if payload.get('currentuser'):
|
||||
comp.loggedinuser = payload['currentuser']
|
||||
loggedinuser = payload.get('loggedinuser') or payload.get('currentuser')
|
||||
if loggedinuser:
|
||||
comp.loggedinuser = loggedinuser
|
||||
if payload.get('serialnumber') and comp.asset:
|
||||
comp.asset.serialnumber = payload['serialnumber']
|
||||
|
||||
# pc-type -> ComputerType via the configurable settings mapping.
|
||||
pctype = (payload.get('pctype') or '').strip()
|
||||
if pctype:
|
||||
from .models import ComputerType
|
||||
mapped = pctype_mapping().get(pctype)
|
||||
if not mapped:
|
||||
warnings.append(f'no ComputerType mapping for pctype: {pctype}')
|
||||
else:
|
||||
ctype = ComputerType.query.filter_by(computertype=mapped).first()
|
||||
if ctype:
|
||||
comp.computertypeid = ctype.computertypeid
|
||||
else:
|
||||
warnings.append(f'mapped ComputerType not found: {mapped}')
|
||||
|
||||
# Vendor / model are free vocab - create if missing.
|
||||
vendorname = (payload.get('vendorname') or '').strip()
|
||||
vendor = None
|
||||
if vendorname:
|
||||
vendor = Vendor.query.filter(Vendor.vendor.ilike(vendorname)).first()
|
||||
if not vendor:
|
||||
vendor = Vendor(vendor=vendorname)
|
||||
db.session.add(vendor)
|
||||
db.session.flush()
|
||||
comp.vendorid = vendor.vendorid
|
||||
|
||||
modelnumber = (payload.get('modelnumber') or '').strip()
|
||||
if modelnumber:
|
||||
model_query = Model.query.filter(Model.modelnumber.ilike(modelnumber))
|
||||
if vendor:
|
||||
model_query = model_query.filter(Model.vendorid == vendor.vendorid)
|
||||
model = model_query.first()
|
||||
if not model:
|
||||
model = Model(modelnumber=modelnumber,
|
||||
vendorid=vendor.vendorid if vendor else None)
|
||||
db.session.add(model)
|
||||
db.session.flush()
|
||||
comp.modelnumberid = model.modelnumberid
|
||||
|
||||
# OS is a controlled vocab - look up only, warn if unknown.
|
||||
osname = (payload.get('osname') or '').strip()
|
||||
if osname:
|
||||
os_row = OperatingSystem.query.filter(
|
||||
OperatingSystem.osname.ilike(osname)).first()
|
||||
if os_row:
|
||||
comp.osid = os_row.osid
|
||||
else:
|
||||
warnings.append(f'unknown operating system: {osname}')
|
||||
|
||||
if payload.get('pcsubtype'):
|
||||
warnings.append('pcsubtype received but not stored (no model field)')
|
||||
|
||||
if payload.get('ipaddress'):
|
||||
ip_comtype = CommunicationType.query.filter_by(comtype='IP').first()
|
||||
primary = Communication.query.filter_by(
|
||||
@@ -167,6 +243,9 @@ class ComputersPlugin(BasePlugin):
|
||||
with app.app_context():
|
||||
self._ensure_asset_type()
|
||||
self._ensure_computer_types()
|
||||
from .pctypemap import seed_pctype_settings
|
||||
seed_pctype_settings()
|
||||
db.session.commit()
|
||||
logger.info("Computers plugin installed")
|
||||
|
||||
def _ensure_asset_type(self) -> None:
|
||||
|
||||
Reference in New Issue
Block a user