Printer assignment end to end, and a container that keeps a site's files
Assign printers to a MACHINE and let whichever PC controls it install them, with the observed half kept strictly apart from the assigned half: a collector report can never write an assignment row. Adoption is explicit. Adds a singular relationship type so defaultprinter can mean exactly one, client scripts that converge a bay's queues and correct a drifted one in place, and a wave gate so a fleet-wide driver rollout cannot stampede its own share. On the container side: the compose stack never persisted the instance directory, so a rebuild discarded plugins.json and every upload. It is a volume now, the image ships instance/ owned by the app user so the mountpoint is writable, and the database moves to MySQL 8.4 LTS, which the Windows runbook has recommended since 8.0 went end of life. The image also could not build at all: the frontend stage never copied the plugin staging script it runs. Adds a topology migration guide covering IIS to Docker and back.
This commit is contained in:
@@ -21,7 +21,7 @@
|
||||
sets in its .env so docker-compose.airgap.yml runs the matching image.
|
||||
|
||||
.PARAMETER MysqlImage
|
||||
MySQL image the stack runs (default mysql:8.0). Must match db.image in
|
||||
MySQL image the stack runs (default mysql:8.4). Must match db.image in
|
||||
docker-compose.airgap.yml.
|
||||
|
||||
.PARAMETER OutDir
|
||||
@@ -36,7 +36,7 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Version = '0.7.0',
|
||||
[string]$MysqlImage = 'mysql:8.0',
|
||||
[string]$MysqlImage = 'mysql:8.4',
|
||||
[string]$OutDir = '.',
|
||||
[switch]$SkipBuild
|
||||
)
|
||||
|
||||
@@ -51,6 +51,19 @@ def _upsert(h, path, payload, unique_field, idfield, list_path=None):
|
||||
return None
|
||||
|
||||
|
||||
def _put(h, path, payload):
|
||||
"""PUT through the harness client. The harness wraps post/get only, so the
|
||||
reconcile-style assignment route goes direct with the same import-mode
|
||||
headers. Returns (status, data)."""
|
||||
headers = {'Authorization': f'Bearer {h.secret}', 'X-Import-Mode': 'true'}
|
||||
resp = h.client.put(path, json=payload, headers=headers)
|
||||
body = resp.get_json() or {}
|
||||
data = body.get('data', body)
|
||||
if resp.status_code >= 400:
|
||||
h.errors.append((path, resp.status_code, payload, data))
|
||||
return resp.status_code, data
|
||||
|
||||
|
||||
# --- classic machinetype routing (per the resolved import decisions) ---------
|
||||
# measuringtools are the physical instruments (the CMM/gauge machine types). A
|
||||
# PC that DRIVES one (pctype CMM/Genspect/Keyence/Wax) is still a computer, not a
|
||||
@@ -350,10 +363,25 @@ def stage_printers(h):
|
||||
'modelnumberid': h.ids.get('model', p['modelid']),
|
||||
'locationid': h.ids.get('location', p['machineid']),
|
||||
}
|
||||
status, _ = h.post('/api/printers', payload)
|
||||
status, data = h.post('/api/printers', payload)
|
||||
assetid = None
|
||||
if status in (200, 201):
|
||||
assetid = _id_of(data, 'assetid')
|
||||
made += 1
|
||||
return {'printers': made}
|
||||
elif status == 409:
|
||||
# Re-run: PRN-<printerid> is already an asset. Resolve it anyway -
|
||||
# without this branch a resumed run crosswalks nothing and the
|
||||
# defaultprinters stage silently links nothing.
|
||||
_, rows = h.get(f"/api/printers?assetnumber={payload['assetnumber']}&per_page=5")
|
||||
items = rows.get('items', rows) if isinstance(rows, dict) else rows
|
||||
for row in (items or []):
|
||||
if str(row.get('assetnumber', '')).strip().lower() == \
|
||||
payload['assetnumber'].lower():
|
||||
assetid = _id_of(row, 'assetid')
|
||||
break
|
||||
if assetid:
|
||||
h.ids.put('printer', p['printerid'], assetid)
|
||||
return {'printers': made, 'crosswalked': h.ids.count('printer')}
|
||||
|
||||
|
||||
# classic pctype -> the measuring instrument that PC drives
|
||||
@@ -676,6 +704,77 @@ def stage_relationships(h):
|
||||
return {'relationships': made, 'dropped_unresolved': dropped}
|
||||
|
||||
|
||||
def stage_defaultprinters(h):
|
||||
"""classic machines.printerid -> a printer assignment on the imported asset.
|
||||
|
||||
Classic records exactly one default printer per machine row. It goes through
|
||||
the assignment reconcile route rather than raw relationship posts: the
|
||||
usesprinter + defaultprinter pairing, printer-type validation, the
|
||||
default-must-be-in-the-set rule and the soft-delete/reactivate semantics all
|
||||
live there, and reconciling converges on a re-run instead of duplicating.
|
||||
|
||||
Runs after assets and printers - it resolves both ends through their
|
||||
crosswalks and drops the row when either end did not import. Needs the
|
||||
usesprinter/defaultprinter relationship types seeded (flask seed
|
||||
reference-data): the route writes nothing without them and every row fails.
|
||||
"""
|
||||
# printerid=0 means no printer recorded, not a dangling FK - excluded so it
|
||||
# never lands in the drop count as something someone has to explain.
|
||||
rows = h.source.rows('shopdb_src',
|
||||
'SELECT machineid, printerid FROM machines '
|
||||
'WHERE isactive=1 AND printerid IS NOT NULL AND printerid>0')
|
||||
# Legacy names for the drop report. Most machines point at a retired
|
||||
# placeholder printer, and a bare "dropped 560" reads like data loss.
|
||||
printernames = {row['printerid']: (row['printerwindowsname'] or '').strip()
|
||||
for row in h.source.rows(
|
||||
'shopdb_src',
|
||||
'SELECT printerid, printerwindowsname FROM printers')}
|
||||
|
||||
counts = {'source_rows': len(rows), 'linked': 0, 'already': 0,
|
||||
'dropped_no_asset': 0, 'dropped_printer_not_imported': 0,
|
||||
'failed': 0}
|
||||
dropped_printers = {}
|
||||
for r in rows:
|
||||
assetid = h.ids.get('asset', r['machineid'])
|
||||
if not assetid:
|
||||
# Machine became a Location, was the 9999 placeholder, a duplicate
|
||||
# machinenumber, or a skipped type.
|
||||
counts['dropped_no_asset'] += 1
|
||||
continue
|
||||
printerassetid = h.ids.get('printer', r['printerid'])
|
||||
if not printerassetid:
|
||||
counts['dropped_printer_not_imported'] += 1
|
||||
label = '{0} {1}'.format(r['printerid'],
|
||||
printernames.get(r['printerid'], '?'))
|
||||
dropped_printers[label] = dropped_printers.get(label, 0) + 1
|
||||
continue
|
||||
|
||||
# Read first: the PUT reconciles the WHOLE set, so a blind write would
|
||||
# unassign anything else already on this asset. Matching state is left
|
||||
# alone so a second run is a no-op, not a rewrite.
|
||||
_, current = h.get(f'/api/printers/assignments/for-asset/{assetid}')
|
||||
current = current if isinstance(current, dict) else {}
|
||||
assigned = list(current.get('printerassetids') or [])
|
||||
if (current.get('defaultprinterassetid') == printerassetid
|
||||
and printerassetid in assigned):
|
||||
counts['already'] += 1
|
||||
continue
|
||||
if printerassetid not in assigned:
|
||||
assigned.append(printerassetid)
|
||||
status, _ = _put(h, f'/api/printers/assignments/for-asset/{assetid}',
|
||||
{'printerassetids': assigned,
|
||||
'defaultprinterassetid': printerassetid})
|
||||
if status in (200, 201):
|
||||
counts['linked'] += 1
|
||||
else:
|
||||
counts['failed'] += 1
|
||||
|
||||
if dropped_printers:
|
||||
counts['dropped_printers'] = dict(
|
||||
sorted(dropped_printers.items(), key=lambda item: -item[1])[:10])
|
||||
return counts
|
||||
|
||||
|
||||
def stage_subnets(h):
|
||||
"""Subnets + VLANs. Classic cidr is the suffix only; full CIDR =
|
||||
INET_NTOA(ipstart)+suffix. VLANs are lookup-or-create by number; duplicate
|
||||
@@ -792,6 +891,7 @@ STAGES = {
|
||||
'notifications': stage_notifications,
|
||||
'knowledgebase': stage_knowledgebase,
|
||||
'relationships': stage_relationships,
|
||||
'defaultprinters': stage_defaultprinters,
|
||||
'subnets': stage_subnets,
|
||||
'usb': stage_usb,
|
||||
'verify': stage_verify,
|
||||
@@ -804,7 +904,7 @@ def main():
|
||||
'--stages',
|
||||
default='reference,employees,catalog,assets,locations,printers,'
|
||||
'metrology,communications,applications,warranties,notifications,'
|
||||
'knowledgebase,relationships,subnets,usb,verify',
|
||||
'knowledgebase,relationships,defaultprinters,subnets,usb,verify',
|
||||
help='comma list of stages to run')
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user