Files
shopdb-flask/scripts/import_ntlars_backups.py
cproudlock bb4169cd8c backups: keep the seed script's docstring site-neutral
The import script is bundled in a multi-site product, so its usage examples
should not name one site's share layout or a dev box path.
2026-08-07 14:30:14 -04:00

138 lines
5.4 KiB
Python

#!/usr/bin/env python
"""Seed backup history from a directory of existing NTLARS .reg files.
A site's file share typically already holds per-machine backups collected by
hand over years, named by machine number. Importing them gives every machine a
baseline revision on day one, so the feature is useful before the fleet
collector has run even once.
Files are keyed by machine number in the FILENAME, which is how the share names
them. The MachineNo embedded in the file is compared against it and any
disagreement is reported: in the curated set the two always agree, so a
mismatch means the file is misfiled and should not be trusted as that machine's
baseline.
Dry run by default. Nothing is written without --commit.
venv/bin/python scripts/import_ntlars_backups.py /path/to/regs
venv/bin/python scripts/import_ntlars_backups.py /path/to/regs --commit
"""
import argparse
import os
import sys
from datetime import datetime
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from shopdb import create_app # noqa: E402
from shopdb.api import db # noqa: E402
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('directory', help='directory holding <machinenumber>.reg files')
parser.add_argument('--commit', action='store_true',
help='actually write (default is a dry run)')
parser.add_argument('--hostname', default='imported',
help="sourcehostname to record (default 'imported')")
args = parser.parse_args()
if not os.path.isdir(args.directory):
parser.error('not a directory: {}'.format(args.directory))
regs = sorted(f for f in os.listdir(args.directory) if f.lower().endswith('.reg'))
if not regs:
parser.error('no .reg files in {}'.format(args.directory))
app = create_app()
with app.app_context():
from shopdb.core.models import Asset
from plugins.backups.models import BackupRevision
from plugins.backups.services.registry import getkind, canonicalhash
kind = getkind('ntlars')
created = skipped = unresolved = rejected = duplicate = 0
mismatches = []
for filename in regs:
path = os.path.join(args.directory, filename)
machinenumber = os.path.splitext(filename)[0]
with open(path, 'rb') as handle:
raw = handle.read()
try:
projection = kind.parse(raw)
except ValueError as exc:
# Unconfigured NTLARS installs are in the corpus; they are not
# a usable baseline for anyone.
print(' REJECT {:<12} {}'.format(machinenumber, exc))
rejected += 1
continue
embedded = kind.embeddedmachineno(projection)
if embedded and embedded != machinenumber:
mismatches.append((machinenumber, embedded))
asset = db.session.query(Asset).filter(
Asset.assetnumber == machinenumber).first()
if asset is None:
print(' NOASSET {:<12} no asset with that assetnumber'.format(
machinenumber))
unresolved += 1
continue
contenthash = canonicalhash(projection)
latest = (db.session.query(BackupRevision)
.filter(BackupRevision.assetid == asset.assetid,
BackupRevision.backupkind == 'ntlars')
.order_by(BackupRevision.backuprevisionid.desc())
.first())
if latest is not None and latest.contenthash == contenthash:
duplicate += 1
continue
if args.commit:
revision = BackupRevision(
assetid=asset.assetid,
backupkind='ntlars',
storagebackend='shopdb',
contenthash=contenthash,
sourcefilename=filename,
bytesize=len(raw),
sourcehostname=args.hostname,
# The file's mtime is the closest thing to when the config
# was actually captured; createdat records the import.
collectedat=datetime.utcfromtimestamp(os.path.getmtime(path)),
)
revision.payload = projection
db.session.add(revision)
created += 1
if args.commit:
db.session.commit()
print()
print('files : {}'.format(len(regs)))
print('would create : {}'.format(created) if not args.commit
else 'created : {}'.format(created))
print('already current : {}'.format(duplicate))
print('no matching asset: {}'.format(unresolved))
print('rejected (blank) : {}'.format(rejected))
if mismatches:
print()
print('filename/embedded MachineNo disagreements ({}):'.format(
len(mismatches)))
for filename_no, embedded_no in mismatches[:20]:
print(' file={:<10} embedded={}'.format(filename_no, embedded_no))
print(' These are likely misfiled; review before trusting them.')
if not args.commit:
print()
print('DRY RUN - nothing written. Re-run with --commit to apply.')
if __name__ == '__main__':
main()