Migrate PC form + collector off legacy Machine onto the asset/computer model
- PCForm saves/loads via computersApi (asset core + computer extension + primary IP in one call); PC Type now uses the dedicated computertypes table instead of MachineType, fixing the cross-table id mismatch. - Collector (/api/collector/*) writes the Computer model: lookup by hostname or asset number, update loggedinuser/lastreporteddate/lastboottime + asset serial, installed apps via ComputerInstalledApp. - Add computers.vendorid + modelnumberid (PCs carry make/model) and computerinstalledapps.installedversion; computer GET now includes communications. Wire COLLECTOR_API_KEY into config. Retires the last write paths to the Machine model for PCs (ADR-001). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,10 @@ from flask import Blueprint, request
|
||||
from flask_jwt_extended import jwt_required
|
||||
|
||||
from shopdb.extensions import db
|
||||
from shopdb.core.models import Asset, AssetType, OperatingSystem, Application, AppVersion, AuditLog
|
||||
from shopdb.core.models import (
|
||||
Asset, AssetType, OperatingSystem, Application, AppVersion, AuditLog,
|
||||
Communication, CommunicationType,
|
||||
)
|
||||
from shopdb.utils.responses import (
|
||||
success_response,
|
||||
error_response,
|
||||
@@ -225,6 +228,10 @@ def get_computer(computer_id: int):
|
||||
|
||||
result = comp.asset.to_dict() if comp.asset else {}
|
||||
result['computer'] = comp.to_dict()
|
||||
result['communications'] = [
|
||||
c.to_dict() for c in
|
||||
Communication.query.filter_by(assetid=comp.assetid).all()
|
||||
]
|
||||
|
||||
return success_response(result)
|
||||
|
||||
@@ -339,6 +346,8 @@ def create_computer():
|
||||
computertypeid=data.get('computertypeid'),
|
||||
hostname=data.get('hostname'),
|
||||
osid=data.get('osid'),
|
||||
vendorid=data.get('vendorid'),
|
||||
modelnumberid=data.get('modelnumberid'),
|
||||
loggedinuser=data.get('loggedinuser'),
|
||||
lastreporteddate=data.get('lastreporteddate'),
|
||||
lastboottime=data.get('lastboottime'),
|
||||
@@ -350,6 +359,17 @@ def create_computer():
|
||||
db.session.add(comp)
|
||||
db.session.flush()
|
||||
|
||||
# Optional primary IP communication
|
||||
if data.get('ipaddress'):
|
||||
ip_comtype = CommunicationType.query.filter_by(comtype='IP').first()
|
||||
if ip_comtype:
|
||||
db.session.add(Communication(
|
||||
assetid=asset.assetid,
|
||||
comtypeid=ip_comtype.comtypeid,
|
||||
ipaddress=data['ipaddress'],
|
||||
isprimary=True,
|
||||
))
|
||||
|
||||
# Audit log
|
||||
AuditLog.log('created', 'Computer', entityid=comp.computerid,
|
||||
entityname=data.get('hostname') or data['assetnumber'])
|
||||
@@ -415,8 +435,9 @@ def update_computer(computer_id: int):
|
||||
setattr(asset, key, data[key])
|
||||
|
||||
# Update computer fields
|
||||
computer_fields = ['computertypeid', 'hostname', 'osid', 'loggedinuser',
|
||||
'lastreporteddate', 'lastboottime', 'isvnc', 'iswinrm', 'isshopfloor']
|
||||
computer_fields = ['computertypeid', 'hostname', 'osid', 'vendorid',
|
||||
'modelnumberid', 'loggedinuser', 'lastreporteddate',
|
||||
'lastboottime', 'isvnc', 'iswinrm', 'isshopfloor']
|
||||
for key in computer_fields:
|
||||
if key in data:
|
||||
old_val = getattr(comp, key)
|
||||
@@ -425,6 +446,23 @@ def update_computer(computer_id: int):
|
||||
changes[key] = {'old': old_val, 'new': new_val}
|
||||
setattr(comp, key, data[key])
|
||||
|
||||
# Upsert the primary IP communication so a single PUT covers it
|
||||
if 'ipaddress' in data:
|
||||
ip = (data.get('ipaddress') or '').strip()
|
||||
primary = Communication.query.filter_by(
|
||||
assetid=asset.assetid, isprimary=True).first()
|
||||
if ip:
|
||||
if primary:
|
||||
primary.ipaddress = ip
|
||||
else:
|
||||
ip_comtype = CommunicationType.query.filter_by(comtype='IP').first()
|
||||
if ip_comtype:
|
||||
db.session.add(Communication(
|
||||
assetid=asset.assetid, comtypeid=ip_comtype.comtypeid,
|
||||
ipaddress=ip, isprimary=True))
|
||||
elif primary:
|
||||
primary.ipaddress = None
|
||||
|
||||
# Audit log if there were changes
|
||||
if changes:
|
||||
AuditLog.log('updated', 'Computer', entityid=comp.computerid,
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Add PC hardware make/model + installed-app version string
|
||||
|
||||
Adds computers.vendorid + computers.modelnumberid (PCs carry vendor/model like
|
||||
equipment) and computerinstalledapps.installedversion (raw version string from
|
||||
automated collection, when there is no curated AppVersion). Supports moving the
|
||||
PC form and the collector off the legacy Machine model onto the asset/computer
|
||||
model (ADR-001).
|
||||
|
||||
Revision ID: 0002_pc_hardware
|
||||
Revises: 0001_baseline_computers
|
||||
Create Date: 2026-06-26
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '0002_pc_hardware'
|
||||
down_revision = '0001_baseline_computers'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
with op.batch_alter_table('computers') as batch_op:
|
||||
batch_op.add_column(sa.Column('vendorid', sa.Integer(), nullable=True))
|
||||
batch_op.add_column(sa.Column('modelnumberid', sa.Integer(), nullable=True))
|
||||
batch_op.create_foreign_key('fk_computers_vendor', 'vendors',
|
||||
['vendorid'], ['vendorid'])
|
||||
batch_op.create_foreign_key('fk_computers_model', 'models',
|
||||
['modelnumberid'], ['modelnumberid'])
|
||||
with op.batch_alter_table('computerinstalledapps') as batch_op:
|
||||
batch_op.add_column(sa.Column('installedversion', sa.String(length=100),
|
||||
nullable=True))
|
||||
|
||||
|
||||
def downgrade():
|
||||
with op.batch_alter_table('computerinstalledapps') as batch_op:
|
||||
batch_op.drop_column('installedversion')
|
||||
with op.batch_alter_table('computers') as batch_op:
|
||||
batch_op.drop_constraint('fk_computers_model', type_='foreignkey')
|
||||
batch_op.drop_constraint('fk_computers_vendor', type_='foreignkey')
|
||||
batch_op.drop_column('modelnumberid')
|
||||
batch_op.drop_column('vendorid')
|
||||
@@ -62,6 +62,18 @@ class Computer(BaseModel):
|
||||
nullable=True
|
||||
)
|
||||
|
||||
# Hardware make/model (PCs carry vendor + model like equipment)
|
||||
vendorid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey('vendors.vendorid'),
|
||||
nullable=True
|
||||
)
|
||||
modelnumberid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey('models.modelnumberid'),
|
||||
nullable=True
|
||||
)
|
||||
|
||||
# Status tracking
|
||||
loggedinuser = db.Column(db.String(100), nullable=True)
|
||||
lastreporteddate = db.Column(db.DateTime, nullable=True)
|
||||
@@ -93,6 +105,8 @@ class Computer(BaseModel):
|
||||
)
|
||||
computertype = db.relationship('ComputerType', backref='computers')
|
||||
operatingsystem = db.relationship('OperatingSystem', backref='computers')
|
||||
vendor = db.relationship('Vendor')
|
||||
model = db.relationship('Model')
|
||||
|
||||
# Installed applications (one-to-many)
|
||||
installedapps = db.relationship(
|
||||
@@ -120,6 +134,10 @@ class Computer(BaseModel):
|
||||
result['computertypename'] = self.computertype.computertype
|
||||
if self.operatingsystem:
|
||||
result['osname'] = self.operatingsystem.osname
|
||||
if self.vendor:
|
||||
result['vendorname'] = self.vendor.vendor
|
||||
if self.model:
|
||||
result['modelname'] = self.model.modelnumber
|
||||
|
||||
return result
|
||||
|
||||
@@ -149,6 +167,8 @@ class ComputerInstalledApp(db.Model):
|
||||
db.ForeignKey('appversions.appversionid'),
|
||||
nullable=True
|
||||
)
|
||||
# Raw version string from automated collection (when no curated AppVersion)
|
||||
installedversion = db.Column(db.String(100), nullable=True)
|
||||
isactive = db.Column(db.Boolean, default=True, nullable=False)
|
||||
installeddate = db.Column(db.DateTime, default=db.func.now())
|
||||
|
||||
|
||||
Reference in New Issue
Block a user