Running `python scripts/export_catalog.py` on a deployed site died with 1045 "Access denied for user 'root'@'localhost'", naming a user that site does not have. Nothing loads .env for a plain script run. wsgi.py calls load_dotenv() for the served app and the flask CLI loads .env for itself, so both reach the real database; a bare `python scripts/...` gets neither. DATABASE_URL then stays unset, FLASK_ENV defaults to development, and config.py falls back to a hardcoded root@localhost URL. The read-only failure above is the harmless version. Six of these seven scripts WRITE - backfill, reclassify, retype, collapse, refile, import - and on any box where those fallback root credentials happen to resolve, they would have silently rewritten the wrong database with no error at all. That is also why this survived: the dev .env points at root, so the fallback matched what the developer expected and nothing ever looked wrong here. All seven now load .env from the repo root, resolved from __file__ rather than the working directory, since these are run by hand during a deployment and rarely from the repo root. tests/test_maintenance_scripts.py pins it for any future script: every script that calls create_app must call load_dotenv, before create_app rather than after it, and with an explicit path rather than a cwd-relative search.
128 lines
5.3 KiB
Python
128 lines
5.3 KiB
Python
"""Reclassify server 'computer' assets into 'network_device' assets.
|
|
|
|
Servers were imported as computers (a PC type), so they show under PCs instead
|
|
of Network. This re-points each server's asset in place - the assetid does NOT
|
|
change, so its communications, relationships, map position, name, location, and
|
|
audit history all carry over. Only the extension row is swapped (computers ->
|
|
networkdevices) and the asset's type is flipped, and the network device is given
|
|
the 'Server' networkdevicetype.
|
|
|
|
Identify servers by a name/hostname prefix (default 'SVR-'), or pass --type to
|
|
match an exact computer type instead.
|
|
|
|
Usage (on the target instance, in the app dir):
|
|
DATABASE_URL=... venv/bin/python scripts/reclassify_servers_to_network.py # dry run
|
|
DATABASE_URL=... venv/bin/python scripts/reclassify_servers_to_network.py --commit # apply
|
|
... --type "Server" --commit
|
|
"""
|
|
|
|
import argparse
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
# Run from anywhere: put the repo root (parent of scripts/) on the path.
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
from dotenv import load_dotenv
|
|
|
|
# Load .env before anything reads config. wsgi.py does this for the served app
|
|
# and the `flask` CLI does it for itself, but a plain `python scripts/...` run
|
|
# gets neither: DATABASE_URL stays unset, FLASK_ENV defaults to development,
|
|
# and config falls back to a hardcoded root@localhost URL. That fails on a real
|
|
# deployment with a bare 1045 Access denied for 'root', naming a user the site
|
|
# does not use - and on a box where those root credentials happen to work, it
|
|
# is worse, because the script silently reads or WRITES the wrong database.
|
|
load_dotenv(Path(__file__).resolve().parent.parent / '.env')
|
|
|
|
|
|
from shopdb import create_app
|
|
from shopdb.api import db
|
|
from shopdb.core.models import Asset, AssetType
|
|
from plugins.computers.models import Computer, ComputerType
|
|
from plugins.network.models import NetworkDevice, NetworkDeviceType
|
|
|
|
|
|
def _get_or_create_networkdevicetype(name):
|
|
ndt = NetworkDeviceType.query.filter_by(networkdevicetype=name).first()
|
|
if not ndt:
|
|
ndt = NetworkDeviceType(networkdevicetype=name,
|
|
description='Server (reclassified from PCs)')
|
|
db.session.add(ndt)
|
|
db.session.flush()
|
|
return ndt
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
group = parser.add_mutually_exclusive_group()
|
|
group.add_argument('--prefix', default='SVR-',
|
|
help="match servers by name/assetnumber/hostname starting "
|
|
"with this (default 'SVR-')")
|
|
group.add_argument('--type',
|
|
help="instead of --prefix, match by an exact computer type name")
|
|
parser.add_argument('--commit', action='store_true',
|
|
help='apply the change; without it this is a dry run')
|
|
args = parser.parse_args()
|
|
|
|
app = create_app()
|
|
with app.app_context():
|
|
nd_assettype = AssetType.query.filter_by(assettype='network_device').first()
|
|
if not nd_assettype:
|
|
sys.exit("No 'network_device' asset type - is the network plugin installed?")
|
|
|
|
query = Computer.query.join(Asset)
|
|
if args.type:
|
|
ctype = ComputerType.query.filter_by(computertype=args.type).first()
|
|
if not ctype:
|
|
sys.exit("No computer type named %r." % args.type)
|
|
query = query.filter(Computer.computertypeid == ctype.computertypeid)
|
|
criterion = "computer type %r" % args.type
|
|
else:
|
|
like = args.prefix + '%'
|
|
query = query.filter(db.or_(
|
|
Asset.assetnumber.like(like),
|
|
Asset.name.like(like),
|
|
Computer.hostname.like(like),
|
|
))
|
|
criterion = "name/hostname prefix %r" % args.prefix
|
|
|
|
servers = query.all()
|
|
if not servers:
|
|
print("No computers match %s - nothing to do." % criterion)
|
|
return
|
|
|
|
ndt = _get_or_create_networkdevicetype('Server')
|
|
print("%d server(s) matching %s -> network_device / type 'Server':"
|
|
% (len(servers), criterion))
|
|
|
|
moved = 0
|
|
for computer in servers:
|
|
asset = computer.asset
|
|
if not asset:
|
|
continue
|
|
label = asset.name or asset.assetnumber or ('asset %d' % asset.assetid)
|
|
print(" - %s (assetid %d)" % (label, asset.assetid))
|
|
if args.commit:
|
|
# Swap extension: create the network device on the SAME asset,
|
|
# re-point the asset's type, then drop the computer extension.
|
|
asset.assettypeid = nd_assettype.assettypeid
|
|
db.session.add(NetworkDevice(
|
|
assetid=asset.assetid,
|
|
networkdevicetypeid=ndt.networkdevicetypeid,
|
|
hostname=getattr(computer, 'hostname', None),
|
|
vendorid=getattr(computer, 'vendorid', None),
|
|
))
|
|
db.session.delete(computer) # asset stays (FK cascade is asset->computer)
|
|
moved += 1
|
|
|
|
if args.commit:
|
|
db.session.commit()
|
|
print("\nDone: %d reclassified. They now appear under Network." % moved)
|
|
else:
|
|
print("\nDRY RUN - nothing changed. Re-run with --commit to apply.")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|