Security hardening, multi-site portability, and a documentation and API-surface sweep

This commit is contained in:
2026-08-14 16:28:34 -04:00
parent 72f8da0e58
commit 265ca2eb01
76 changed files with 3684 additions and 811 deletions

View File

@@ -0,0 +1,69 @@
"""Asset-number prefix per network device type.
Every network device already follows one convention, applied by hand:
AP-<name>, SW-<name>, SVR-<name>, IDF-<name>. 45 records, no exceptions. This
stores the prefix on the type so the create path can generate the asset number
instead of asking someone to retype the same value twice.
Nullable: a type with no prefix generates the bare name. Nobody has to invent
one for a type that does not want it.
It deliberately is NOT derived from the type name - 'Access Point' and 'Access
Panel' both initialise to AP, and assetnumber is unique, so the second type
would collide with the first on every device it created.
Backfilled for the four types already in use so existing conventions carry
forward. A site whose types are named differently gets NULL and generates bare
names, which is correct rather than wrong.
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'network0003prefix'
down_revision = 'network0002model'
branch_labels = None
depends_on = None
# type name -> prefix, from what the existing records already use
SEEDPREFIXES = {
'Access Point': 'AP',
'Switch': 'SW',
'Server': 'SVR',
'IDF': 'IDF',
}
def upgrade():
# Guarded like network0002model: on a FRESH database the table is built from
# the SQLAlchemy models, which already declare this column, so an
# unconditional add fails with "duplicate column name".
bind = op.get_bind()
insp = sa.inspect(bind)
if 'networkdevicetypes' not in insp.get_table_names():
return
cols = {c['name'] for c in insp.get_columns('networkdevicetypes')}
if 'prefix' not in cols:
op.add_column('networkdevicetypes',
sa.Column('prefix', sa.String(length=12), nullable=True))
# Backfill only where the type exists and has no prefix yet, so a re-run and
# a site that already set its own are both left alone.
for typename, prefix in SEEDPREFIXES.items():
bind.execute(
sa.text('UPDATE networkdevicetypes SET prefix = :prefix '
'WHERE networkdevicetype = :typename '
'AND (prefix IS NULL OR prefix = :empty)'),
{'prefix': prefix, 'typename': typename, 'empty': ''})
def downgrade():
bind = op.get_bind()
insp = sa.inspect(bind)
if 'networkdevicetypes' not in insp.get_table_names():
return
cols = {c['name'] for c in insp.get_columns('networkdevicetypes')}
if 'prefix' in cols:
op.drop_column('networkdevicetypes', 'prefix')