Add two commands for cleaning up computer types
Twenty computer types existed and six had a machine on them. Three of the empty ones were near-duplicates of live types - Shopfloor beside Shopfloor PC, CMM beside CMM PC, Engineer Workstation beside Engineer - which is the kind of list that grows because deleting from it by hand is nerve-wracking. retype-by-role types a PC by WHAT IT CONTROLS. A metrology box has an active relationship to a measuring_tool asset, so the fact is already recorded and does not need inferring from a hostname or a tool's name. The imaging scope is the authoritative source and stays so, but only 17 of roughly 292 PCs have ever reported one, so it cannot answer for the fleet yet. Relationships can: 49 PCs drive a measuring tool, and 36 of them are currently typed as ordinary shopfloor machines. prune-types deletes a type only when nothing is on it AND the live pctype map does not name it. The second guard is the one worth having: the map is what the collector assigns from at check-in, so deleting a mapped type would leave every future check-in logging 'mapped ComputerType not found' instead of typing the machine. Both report by default and write only with --apply, and prune prints why it is keeping each type it keeps rather than only what it removes. Also fixes `computers stats`, which counted the computer type named 'Shopfloor' - the EMPTY duplicate - rather than 'Shopfloor PC'. It has been reporting 0 shopfloor PCs for as long as both rows existed, which is the same duplication these commands clear up. Scope: product
This commit is contained in:
@@ -1875,6 +1875,123 @@ class ComputersPlugin(BasePlugin):
|
||||
for t in types:
|
||||
click.echo(f" [{t.computertypeid}] {t.computertype}")
|
||||
|
||||
@computerscli.command('retype-by-role')
|
||||
@click.option('--apply', 'apply_changes', is_flag=True,
|
||||
help='Actually write. Without this the command only reports.')
|
||||
@click.option('--typename', default='Metrology PC', show_default=True,
|
||||
help='ComputerType to assign to PCs that drive a measuring tool.')
|
||||
def retype_by_role(apply_changes, typename):
|
||||
"""Type PCs that drive a measuring tool as metrology machines.
|
||||
|
||||
WHY THE RELATIONSHIP AND NOT THE NAME. A PC's role is already
|
||||
recorded in what it controls: a metrology box has an active
|
||||
relationship to a measuring_tool asset. Matching on hostnames or on
|
||||
the tool's name would guess; this reads the fact.
|
||||
|
||||
The imaging scope (gea-shopfloor-keyence and friends) is the OTHER
|
||||
source, and it is the authoritative one - but only 17 of ~292 PCs
|
||||
have ever reported an enforcement scope, so it cannot answer for the
|
||||
fleet today. Relationships can.
|
||||
"""
|
||||
from shopdb.api import Asset
|
||||
|
||||
target = ComputerType.query.filter_by(computertype=typename).first()
|
||||
if not target:
|
||||
if not apply_changes:
|
||||
click.echo(click.style(
|
||||
"Type '%s' does not exist yet; it would be created."
|
||||
% typename, fg='yellow'))
|
||||
else:
|
||||
target = ComputerType(computertype=typename, isactive=True)
|
||||
db.session.add(target)
|
||||
db.session.flush()
|
||||
click.echo("Created ComputerType '%s'." % typename)
|
||||
|
||||
rows = db.session.execute(db.text("""
|
||||
SELECT DISTINCT c.computerid, a.assetnumber, c.hostname,
|
||||
ct.computertype
|
||||
FROM computers c
|
||||
JOIN assets a ON a.assetid = c.assetid
|
||||
LEFT JOIN computertypes ct ON ct.computertypeid = c.computertypeid
|
||||
JOIN assetrelationships r ON r.sourceassetid = c.assetid
|
||||
JOIN assets ta ON ta.assetid = r.targetassetid
|
||||
JOIN assettypes at ON at.assettypeid = ta.assettypeid
|
||||
WHERE at.assettype = 'measuring_tool' AND r.isactive = 1
|
||||
ORDER BY ct.computertype, a.assetnumber
|
||||
""")).fetchall()
|
||||
|
||||
moving = [r for r in rows if (r[3] or '') != typename]
|
||||
click.echo('%d PC(s) drive a measuring tool; %d would change type.'
|
||||
% (len(rows), len(moving)))
|
||||
if not moving:
|
||||
return
|
||||
for r in moving:
|
||||
click.echo(' %-14s %-14s %-16s -> %s'
|
||||
% (r[1], r[2] or '(no hostname)', r[3] or '(none)', typename))
|
||||
|
||||
if not apply_changes:
|
||||
click.echo(click.style(
|
||||
'Dry run. Nothing was written. Re-run with --apply.', fg='yellow'))
|
||||
return
|
||||
|
||||
for r in moving:
|
||||
db.session.get(Computer, r[0]).computertypeid = target.computertypeid
|
||||
db.session.commit()
|
||||
click.echo(click.style('Retyped %d PC(s).' % len(moving), fg='green'))
|
||||
|
||||
@computerscli.command('prune-types')
|
||||
@click.option('--apply', 'apply_changes', is_flag=True,
|
||||
help='Actually delete. Without this the command only reports.')
|
||||
def prune_types(apply_changes):
|
||||
"""Delete computer types nothing uses and nothing maps to.
|
||||
|
||||
Two guards, because a type is cheap to keep and expensive to delete
|
||||
by mistake. A type is KEPT when any computer is on it, or when the
|
||||
live pctype map names it - the map is what the collector assigns
|
||||
from at check-in, so deleting a mapped type would make every future
|
||||
check-in log 'mapped ComputerType not found' instead of typing the
|
||||
machine.
|
||||
"""
|
||||
from .pctypemap import pctype_mapping
|
||||
|
||||
mapped = {str(v).strip().lower() for v in pctype_mapping().values()}
|
||||
counts = dict(db.session.execute(db.text("""
|
||||
SELECT ct.computertypeid, COUNT(c.computerid)
|
||||
FROM computertypes ct
|
||||
LEFT JOIN computers c ON c.computertypeid = ct.computertypeid
|
||||
GROUP BY ct.computertypeid
|
||||
""")).fetchall())
|
||||
|
||||
removable, kept = [], []
|
||||
for ctype in ComputerType.query.order_by(ComputerType.computertype).all():
|
||||
inuse = counts.get(ctype.computertypeid, 0)
|
||||
ismapped = ctype.computertype.strip().lower() in mapped
|
||||
if inuse:
|
||||
kept.append((ctype.computertype, '%d computer(s)' % inuse))
|
||||
elif ismapped:
|
||||
kept.append((ctype.computertype, 'named in the pctype map'))
|
||||
else:
|
||||
removable.append(ctype)
|
||||
|
||||
click.echo('KEEPING %d:' % len(kept))
|
||||
for name, why in kept:
|
||||
click.echo(' %-24s %s' % (name, why))
|
||||
click.echo('')
|
||||
click.echo('DELETING %d:' % len(removable))
|
||||
for ctype in removable:
|
||||
click.echo(' %s' % ctype.computertype)
|
||||
|
||||
if not removable:
|
||||
return
|
||||
if not apply_changes:
|
||||
click.echo(click.style(
|
||||
'Dry run. Nothing was deleted. Re-run with --apply.', fg='yellow'))
|
||||
return
|
||||
for ctype in removable:
|
||||
db.session.delete(ctype)
|
||||
db.session.commit()
|
||||
click.echo(click.style('Deleted %d type(s).' % len(removable), fg='green'))
|
||||
|
||||
@computerscli.command('stats')
|
||||
def stats():
|
||||
"""Show computer statistics."""
|
||||
@@ -1888,8 +2005,10 @@ class ComputersPlugin(BasePlugin):
|
||||
|
||||
click.echo(f"Total active computers: {total}")
|
||||
|
||||
# Shopfloor count (by the Shopfloor computer type)
|
||||
sf = ComputerType.query.filter_by(computertype='Shopfloor').first()
|
||||
# 'Shopfloor PC' - NOT 'Shopfloor', which is an empty
|
||||
# duplicate this counted for as long as both existed, so the
|
||||
# number reported was always 0.
|
||||
sf = ComputerType.query.filter_by(computertype='Shopfloor PC').first()
|
||||
shopfloor = db.session.query(Computer).join(Asset).filter(
|
||||
Asset.isactive == True,
|
||||
Computer.computertypeid == (sf.computertypeid if sf else -1)
|
||||
|
||||
Reference in New Issue
Block a user