Read a numeric natural key as a name before reading it as an id
All checks were successful
CI / naming (push) Successful in 4s
CI / frontend (push) Successful in 13s
CI / migrations-mysql (push) Successful in 41s
CI / backend (push) Successful in 4m18s

An Azure device import died part-way with a foreign-key error, and the crash
was the lucky outcome.

The Azure adapter routes a device that already exists to its real asset by
writing that asset's assetnumber into the assetid column of the CSV it
generates, expecting the engine to look it up. The resolver instead takes any
all-digit string at face value as a literal id. This site numbers operations
0NNN, so numeric assetnumbers are ordinary here: `0704` was read as id 704,
which does not exist, hence the error. `0600` would have been read as id 600 -
a printer - and bound a PC to it silently, reporting success.

Two changes, both needed.

The resolver now tries the label first for an all-digit string and only falls
back to reading it as an id when nothing is named that. The face-value
behaviour exists for a real reason - exports round-trip and carry real ids, as
its original comment says - so it stays as the fallback rather than being
removed. Somebody writing `0600` in a column keyed on assetnumber means the
asset called 0600, and binding the row to an unrelated record is far worse than
failing.

_existing_hostname_map now hands over the assetid rather than the assetnumber.
Giving the resolver the id it is going to treat the value as makes the
passthrough correct by construction instead of correct by luck.

Verified against a restored copy of the production database: 234 devices, 221
matching an existing computer and updating in place, 13 new, no problems. The
same run previously failed on the foreign key.

Neither half was wrong alone. The passthrough dates from the original CSV
import and the hostname map from the wilm merge months later; they only meet
at a site whose natural keys are numeric.

Scope: product
This commit is contained in:
cproudlock
2026-08-31 15:55:21 -04:00
parent 2feb569686
commit 259fe0de8f
3 changed files with 74 additions and 10 deletions

View File

@@ -334,23 +334,32 @@ def download_import_template(tablename):
def _existing_hostname_map():
"""Case-insensitive hostname -> assetnumber map for existing computers.
"""Case-insensitive hostname -> assetid map for existing computers.
Azure device-group exports align on hostname (their displayName), while the
import engine keys assets on assetnumber. This lets the Azure adapter route
a matched device to its real asset so the import updates in place.
Azure and Thor exports align on hostname (their displayName), while the
import engine keys assets on assetnumber. This lets those adapters route a
matched device to its real asset so the import updates in place.
THE ID, NOT THE ASSETNUMBER, and that is not a style choice. The resolver
takes any all-digit value at face value as a literal id rather than looking
it up, so an assetnumber like `0600` resolved to asset 600 - a different
asset entirely, or none. This site numbers operations 0NNN, so those
assetnumbers are real and common: one of them pointed a PC at a printer's
record, and another at an id that does not exist, which is the foreign-key
error an Azure import died on. Handing over the id the resolver is going to
treat it as makes the passthrough correct by construction.
"""
assets = db.metadata.tables['assets']
computers = db.metadata.tables['computers']
query = sa_select(
computers.c.hostname, assets.c.assetnumber
computers.c.hostname, assets.c.assetid
).select_from(
computers.join(assets, computers.c.assetid == assets.c.assetid)
).where(computers.c.hostname.isnot(None))
mapping = {}
for hostname, assetnumber in db.session.execute(query):
for hostname, assetid in db.session.execute(query):
if hostname:
mapping[hostname.strip().lower()] = assetnumber
mapping[hostname.strip().lower()] = assetid
return mapping