Import-API hardening: network device IP endpoint + communicationtypes seeder
The legacy-import surface (docs/IMPORT-API.md) is the schema-agnostic contract every adopting site targets; these close two gaps found while mapping the WJ classic import. Network device IP: POST/PUT /api/network now accept an `ipaddress` and materialize a primary Communication (mirroring the printer route), and GET (list + detail + create/update result) surface it. Previously a network device's IP - which lives in the communications table, not on the extension - had no HTTP import path at all. communicationtypes seed: `flask seed reference-data` now seeds the eight canonical communication types (IP/Serial/Network_Interface/USB/Parallel/VNC/ FTP/DNC), which IMPORT-API.md already documents as a prerequisite. The IP type must exist before any asset import so printer/network routes can attach an IP. There is no CRUD endpoint for these, so seeding is the only path. Tests: network create/update IP upsert + GET surfacing + seed creates IP type. 204 targeted tests pass; naming + pyflakes green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
from flask import Blueprint, request
|
||||
from flask_jwt_extended import jwt_required
|
||||
|
||||
from shopdb.api import db, Asset, AssetType, Vendor, AuditLog, success_response, error_response, paginated_response, ErrorCodes, get_pagination_params, paginate_query
|
||||
from shopdb.api import db, Asset, AssetType, Vendor, Communication, CommunicationType, AuditLog, success_response, error_response, paginated_response, ErrorCodes, get_pagination_params, paginate_query
|
||||
|
||||
from ..models import NetworkDevice, NetworkDeviceType, Subnet, VLAN
|
||||
|
||||
@@ -12,6 +12,31 @@ from shopdb.api import require_permission, apply_import_timestamps
|
||||
network_bp = Blueprint('network', __name__)
|
||||
|
||||
|
||||
def _primary_ip(assetid):
|
||||
"""The asset's primary IP address string, or None. Mirrors the printer
|
||||
route: a device's IP lives in a Communication row, not on the extension."""
|
||||
comm = (Communication.query.filter_by(assetid=assetid, isprimary=True).first()
|
||||
or Communication.query.filter_by(assetid=assetid).first())
|
||||
return comm.ipaddress if comm else None
|
||||
|
||||
|
||||
def _upsert_primary_ip(asset, ip):
|
||||
"""Create/update/clear the asset's primary IP Communication from an
|
||||
ipaddress string. No-op if the IP CommunicationType is not seeded."""
|
||||
ip = (ip or '').strip()
|
||||
if not ip:
|
||||
return
|
||||
comm = Communication.query.filter_by(assetid=asset.assetid, isprimary=True).first()
|
||||
if comm:
|
||||
comm.ipaddress = ip
|
||||
return
|
||||
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))
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Network Device Types
|
||||
# =============================================================================
|
||||
@@ -233,6 +258,7 @@ def list_network_devices():
|
||||
for netdev in items:
|
||||
item = netdev.asset.to_dict() if netdev.asset else {}
|
||||
item['networkdevice'] = netdev.to_dict()
|
||||
item['ipaddress'] = _primary_ip(netdev.assetid)
|
||||
data.append(item)
|
||||
|
||||
return paginated_response(data, page, per_page, total)
|
||||
@@ -253,6 +279,7 @@ def get_network_device(device_id: int):
|
||||
|
||||
result = netdev.asset.to_dict() if netdev.asset else {}
|
||||
result['networkdevice'] = netdev.to_dict()
|
||||
result['ipaddress'] = _primary_ip(netdev.assetid)
|
||||
|
||||
return success_response(result)
|
||||
|
||||
@@ -272,6 +299,7 @@ def get_network_device_by_asset(asset_id: int):
|
||||
|
||||
result = netdev.asset.to_dict() if netdev.asset else {}
|
||||
result['networkdevice'] = netdev.to_dict()
|
||||
result['ipaddress'] = _primary_ip(netdev.assetid)
|
||||
|
||||
return success_response(result)
|
||||
|
||||
@@ -291,6 +319,7 @@ def get_network_device_by_hostname(hostname: str):
|
||||
|
||||
result = netdev.asset.to_dict() if netdev.asset else {}
|
||||
result['networkdevice'] = netdev.to_dict()
|
||||
result['ipaddress'] = _primary_ip(netdev.assetid)
|
||||
|
||||
return success_response(result)
|
||||
|
||||
@@ -380,6 +409,11 @@ def create_network_device():
|
||||
db.session.add(netdev)
|
||||
db.session.flush()
|
||||
|
||||
# Attach the primary IP as a Communication when supplied (a device's IP is
|
||||
# not a NetworkDevice column). Mirrors the printer create route.
|
||||
if data.get('ipaddress'):
|
||||
_upsert_primary_ip(asset, data['ipaddress'])
|
||||
|
||||
# Preserve legacy timestamps in import mode (no-op otherwise)
|
||||
apply_import_timestamps(asset, data)
|
||||
|
||||
@@ -391,6 +425,7 @@ def create_network_device():
|
||||
|
||||
result = asset.to_dict()
|
||||
result['networkdevice'] = netdev.to_dict()
|
||||
result['ipaddress'] = _primary_ip(asset.assetid)
|
||||
|
||||
return success_response(result, message='Network device created', http_code=201)
|
||||
|
||||
@@ -465,11 +500,16 @@ def update_network_device(device_id: int):
|
||||
AuditLog.log('updated', 'NetworkDevice', entityid=netdev.networkdeviceid,
|
||||
entityname=netdev.hostname or asset.assetnumber, changes=changes)
|
||||
|
||||
# Upsert the primary IP Communication when an ipaddress is supplied.
|
||||
if 'ipaddress' in data:
|
||||
_upsert_primary_ip(asset, data['ipaddress'])
|
||||
|
||||
apply_import_timestamps(asset, data)
|
||||
db.session.commit()
|
||||
|
||||
result = asset.to_dict()
|
||||
result['networkdevice'] = netdev.to_dict()
|
||||
result['ipaddress'] = _primary_ip(asset.assetid)
|
||||
|
||||
return success_response(result, message='Network device updated')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user