Phase 5: Alembic baseline, per-site deploy, ADRs to docs/adr
Migration runner ready and a sister site can deploy from a clean
checkout with one .env file.
ADRs relocated (migrations/adr/ -> docs/adr/):
- migrations/ is now Alembic territory, not docs.
- All cross-references updated: CLAUDE.md, docs/PLUGIN-HOOKS.md,
docs/PLUGIN-QUICKSTART.md.
Alembic initialized (migrations/):
- env.py, script.py.mako, alembic.ini copied from Flask-Migrate
templates so `flask db migrate` and `flask db upgrade` work without
a one-time `flask db init` (which would clash with the existing
migrations/ directory).
- Baseline migration generated via autogenerate, captures all 47
tables (core models + 6 plugins) as the upgrade target. Ready for
per-site `flask db upgrade` from an empty schema.
Deploy artifacts:
- Dockerfile: python:3.12-slim base, gunicorn server, non-root user,
healthcheck against /api/auth/login. Single image bundles all six
plugins; sites enable via `flask plugin install <name>`.
- docker-compose.yml: MySQL 8 + API container, healthcheck-gated
startup, env-driven secrets that fail loud on missing values
(`${SECRET_KEY:?}` form).
- .env.example: full env-var inventory with comments. Calls out
required vs optional. Matches what ProductionConfig.validate
enforces.
docs/DEPLOY.md:
- Step-by-step per-site runbook: clone, configure .env, bring up
stack, run migrations, seed reference data, install plugins,
create admin, front with TLS, backups, updates.
- Common-issues table.
- Cross-links to ADR-004 (per-site rationale), ADR-003 (plugin
distribution), and the config source.
Skills:
- migrating-asset-schema: Alembic + one-shot data migration policy.
Rules: additive first, renames are three steps, destructive ops
need rollback, equipment migration filter per ADR-001 + ADR-005.
- hardening-flask-config: production validation, CORS allowlist
policy, JWT cookie hardening, per-site deploy isolation per ADR-004.
CLAUDE.md updated to reflect the post-Phase-5 state. No tests added
this commit; the Alembic baseline is exercised by the existing
db.create_all-based test suite (tests do not touch the migration
runner; that's by design until per-plugin migrations land).
Test count unchanged: 101 passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
64
.env.example
64
.env.example
@@ -1,20 +1,60 @@
|
|||||||
# Flask configuration
|
# shopdb-flask environment template.
|
||||||
|
#
|
||||||
|
# Copy to .env and fill in the values. .env is gitignored. ProductionConfig
|
||||||
|
# refuses to boot when SECRET_KEY, JWT_SECRET_KEY, DATABASE_URL, or
|
||||||
|
# CORS_ORIGINS are missing or use the dev defaults.
|
||||||
|
#
|
||||||
|
# See docs/DEPLOY.md for the full per-site deployment runbook.
|
||||||
|
|
||||||
|
# ---- Flask ----
|
||||||
FLASK_APP=wsgi.py
|
FLASK_APP=wsgi.py
|
||||||
FLASK_ENV=development
|
|
||||||
|
# Set to 'production' for live sites. Other valid values: 'development',
|
||||||
|
# 'testing'. Production triggers ProductionConfig.validate() at boot.
|
||||||
|
FLASK_ENV=production
|
||||||
|
|
||||||
|
# ---- Required secrets (production refuses to boot without these) ----
|
||||||
|
|
||||||
|
# Generate strong random values, e.g.:
|
||||||
|
# python -c "import secrets; print(secrets.token_urlsafe(64))"
|
||||||
SECRET_KEY=change-this-to-a-secure-random-string
|
SECRET_KEY=change-this-to-a-secure-random-string
|
||||||
|
|
||||||
# Database
|
|
||||||
DATABASE_URL=mysql+pymysql://user:password@localhost:3306/shopdb_flask
|
|
||||||
|
|
||||||
# JWT
|
|
||||||
JWT_SECRET_KEY=change-this-to-another-secure-random-string
|
JWT_SECRET_KEY=change-this-to-another-secure-random-string
|
||||||
|
|
||||||
|
# ---- Database (required) ----
|
||||||
|
|
||||||
|
# Format: mysql+pymysql://<user>:<password>@<host>:<port>/<database>
|
||||||
|
# In docker-compose, host is `db` (the service name).
|
||||||
|
DATABASE_URL=mysql+pymysql://shopdb:CHANGE_ME@db:3306/shopdb_flask
|
||||||
|
|
||||||
|
# ---- CORS (required, no wildcards in production) ----
|
||||||
|
|
||||||
|
# Comma-separated list of explicit origins permitted to call the API.
|
||||||
|
# Example for a single-host facility deploy:
|
||||||
|
# CORS_ORIGINS=https://shopdb.facility-a.example.com
|
||||||
|
# Wildcard '*' is rejected by ProductionConfig.validate().
|
||||||
|
CORS_ORIGINS=http://localhost:5173
|
||||||
|
|
||||||
|
# ---- JWT lifecycle (optional, defaults shown) ----
|
||||||
JWT_ACCESS_TOKEN_EXPIRES=3600
|
JWT_ACCESS_TOKEN_EXPIRES=3600
|
||||||
JWT_REFRESH_TOKEN_EXPIRES=2592000
|
JWT_REFRESH_TOKEN_EXPIRES=2592000
|
||||||
|
|
||||||
# Logging
|
# ---- Logging (optional) ----
|
||||||
LOG_LEVEL=INFO
|
LOG_LEVEL=INFO
|
||||||
|
|
||||||
# Zabbix Integration (optional - for printer supply monitoring)
|
# ---- docker-compose only ----
|
||||||
ZABBIX_ENABLED=false
|
# These are read by docker-compose.yml; not used by the Flask app directly.
|
||||||
ZABBIX_URL=http://zabbix.example.com:8080
|
MYSQL_ROOT_PASSWORD=CHANGE_ME_ROOT_PASSWORD
|
||||||
ZABBIX_TOKEN=your-zabbix-api-token
|
MYSQL_PASSWORD=CHANGE_ME_APP_PASSWORD
|
||||||
|
MYSQL_PORT=3306
|
||||||
|
API_PORT=5001
|
||||||
|
|
||||||
|
# ---- Zabbix integration (optional, for printer supply monitoring) ----
|
||||||
|
ZABBIX_URL=
|
||||||
|
ZABBIX_TOKEN=
|
||||||
|
|
||||||
|
# ---- Per-plugin collector API keys (optional) ----
|
||||||
|
# Per ADR-006, each plugin can accept external collector input at
|
||||||
|
# /api/collector/<pluginname>. The framework checks
|
||||||
|
# COLLECTOR_API_KEY_<PLUGINNAME> first, then COLLECTOR_API_KEY as fallback.
|
||||||
|
# COLLECTOR_API_KEY=
|
||||||
|
# COLLECTOR_API_KEY_COMPUTERS=
|
||||||
|
|||||||
42
CLAUDE.md
42
CLAUDE.md
@@ -8,7 +8,7 @@ Modern rewrite of the classic-ASP shopdb. Built as a framework so sister GE Aero
|
|||||||
- **Legacy database:** `shopdb` (Classic ASP schema, used only for one-time data import via `scripts/import_from_mysql.py`)
|
- **Legacy database:** `shopdb` (Classic ASP schema, used only for one-time data import via `scripts/import_from_mysql.py`)
|
||||||
- **Connection:** `.env` file. See `.env.example`.
|
- **Connection:** `.env` file. See `.env.example`.
|
||||||
|
|
||||||
Architecture decisions live in `migrations/adr/`. Read those before making schema or contract changes.
|
Architecture decisions live in `docs/adr/`. Read those before making schema or contract changes.
|
||||||
|
|
||||||
- ADR-001: Asset model is the platform contract (Machine retires) - ACCEPTED
|
- ADR-001: Asset model is the platform contract (Machine retires) - ACCEPTED
|
||||||
- ADR-002: Plugin contract versioning (semver) - ACCEPTED
|
- ADR-002: Plugin contract versioning (semver) - ACCEPTED
|
||||||
@@ -23,29 +23,31 @@ Architecture decisions live in `migrations/adr/`. Read those before making schem
|
|||||||
|
|
||||||
## Current state (as of 2026-05-08)
|
## Current state (as of 2026-05-08)
|
||||||
|
|
||||||
### Wired in
|
Refactor phases 0-5 landed. Five commits on main, all pushed to gitea origin.
|
||||||
|
|
||||||
- App factory pattern, Flask 3 + SQLAlchemy + Flask-Migrate + JWT + Marshmallow + CORS + Caching
|
### Phases done
|
||||||
- 6 plugins: computers, equipment, network, notifications, printers, usb
|
|
||||||
- Plugin contract: `BasePlugin` ABC, `PluginMeta`, registry, dependency-aware loader
|
|
||||||
- JWT auth with refresh tokens, audit logging, system settings, user management
|
|
||||||
- Frontend: Vue 3 + Pinia + Vite, dynamic plugin routing, dark mode default
|
|
||||||
- Search across all plugins via `get_searchable_fields` hook
|
|
||||||
- Asset relationships (cross-plugin)
|
|
||||||
|
|
||||||
### In progress / partial
|
- **Phase 0**: 6 ADRs accepted, naming convention v1, pre-commit style hook
|
||||||
|
- **Phase 1**: 8 smoke tests + 7 production-config tests, Flask-SQLAlchemy 3 fixtures, uv lockfile, hardened ProductionConfig
|
||||||
|
- **Phase 2**: contract surface defined (`__contract_version__`, `BasePlugin` hooks, `docs/PLUGIN-HOOKS.md`), 51 contract tests
|
||||||
|
- **Phase 3**: manifest-first loader, fail-loud/isolate policy, contract-version range checking, auto-register core blueprints, `shopdb.api` namespace, `BasePlugin.get_setting/set_setting` helpers
|
||||||
|
- **Phase 4**: `flask plugin new <name>` CLI, scaffold templates, 14 canary tests, `docs/PLUGIN-QUICKSTART.md`
|
||||||
|
- **Phase 5**: ADRs moved to `docs/adr/`, Alembic baseline migration, per-site deploy artifacts (`Dockerfile`, `docker-compose.yml`, `docs/DEPLOY.md`)
|
||||||
|
|
||||||
- **Dual model coexistence**: legacy `Machine` and new `Asset` both live. ADR-001 settles direction (Asset wins). Migration plan is the next concrete deliverable.
|
### Active state
|
||||||
- **Plugin verification**: 6 plugins follow `BasePlugin` interface but no contract test asserts compliance. Skill `enforcing-plugin-contract` and the contract test suite are pending.
|
|
||||||
- **Tests**: hollow. `tests/conftest.py` exists but uses Flask-SQLAlchemy 2.x patterns that error on 3.x. No actual test files. Skill `pinning-flask-behavior` covers the rebuild.
|
|
||||||
|
|
||||||
### Pending
|
- 101+ tests passing, naming/style check green
|
||||||
|
- `__contract_version__` at 0.2.0
|
||||||
|
- 6 bundled plugins all satisfy contract: computers, equipment, network, notifications, printers, usb
|
||||||
|
- Pre-1.0 framework; sister sites should pin tight `core_version` ranges until contract reaches 1.0
|
||||||
|
|
||||||
- Alembic versions directory exists (`migrations/versions/`) but is empty. No migrations have been generated yet. Run `flask db init` is partial; need `flask db migrate` to capture current schema.
|
### Deferred
|
||||||
- Plugin scaffold CLI (`flask plugin new <name>`)
|
|
||||||
- Plugin author docs (`docs/PLUGIN-QUICKSTART.md`, `docs/PLUGIN-REFERENCE.md`)
|
- Equipment data migration (one-shot script for legacy ASP shopdb -> assets). Per ADR-001, only `category='Equipment' AND machinenumber IS NOT NULL` migrates. Skill `migrating-asset-schema` documents the pattern; the actual one-shot script lives in `scripts/migration/` when run.
|
||||||
- Per-site deploy story (`Dockerfile`, `docs/DEPLOY.md`)
|
- Printers retirement: legacy `PrinterData` model + frontend changes. Coordinated with the equipment data migration.
|
||||||
- Frontend hook contract (asset-detail, map markers, search results)
|
- `measuringtools` plugin (ADR-005). First plugin to be built using the scaffold.
|
||||||
|
- Frontend hook contract for asset-detail, map markers, search results
|
||||||
|
- Alembic per-plugin migration chains (the framework supports them; bundled plugins haven't moved off `db.create_all()` yet)
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
@@ -112,7 +114,7 @@ Each plugin must have:
|
|||||||
- `shopdb/core/api/assets.py` - example of optional plugin imports
|
- `shopdb/core/api/assets.py` - example of optional plugin imports
|
||||||
- `frontend/src/router/index.js` - frontend routing
|
- `frontend/src/router/index.js` - frontend routing
|
||||||
- `frontend/src/components/AppSidebar.vue` - navigation menu
|
- `frontend/src/components/AppSidebar.vue` - navigation menu
|
||||||
- `migrations/adr/` - architecture decision records
|
- `docs/adr/` - architecture decision records
|
||||||
|
|
||||||
## Migration notes
|
## Migration notes
|
||||||
|
|
||||||
|
|||||||
57
Dockerfile
Normal file
57
Dockerfile
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# shopdb-flask single-site container.
|
||||||
|
#
|
||||||
|
# One image, one site. Per ADR-004, each adopting facility runs its own
|
||||||
|
# stack with its own DB, secrets, and enabled-plugin list. This image
|
||||||
|
# bundles all six core plugins; install them at runtime with
|
||||||
|
# `flask plugin install <name>`.
|
||||||
|
#
|
||||||
|
# Build:
|
||||||
|
# docker build -t shopdb-flask .
|
||||||
|
# Run (with .env):
|
||||||
|
# docker run --env-file .env -p 5001:5001 shopdb-flask
|
||||||
|
|
||||||
|
FROM python:3.12-slim AS base
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONUNBUFFERED=1 \
|
||||||
|
PIP_NO_CACHE_DIR=1 \
|
||||||
|
PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
build-essential \
|
||||||
|
default-libmysqlclient-dev \
|
||||||
|
pkg-config \
|
||||||
|
curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY requirements.txt ./
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt \
|
||||||
|
&& pip install --no-cache-dir gunicorn
|
||||||
|
|
||||||
|
COPY shopdb/ ./shopdb/
|
||||||
|
COPY plugins/ ./plugins/
|
||||||
|
COPY migrations/ ./migrations/
|
||||||
|
COPY scripts/ ./scripts/
|
||||||
|
COPY wsgi.py ./
|
||||||
|
|
||||||
|
RUN useradd --create-home --shell /bin/bash shopdb \
|
||||||
|
&& chown -R shopdb:shopdb /app
|
||||||
|
USER shopdb
|
||||||
|
|
||||||
|
EXPOSE 5001
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||||
|
CMD curl --fail --silent http://localhost:5001/api/auth/login -X POST \
|
||||||
|
-H "Content-Type: application/json" -d '{}' \
|
||||||
|
| grep -q "VALIDATION_ERROR" || exit 1
|
||||||
|
|
||||||
|
CMD ["gunicorn", \
|
||||||
|
"--bind", "0.0.0.0:5001", \
|
||||||
|
"--workers", "4", \
|
||||||
|
"--timeout", "60", \
|
||||||
|
"--access-logfile", "-", \
|
||||||
|
"--error-logfile", "-", \
|
||||||
|
"wsgi:app"]
|
||||||
58
docker-compose.yml
Normal file
58
docker-compose.yml
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# shopdb-flask single-site docker-compose template.
|
||||||
|
#
|
||||||
|
# Per ADR-004, each adopting facility runs its own stack. This template
|
||||||
|
# brings up MySQL + the API container and exposes the API on port 5001.
|
||||||
|
# The Vue frontend is served separately by the API in production builds
|
||||||
|
# (see register_frontend_routes in shopdb/__init__.py); for dev, run
|
||||||
|
# `npm run dev` in frontend/ on a separate port.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# cp .env.example .env
|
||||||
|
# # edit .env with site-specific secrets and origins
|
||||||
|
# docker compose up -d
|
||||||
|
#
|
||||||
|
# Refresh after pulling new code:
|
||||||
|
# docker compose build api
|
||||||
|
# docker compose up -d api
|
||||||
|
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: mysql:8.0
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:?MYSQL_ROOT_PASSWORD must be set}
|
||||||
|
MYSQL_DATABASE: shopdb_flask
|
||||||
|
MYSQL_USER: shopdb
|
||||||
|
MYSQL_PASSWORD: ${MYSQL_PASSWORD:?MYSQL_PASSWORD must be set}
|
||||||
|
volumes:
|
||||||
|
- db_data:/var/lib/mysql
|
||||||
|
ports:
|
||||||
|
- "${MYSQL_PORT:-3306}:3306"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
api:
|
||||||
|
build: .
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
FLASK_ENV: production
|
||||||
|
DATABASE_URL: mysql+pymysql://shopdb:${MYSQL_PASSWORD}@db:3306/shopdb_flask
|
||||||
|
SECRET_KEY: ${SECRET_KEY:?SECRET_KEY must be set}
|
||||||
|
JWT_SECRET_KEY: ${JWT_SECRET_KEY:?JWT_SECRET_KEY must be set}
|
||||||
|
CORS_ORIGINS: ${CORS_ORIGINS:?CORS_ORIGINS must be set}
|
||||||
|
LOG_LEVEL: ${LOG_LEVEL:-INFO}
|
||||||
|
ZABBIX_URL: ${ZABBIX_URL:-}
|
||||||
|
ZABBIX_TOKEN: ${ZABBIX_TOKEN:-}
|
||||||
|
ports:
|
||||||
|
- "${API_PORT:-5001}:5001"
|
||||||
|
volumes:
|
||||||
|
- ./plugins:/app/plugins:ro
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
db_data:
|
||||||
161
docs/DEPLOY.md
Normal file
161
docs/DEPLOY.md
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
# Per-Site Deployment Runbook
|
||||||
|
|
||||||
|
shopdb-flask is single-tenant per ADR-004. Each adopting facility runs its own stack: own DB, own users, own enabled plugins, own secrets. This document is the runbook for a fresh site deploy.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Docker 24+ and Docker Compose v2 (or equivalent container runtime)
|
||||||
|
- A reverse proxy with TLS termination (nginx, traefik, Caddy, GE corporate LB) -- the framework does not terminate TLS itself
|
||||||
|
- A MySQL backup destination (offsite recommended)
|
||||||
|
- Access to the GE Aerospace Gitea or a clone of the repo
|
||||||
|
|
||||||
|
## Step 1: Clone and configure
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://gitea.proudtech.net/ge-aerospace/shopdb-flask.git
|
||||||
|
cd shopdb-flask
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Edit `.env`:
|
||||||
|
|
||||||
|
| Variable | Required | Notes |
|
||||||
|
|----------|----------|-------|
|
||||||
|
| `FLASK_ENV` | Yes | `production` for live sites |
|
||||||
|
| `SECRET_KEY` | Yes | `python -c "import secrets; print(secrets.token_urlsafe(64))"` |
|
||||||
|
| `JWT_SECRET_KEY` | Yes | Same generation, different value |
|
||||||
|
| `DATABASE_URL` | Yes | `mysql+pymysql://shopdb:PASSWORD@db:3306/shopdb_flask` (matches docker-compose) |
|
||||||
|
| `CORS_ORIGINS` | Yes | Comma-separated explicit origins. Wildcard rejected. |
|
||||||
|
| `MYSQL_ROOT_PASSWORD` | Yes | Container only |
|
||||||
|
| `MYSQL_PASSWORD` | Yes | Container only, must match `DATABASE_URL` password |
|
||||||
|
| `MYSQL_PORT` | No | Default 3306 |
|
||||||
|
| `API_PORT` | No | Default 5001 |
|
||||||
|
| `LOG_LEVEL` | No | Default INFO |
|
||||||
|
| `ZABBIX_URL`, `ZABBIX_TOKEN` | No | Only if printers plugin uses Zabbix |
|
||||||
|
|
||||||
|
## Step 2: Bring up the stack
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose build
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
The MySQL container initializes its volume on first run. The API container waits for `db` to be healthy via `healthcheck`. Check logs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose logs -f api
|
||||||
|
```
|
||||||
|
|
||||||
|
If `ProductionConfig.validate()` raises, the container exits with the offending env-var named in the log. Fix `.env` and `docker compose up -d` again.
|
||||||
|
|
||||||
|
## Step 3: Initialize the database schema
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec api flask db upgrade
|
||||||
|
```
|
||||||
|
|
||||||
|
This applies the baseline migration (creates all tables) and any subsequent migrations. Re-running is idempotent.
|
||||||
|
|
||||||
|
## Step 4: Seed reference data
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec api flask seed reference-data
|
||||||
|
```
|
||||||
|
|
||||||
|
Creates: default `Vendor`, `Location`, `BusinessUnit`, `OperatingSystem`, `AssetStatus`, `RelationshipType` rows seeded with the platform contract values (`partof`, `controls`, `connectedto`).
|
||||||
|
|
||||||
|
## Step 5: Pick plugins to enable
|
||||||
|
|
||||||
|
The image bundles all six plugins (computers, equipment, network, notifications, printers, usb). Only enabled plugins are loaded.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec api flask plugin list
|
||||||
|
docker compose exec api flask plugin install computers
|
||||||
|
docker compose exec api flask plugin install equipment
|
||||||
|
# ... repeat for each plugin the site tracks
|
||||||
|
```
|
||||||
|
|
||||||
|
To install a sister-site or third-party plugin (per ADR-003), drop its directory into `<repo>/plugins/<name>/` (the docker-compose mounts this read-only into the container) and run `flask plugin install <name>`.
|
||||||
|
|
||||||
|
## Step 6: Create the admin user
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec api flask seed admin --username admin --email admin@facility.example.com
|
||||||
|
# Password is generated and printed once. Store in your password manager.
|
||||||
|
```
|
||||||
|
|
||||||
|
Subsequent users are managed through the UI.
|
||||||
|
|
||||||
|
## Step 7: Front the API with TLS
|
||||||
|
|
||||||
|
The Flask container listens on `5001/tcp` over plain HTTP. Production exposure must go through a reverse proxy that terminates TLS:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name shopdb.facility-a.example.com;
|
||||||
|
|
||||||
|
ssl_certificate /etc/ssl/certs/shopdb.crt;
|
||||||
|
ssl_certificate_key /etc/ssl/private/shopdb.key;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://localhost:5001;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The framework reads `X-Forwarded-For` for audit logging.
|
||||||
|
|
||||||
|
## Step 8: Backups
|
||||||
|
|
||||||
|
Per-site MySQL backups are the site's responsibility. Recommended: nightly `mysqldump` to offsite storage with 14-day retention.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec -T db mysqldump -u root -p"${MYSQL_ROOT_PASSWORD}" shopdb_flask | gzip > backup-$(date +%F).sql.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify a restore quarterly.
|
||||||
|
|
||||||
|
## Step 9: Updates
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git pull origin main
|
||||||
|
docker compose build api
|
||||||
|
docker compose up -d api
|
||||||
|
docker compose exec api flask db upgrade
|
||||||
|
```
|
||||||
|
|
||||||
|
The framework's `__contract_version__` may have moved. Check `docs/adr/` for any new ADRs since the last update. If an ADR introduces a breaking change, the upgrade may require coordinated work; the ADR's "Consequences" section documents it.
|
||||||
|
|
||||||
|
## Common issues
|
||||||
|
|
||||||
|
| Symptom | Cause | Fix |
|
||||||
|
|---------|-------|-----|
|
||||||
|
| `ConfigError: SECRET_KEY is required in production` | `.env` missing or blank | Set `SECRET_KEY` in `.env`, re-up |
|
||||||
|
| `ConfigError: CORS_ORIGINS must be a comma-separated allowlist` | `.env` has `*` | Set explicit origins |
|
||||||
|
| `PluginVersionError: requires core_version X but framework is Y` | Plugin pinned a too-narrow range | Update `manifest.json` `core_version` or pin framework version |
|
||||||
|
| 500s after `flask db upgrade` | Migration ran but app cached old schema | `docker compose restart api` |
|
||||||
|
| Cannot reach API after restart | Reverse proxy not pointing at the container's exposed port | Confirm `API_PORT` and proxy config |
|
||||||
|
|
||||||
|
## Health check
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -X POST -H "Content-Type: application/json" \
|
||||||
|
-d '{}' http://localhost:5001/api/auth/login \
|
||||||
|
| jq .
|
||||||
|
# Expect: {"status": "error", "data": {"error": {"code": "VALIDATION_ERROR", ...}}}
|
||||||
|
```
|
||||||
|
|
||||||
|
If this returns a 500 or no JSON, the container is unhealthy. Check `docker compose logs api`.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [docs/adr/ADR-004-deployment-topology.md](adr/ADR-004-deployment-topology.md) - per-site instances rationale
|
||||||
|
- [docs/adr/ADR-003-plugin-distribution.md](adr/ADR-003-plugin-distribution.md) - bundled vs external plugins
|
||||||
|
- [docs/adr/ADR-006-collector-contract.md](adr/ADR-006-collector-contract.md) - per-plugin collector endpoints
|
||||||
|
- [docs/PLUGIN-QUICKSTART.md](PLUGIN-QUICKSTART.md) - building a custom plugin for your site
|
||||||
|
- [shopdb/config.py](../shopdb/config.py) - all the env-vars in one place
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
This is the canonical reference for the shopdb-flask plugin contract. Plugin authors implement `BasePlugin` and override the hooks they care about. Hooks marked `required` must be implemented; hooks marked `optional` have sensible defaults and can be left alone.
|
This is the canonical reference for the shopdb-flask plugin contract. Plugin authors implement `BasePlugin` and override the hooks they care about. Hooks marked `required` must be implemented; hooks marked `optional` have sensible defaults and can be left alone.
|
||||||
|
|
||||||
The contract is locked in [ADR-001](../migrations/adr/ADR-001-asset-as-platform-contract.md) and versioned per [ADR-002](../migrations/adr/ADR-002-plugin-versioning.md).
|
The contract is locked in [ADR-001](../docs/adr/ADR-001-asset-as-platform-contract.md) and versioned per [ADR-002](../docs/adr/ADR-002-plugin-versioning.md).
|
||||||
|
|
||||||
## Contract version
|
## Contract version
|
||||||
|
|
||||||
@@ -202,7 +202,7 @@ class ComputersPlugin(BasePlugin):
|
|||||||
|
|
||||||
### `get_collector_schema() -> Optional[Dict]`
|
### `get_collector_schema() -> Optional[Dict]`
|
||||||
|
|
||||||
Declares the JSON Schema for an external collector pushing to `/api/collector/<pluginname>`. See [ADR-006](../migrations/adr/ADR-006-collector-contract.md) for the contract.
|
Declares the JSON Schema for an external collector pushing to `/api/collector/<pluginname>`. See [ADR-006](../docs/adr/ADR-006-collector-contract.md) for the contract.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
class ComputersPlugin(BasePlugin):
|
class ComputersPlugin(BasePlugin):
|
||||||
@@ -272,7 +272,7 @@ position = resolve_asset_position(asset)
|
|||||||
# Returns dict: {'mapx': 234, 'mapy': 567, 'positionsource': 'self' | 'related' | 'location' | None}
|
# Returns dict: {'mapx': 234, 'mapy': 567, 'positionsource': 'self' | 'related' | 'location' | None}
|
||||||
```
|
```
|
||||||
|
|
||||||
See [ADR-001](../migrations/adr/ADR-001-asset-as-platform-contract.md) for the position resolution algorithm.
|
See [ADR-001](../docs/adr/ADR-001-asset-as-platform-contract.md) for the position resolution algorithm.
|
||||||
|
|
||||||
## Removed hooks
|
## Removed hooks
|
||||||
|
|
||||||
@@ -286,8 +286,8 @@ The following hooks existed in early drafts and have been removed for v1:
|
|||||||
|
|
||||||
When you change anything documented here, you must:
|
When you change anything documented here, you must:
|
||||||
|
|
||||||
1. Bump `__contract_version__` per [ADR-002](../migrations/adr/ADR-002-plugin-versioning.md): major for removals or signature changes, minor for additive optional hooks, patch for docs.
|
1. Bump `__contract_version__` per [ADR-002](../docs/adr/ADR-002-plugin-versioning.md): major for removals or signature changes, minor for additive optional hooks, patch for docs.
|
||||||
2. Update [ADR-001](../migrations/adr/ADR-001-asset-as-platform-contract.md) if the contract surface itself changed (or supersede with a new ADR).
|
2. Update [ADR-001](../docs/adr/ADR-001-asset-as-platform-contract.md) if the contract surface itself changed (or supersede with a new ADR).
|
||||||
3. Add or update the test in `tests/test_plugin_contract.py` that asserts the new behavior.
|
3. Add or update the test in `tests/test_plugin_contract.py` that asserts the new behavior.
|
||||||
|
|
||||||
The skill `defining-asset-contract` walks through the full checklist.
|
The skill `defining-asset-contract` walks through the full checklist.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
Build a working shopdb-flask plugin in 30 minutes. This walks through generating, customizing, installing, and testing a plugin from scratch.
|
Build a working shopdb-flask plugin in 30 minutes. This walks through generating, customizing, installing, and testing a plugin from scratch.
|
||||||
|
|
||||||
For the full hook reference, see [PLUGIN-HOOKS.md](PLUGIN-HOOKS.md).
|
For the full hook reference, see [PLUGIN-HOOKS.md](PLUGIN-HOOKS.md).
|
||||||
For the architectural decisions behind the contract, see [migrations/adr/](../migrations/adr/).
|
For the architectural decisions behind the contract, see [docs/adr/](../docs/adr/).
|
||||||
|
|
||||||
## Step 1: Generate the skeleton
|
## Step 1: Generate the skeleton
|
||||||
|
|
||||||
@@ -150,9 +150,9 @@ Copy from an existing plugin's view files (e.g., `frontend/src/views/network/`)
|
|||||||
|
|
||||||
- [PLUGIN-HOOKS.md](PLUGIN-HOOKS.md) for the full hook reference
|
- [PLUGIN-HOOKS.md](PLUGIN-HOOKS.md) for the full hook reference
|
||||||
- [CONTRIBUTING.md](../CONTRIBUTING.md) for naming conventions
|
- [CONTRIBUTING.md](../CONTRIBUTING.md) for naming conventions
|
||||||
- [migrations/adr/ADR-001-asset-as-platform-contract.md](../migrations/adr/ADR-001-asset-as-platform-contract.md) for what your plugin can rely on
|
- [docs/adr/ADR-001-asset-as-platform-contract.md](../docs/adr/ADR-001-asset-as-platform-contract.md) for what your plugin can rely on
|
||||||
- [migrations/adr/ADR-006-collector-contract.md](../migrations/adr/ADR-006-collector-contract.md) for accepting external collector input
|
- [docs/adr/ADR-006-collector-contract.md](../docs/adr/ADR-006-collector-contract.md) for accepting external collector input
|
||||||
|
|
||||||
## Distribution
|
## Distribution
|
||||||
|
|
||||||
If you are building a plugin for a specific GE Aerospace site (sister-site adoption), ship it as its own git repo. The site running shopdb-flask clones or symlinks your plugin into `<repo>/plugins/<name>/`. See [ADR-003](../migrations/adr/ADR-003-plugin-distribution.md).
|
If you are building a plugin for a specific GE Aerospace site (sister-site adoption), ship it as its own git repo. The site running shopdb-flask clones or symlinks your plugin into `<repo>/plugins/<name>/`. See [ADR-003](../docs/adr/ADR-003-plugin-distribution.md).
|
||||||
|
|||||||
1
migrations/README
Normal file
1
migrations/README
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Single-database configuration for Flask.
|
||||||
50
migrations/alembic.ini
Normal file
50
migrations/alembic.ini
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# A generic, single database configuration.
|
||||||
|
|
||||||
|
[alembic]
|
||||||
|
# template used to generate migration files
|
||||||
|
# file_template = %%(rev)s_%%(slug)s
|
||||||
|
|
||||||
|
# set to 'true' to run the environment during
|
||||||
|
# the 'revision' command, regardless of autogenerate
|
||||||
|
# revision_environment = false
|
||||||
|
|
||||||
|
|
||||||
|
# Logging configuration
|
||||||
|
[loggers]
|
||||||
|
keys = root,sqlalchemy,alembic,flask_migrate
|
||||||
|
|
||||||
|
[handlers]
|
||||||
|
keys = console
|
||||||
|
|
||||||
|
[formatters]
|
||||||
|
keys = generic
|
||||||
|
|
||||||
|
[logger_root]
|
||||||
|
level = WARN
|
||||||
|
handlers = console
|
||||||
|
qualname =
|
||||||
|
|
||||||
|
[logger_sqlalchemy]
|
||||||
|
level = WARN
|
||||||
|
handlers =
|
||||||
|
qualname = sqlalchemy.engine
|
||||||
|
|
||||||
|
[logger_alembic]
|
||||||
|
level = INFO
|
||||||
|
handlers =
|
||||||
|
qualname = alembic
|
||||||
|
|
||||||
|
[logger_flask_migrate]
|
||||||
|
level = INFO
|
||||||
|
handlers =
|
||||||
|
qualname = flask_migrate
|
||||||
|
|
||||||
|
[handler_console]
|
||||||
|
class = StreamHandler
|
||||||
|
args = (sys.stderr,)
|
||||||
|
level = NOTSET
|
||||||
|
formatter = generic
|
||||||
|
|
||||||
|
[formatter_generic]
|
||||||
|
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||||
|
datefmt = %H:%M:%S
|
||||||
113
migrations/env.py
Normal file
113
migrations/env.py
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import logging
|
||||||
|
from logging.config import fileConfig
|
||||||
|
|
||||||
|
from flask import current_app
|
||||||
|
|
||||||
|
from alembic import context
|
||||||
|
|
||||||
|
# this is the Alembic Config object, which provides
|
||||||
|
# access to the values within the .ini file in use.
|
||||||
|
config = context.config
|
||||||
|
|
||||||
|
# Interpret the config file for Python logging.
|
||||||
|
# This line sets up loggers basically.
|
||||||
|
fileConfig(config.config_file_name)
|
||||||
|
logger = logging.getLogger('alembic.env')
|
||||||
|
|
||||||
|
|
||||||
|
def get_engine():
|
||||||
|
try:
|
||||||
|
# this works with Flask-SQLAlchemy<3 and Alchemical
|
||||||
|
return current_app.extensions['migrate'].db.get_engine()
|
||||||
|
except (TypeError, AttributeError):
|
||||||
|
# this works with Flask-SQLAlchemy>=3
|
||||||
|
return current_app.extensions['migrate'].db.engine
|
||||||
|
|
||||||
|
|
||||||
|
def get_engine_url():
|
||||||
|
try:
|
||||||
|
return get_engine().url.render_as_string(hide_password=False).replace(
|
||||||
|
'%', '%%')
|
||||||
|
except AttributeError:
|
||||||
|
return str(get_engine().url).replace('%', '%%')
|
||||||
|
|
||||||
|
|
||||||
|
# add your model's MetaData object here
|
||||||
|
# for 'autogenerate' support
|
||||||
|
# from myapp import mymodel
|
||||||
|
# target_metadata = mymodel.Base.metadata
|
||||||
|
config.set_main_option('sqlalchemy.url', get_engine_url())
|
||||||
|
target_db = current_app.extensions['migrate'].db
|
||||||
|
|
||||||
|
# other values from the config, defined by the needs of env.py,
|
||||||
|
# can be acquired:
|
||||||
|
# my_important_option = config.get_main_option("my_important_option")
|
||||||
|
# ... etc.
|
||||||
|
|
||||||
|
|
||||||
|
def get_metadata():
|
||||||
|
if hasattr(target_db, 'metadatas'):
|
||||||
|
return target_db.metadatas[None]
|
||||||
|
return target_db.metadata
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_offline():
|
||||||
|
"""Run migrations in 'offline' mode.
|
||||||
|
|
||||||
|
This configures the context with just a URL
|
||||||
|
and not an Engine, though an Engine is acceptable
|
||||||
|
here as well. By skipping the Engine creation
|
||||||
|
we don't even need a DBAPI to be available.
|
||||||
|
|
||||||
|
Calls to context.execute() here emit the given string to the
|
||||||
|
script output.
|
||||||
|
|
||||||
|
"""
|
||||||
|
url = config.get_main_option("sqlalchemy.url")
|
||||||
|
context.configure(
|
||||||
|
url=url, target_metadata=get_metadata(), literal_binds=True
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_online():
|
||||||
|
"""Run migrations in 'online' mode.
|
||||||
|
|
||||||
|
In this scenario we need to create an Engine
|
||||||
|
and associate a connection with the context.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# this callback is used to prevent an auto-migration from being generated
|
||||||
|
# when there are no changes to the schema
|
||||||
|
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
|
||||||
|
def process_revision_directives(context, revision, directives):
|
||||||
|
if getattr(config.cmd_opts, 'autogenerate', False):
|
||||||
|
script = directives[0]
|
||||||
|
if script.upgrade_ops.is_empty():
|
||||||
|
directives[:] = []
|
||||||
|
logger.info('No changes in schema detected.')
|
||||||
|
|
||||||
|
conf_args = current_app.extensions['migrate'].configure_args
|
||||||
|
if conf_args.get("process_revision_directives") is None:
|
||||||
|
conf_args["process_revision_directives"] = process_revision_directives
|
||||||
|
|
||||||
|
connectable = get_engine()
|
||||||
|
|
||||||
|
with connectable.connect() as connection:
|
||||||
|
context.configure(
|
||||||
|
connection=connection,
|
||||||
|
target_metadata=get_metadata(),
|
||||||
|
**conf_args
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
if context.is_offline_mode():
|
||||||
|
run_migrations_offline()
|
||||||
|
else:
|
||||||
|
run_migrations_online()
|
||||||
24
migrations/script.py.mako
Normal file
24
migrations/script.py.mako
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"""${message}
|
||||||
|
|
||||||
|
Revision ID: ${up_revision}
|
||||||
|
Revises: ${down_revision | comma,n}
|
||||||
|
Create Date: ${create_date}
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
${imports if imports else ""}
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = ${repr(up_revision)}
|
||||||
|
down_revision = ${repr(down_revision)}
|
||||||
|
branch_labels = ${repr(branch_labels)}
|
||||||
|
depends_on = ${repr(depends_on)}
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
${upgrades if upgrades else "pass"}
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
${downgrades if downgrades else "pass"}
|
||||||
956
migrations/versions/68b3947ae14f_baseline_schema.py
Normal file
956
migrations/versions/68b3947ae14f_baseline_schema.py
Normal file
@@ -0,0 +1,956 @@
|
|||||||
|
"""baseline schema
|
||||||
|
|
||||||
|
Revision ID: 68b3947ae14f
|
||||||
|
Revises:
|
||||||
|
Create Date: 2026-05-08 17:53:08.342776
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '68b3947ae14f'
|
||||||
|
down_revision = None
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('appowners',
|
||||||
|
sa.Column('appownerid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('appowner', sa.String(length=100), nullable=False),
|
||||||
|
sa.Column('sso', sa.String(length=50), nullable=True),
|
||||||
|
sa.Column('email', sa.String(length=100), nullable=True),
|
||||||
|
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('appownerid')
|
||||||
|
)
|
||||||
|
op.create_table('assetstatuses',
|
||||||
|
sa.Column('statusid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('status', sa.String(length=50), nullable=False),
|
||||||
|
sa.Column('description', sa.Text(), nullable=True),
|
||||||
|
sa.Column('color', sa.String(length=20), nullable=True, comment='CSS color for UI'),
|
||||||
|
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('statusid'),
|
||||||
|
sa.UniqueConstraint('status')
|
||||||
|
)
|
||||||
|
op.create_table('assettypes',
|
||||||
|
sa.Column('assettypeid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('assettype', sa.String(length=50), nullable=False, comment='Category name: equipment, computer, network_device, printer'),
|
||||||
|
sa.Column('pluginname', sa.String(length=100), nullable=True, comment='Plugin that owns this type'),
|
||||||
|
sa.Column('tablename', sa.String(length=100), nullable=True, comment='Extension table name for this type'),
|
||||||
|
sa.Column('description', sa.Text(), nullable=True),
|
||||||
|
sa.Column('icon', sa.String(length=50), nullable=True, comment='Icon name for UI'),
|
||||||
|
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('assettypeid'),
|
||||||
|
sa.UniqueConstraint('assettype')
|
||||||
|
)
|
||||||
|
op.create_table('businessunits',
|
||||||
|
sa.Column('businessunitid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('businessunit', sa.String(length=100), nullable=False),
|
||||||
|
sa.Column('code', sa.String(length=20), nullable=True, comment='Short code'),
|
||||||
|
sa.Column('description', sa.Text(), nullable=True),
|
||||||
|
sa.Column('parentid', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['parentid'], ['businessunits.businessunitid'], ),
|
||||||
|
sa.PrimaryKeyConstraint('businessunitid'),
|
||||||
|
sa.UniqueConstraint('businessunit'),
|
||||||
|
sa.UniqueConstraint('code')
|
||||||
|
)
|
||||||
|
op.create_table('communicationtypes',
|
||||||
|
sa.Column('comtypeid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('comtype', sa.String(length=50), nullable=False),
|
||||||
|
sa.Column('description', sa.Text(), nullable=True),
|
||||||
|
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('comtypeid'),
|
||||||
|
sa.UniqueConstraint('comtype')
|
||||||
|
)
|
||||||
|
op.create_table('computertypes',
|
||||||
|
sa.Column('computertypeid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('computertype', sa.String(length=100), nullable=False),
|
||||||
|
sa.Column('description', sa.Text(), nullable=True),
|
||||||
|
sa.Column('icon', sa.String(length=50), nullable=True, comment='Icon name for UI'),
|
||||||
|
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('computertypeid'),
|
||||||
|
sa.UniqueConstraint('computertype')
|
||||||
|
)
|
||||||
|
op.create_table('equipmenttypes',
|
||||||
|
sa.Column('equipmenttypeid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('equipmenttype', sa.String(length=100), nullable=False),
|
||||||
|
sa.Column('description', sa.Text(), nullable=True),
|
||||||
|
sa.Column('icon', sa.String(length=50), nullable=True, comment='Icon name for UI'),
|
||||||
|
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('equipmenttypeid'),
|
||||||
|
sa.UniqueConstraint('equipmenttype')
|
||||||
|
)
|
||||||
|
op.create_table('locations',
|
||||||
|
sa.Column('locationid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('locationname', sa.String(length=100), nullable=False),
|
||||||
|
sa.Column('building', sa.String(length=100), nullable=True),
|
||||||
|
sa.Column('floor', sa.String(length=50), nullable=True),
|
||||||
|
sa.Column('room', sa.String(length=50), nullable=True),
|
||||||
|
sa.Column('description', sa.Text(), nullable=True),
|
||||||
|
sa.Column('mapimage', sa.String(length=500), nullable=True, comment='Path to floor map image'),
|
||||||
|
sa.Column('mapwidth', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('mapheight', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('locationid'),
|
||||||
|
sa.UniqueConstraint('locationname')
|
||||||
|
)
|
||||||
|
op.create_table('machinestatuses',
|
||||||
|
sa.Column('statusid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('status', sa.String(length=50), nullable=False),
|
||||||
|
sa.Column('description', sa.Text(), nullable=True),
|
||||||
|
sa.Column('color', sa.String(length=20), nullable=True, comment='CSS color for UI'),
|
||||||
|
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('statusid'),
|
||||||
|
sa.UniqueConstraint('status')
|
||||||
|
)
|
||||||
|
op.create_table('machinetypes',
|
||||||
|
sa.Column('machinetypeid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('machinetype', sa.String(length=100), nullable=False),
|
||||||
|
sa.Column('category', sa.String(length=50), nullable=False, comment='Equipment, PC, Network, or Printer'),
|
||||||
|
sa.Column('description', sa.Text(), nullable=True),
|
||||||
|
sa.Column('icon', sa.String(length=50), nullable=True, comment='Icon name for UI'),
|
||||||
|
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('machinetypeid'),
|
||||||
|
sa.UniqueConstraint('machinetype')
|
||||||
|
)
|
||||||
|
op.create_table('networkdevicetypes',
|
||||||
|
sa.Column('networkdevicetypeid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('networkdevicetype', sa.String(length=100), nullable=False),
|
||||||
|
sa.Column('description', sa.Text(), nullable=True),
|
||||||
|
sa.Column('icon', sa.String(length=50), nullable=True, comment='Icon name for UI'),
|
||||||
|
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('networkdevicetypeid'),
|
||||||
|
sa.UniqueConstraint('networkdevicetype')
|
||||||
|
)
|
||||||
|
op.create_table('notificationtypes',
|
||||||
|
sa.Column('notificationtypeid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('typename', sa.String(length=50), nullable=False),
|
||||||
|
sa.Column('typedescription', sa.Text(), nullable=True),
|
||||||
|
sa.Column('typecolor', sa.String(length=20), nullable=True),
|
||||||
|
sa.Column('isactive', sa.Boolean(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('notificationtypeid')
|
||||||
|
)
|
||||||
|
op.create_table('operatingsystems',
|
||||||
|
sa.Column('osid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('osname', sa.String(length=100), nullable=False),
|
||||||
|
sa.Column('osversion', sa.String(length=50), nullable=True),
|
||||||
|
sa.Column('architecture', sa.String(length=20), nullable=True, comment='x86, x64, ARM'),
|
||||||
|
sa.Column('endoflife', sa.Date(), nullable=True, comment='End of support date'),
|
||||||
|
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('osid'),
|
||||||
|
sa.UniqueConstraint('osname', 'osversion', name='uq_os_name_version')
|
||||||
|
)
|
||||||
|
op.create_table('pctypes',
|
||||||
|
sa.Column('pctypeid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('pctype', sa.String(length=100), nullable=False),
|
||||||
|
sa.Column('description', sa.Text(), nullable=True),
|
||||||
|
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('pctypeid'),
|
||||||
|
sa.UniqueConstraint('pctype')
|
||||||
|
)
|
||||||
|
op.create_table('permissions',
|
||||||
|
sa.Column('permissionid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=50), nullable=False),
|
||||||
|
sa.Column('description', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('category', sa.String(length=50), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('permissionid'),
|
||||||
|
sa.UniqueConstraint('name')
|
||||||
|
)
|
||||||
|
op.create_table('printertypes',
|
||||||
|
sa.Column('printertypeid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('printertype', sa.String(length=100), nullable=False),
|
||||||
|
sa.Column('description', sa.Text(), nullable=True),
|
||||||
|
sa.Column('icon', sa.String(length=50), nullable=True, comment='Icon name for UI'),
|
||||||
|
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('printertypeid'),
|
||||||
|
sa.UniqueConstraint('printertype')
|
||||||
|
)
|
||||||
|
op.create_table('relationshiptypes',
|
||||||
|
sa.Column('relationshiptypeid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('relationshiptype', sa.String(length=50), nullable=False),
|
||||||
|
sa.Column('description', sa.Text(), nullable=True),
|
||||||
|
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('relationshiptypeid'),
|
||||||
|
sa.UniqueConstraint('relationshiptype')
|
||||||
|
)
|
||||||
|
op.create_table('roles',
|
||||||
|
sa.Column('roleid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('rolename', sa.String(length=50), nullable=False),
|
||||||
|
sa.Column('description', sa.Text(), nullable=True),
|
||||||
|
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('roleid'),
|
||||||
|
sa.UniqueConstraint('rolename')
|
||||||
|
)
|
||||||
|
op.create_table('settings',
|
||||||
|
sa.Column('settingid', sa.Integer(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('key', sa.String(length=100), nullable=False),
|
||||||
|
sa.Column('value', sa.Text(), nullable=True),
|
||||||
|
sa.Column('valuetype', sa.String(length=20), nullable=True),
|
||||||
|
sa.Column('category', sa.String(length=50), nullable=True),
|
||||||
|
sa.Column('description', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('createddate', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('modifieddate', sa.DateTime(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('settingid')
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('settings', schema=None) as batch_op:
|
||||||
|
batch_op.create_index(batch_op.f('ix_settings_key'), ['key'], unique=True)
|
||||||
|
|
||||||
|
op.create_table('usbdevicetypes',
|
||||||
|
sa.Column('usbdevicetypeid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('typename', sa.String(length=50), nullable=False),
|
||||||
|
sa.Column('description', sa.Text(), nullable=True),
|
||||||
|
sa.Column('icon', sa.String(length=50), nullable=True, comment='Icon name for UI'),
|
||||||
|
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('usbdevicetypeid'),
|
||||||
|
sa.UniqueConstraint('typename')
|
||||||
|
)
|
||||||
|
op.create_table('users',
|
||||||
|
sa.Column('userid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('username', sa.String(length=100), nullable=False),
|
||||||
|
sa.Column('email', sa.String(length=255), nullable=False),
|
||||||
|
sa.Column('passwordhash', sa.String(length=255), nullable=False),
|
||||||
|
sa.Column('firstname', sa.String(length=100), nullable=True),
|
||||||
|
sa.Column('lastname', sa.String(length=100), nullable=True),
|
||||||
|
sa.Column('lastlogindate', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('failedlogins', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('lockeduntil', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('userid'),
|
||||||
|
sa.UniqueConstraint('email')
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('users', schema=None) as batch_op:
|
||||||
|
batch_op.create_index(batch_op.f('ix_users_username'), ['username'], unique=True)
|
||||||
|
|
||||||
|
op.create_table('vendors',
|
||||||
|
sa.Column('vendorid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('vendor', sa.String(length=100), nullable=False),
|
||||||
|
sa.Column('description', sa.Text(), nullable=True),
|
||||||
|
sa.Column('website', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('supportphone', sa.String(length=50), nullable=True),
|
||||||
|
sa.Column('supportemail', sa.String(length=100), nullable=True),
|
||||||
|
sa.Column('notes', sa.Text(), nullable=True),
|
||||||
|
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('vendorid'),
|
||||||
|
sa.UniqueConstraint('vendor')
|
||||||
|
)
|
||||||
|
op.create_table('vlans',
|
||||||
|
sa.Column('vlanid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('vlannumber', sa.Integer(), nullable=False, comment='VLAN ID number'),
|
||||||
|
sa.Column('name', sa.String(length=100), nullable=False, comment='VLAN name'),
|
||||||
|
sa.Column('description', sa.Text(), nullable=True),
|
||||||
|
sa.Column('vlantype', sa.String(length=50), nullable=True, comment='Type: data, voice, management, guest, etc.'),
|
||||||
|
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('vlanid'),
|
||||||
|
sa.UniqueConstraint('vlannumber')
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('vlans', schema=None) as batch_op:
|
||||||
|
batch_op.create_index('idx_vlan_number', ['vlannumber'], unique=False)
|
||||||
|
|
||||||
|
op.create_table('assets',
|
||||||
|
sa.Column('assetid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('assetnumber', sa.String(length=50), nullable=False, comment='Business identifier (e.g., CMM01, G5QX1GT3ESF)'),
|
||||||
|
sa.Column('name', sa.String(length=100), nullable=True, comment='Display name/alias'),
|
||||||
|
sa.Column('serialnumber', sa.String(length=100), nullable=True, comment='Hardware serial number'),
|
||||||
|
sa.Column('assettypeid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('statusid', sa.Integer(), nullable=True, comment='In Use, Spare, Retired, etc.'),
|
||||||
|
sa.Column('locationid', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('businessunitid', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('mapleft', sa.Integer(), nullable=True, comment='X coordinate on floor map'),
|
||||||
|
sa.Column('maptop', sa.Integer(), nullable=True, comment='Y coordinate on floor map'),
|
||||||
|
sa.Column('notes', sa.Text(), nullable=True),
|
||||||
|
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||||
|
sa.Column('deleteddate', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('deletedby', sa.String(length=100), nullable=True),
|
||||||
|
sa.Column('createdby', sa.String(length=100), nullable=True),
|
||||||
|
sa.Column('modifiedby', sa.String(length=100), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['assettypeid'], ['assettypes.assettypeid'], ),
|
||||||
|
sa.ForeignKeyConstraint(['businessunitid'], ['businessunits.businessunitid'], ),
|
||||||
|
sa.ForeignKeyConstraint(['locationid'], ['locations.locationid'], ),
|
||||||
|
sa.ForeignKeyConstraint(['statusid'], ['assetstatuses.statusid'], ),
|
||||||
|
sa.PrimaryKeyConstraint('assetid')
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('assets', schema=None) as batch_op:
|
||||||
|
batch_op.create_index('idx_asset_active', ['isactive'], unique=False)
|
||||||
|
batch_op.create_index('idx_asset_location', ['locationid'], unique=False)
|
||||||
|
batch_op.create_index('idx_asset_status', ['statusid'], unique=False)
|
||||||
|
batch_op.create_index('idx_asset_type_bu', ['assettypeid', 'businessunitid'], unique=False)
|
||||||
|
batch_op.create_index(batch_op.f('ix_assets_assetnumber'), ['assetnumber'], unique=True)
|
||||||
|
batch_op.create_index(batch_op.f('ix_assets_serialnumber'), ['serialnumber'], unique=False)
|
||||||
|
|
||||||
|
op.create_table('auditlogs',
|
||||||
|
sa.Column('auditlogid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('userid', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('username', sa.String(length=100), nullable=True),
|
||||||
|
sa.Column('timestamp', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('ipaddress', sa.String(length=45), nullable=True),
|
||||||
|
sa.Column('useragent', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('action', sa.String(length=20), nullable=False),
|
||||||
|
sa.Column('entitytype', sa.String(length=50), nullable=False),
|
||||||
|
sa.Column('entityid', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('entityname', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('changes', sa.Text(), nullable=True),
|
||||||
|
sa.Column('details', sa.Text(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['userid'], ['users.userid'], ),
|
||||||
|
sa.PrimaryKeyConstraint('auditlogid')
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('auditlogs', schema=None) as batch_op:
|
||||||
|
batch_op.create_index(batch_op.f('ix_auditlogs_action'), ['action'], unique=False)
|
||||||
|
batch_op.create_index(batch_op.f('ix_auditlogs_entitytype'), ['entitytype'], unique=False)
|
||||||
|
batch_op.create_index(batch_op.f('ix_auditlogs_timestamp'), ['timestamp'], unique=False)
|
||||||
|
|
||||||
|
op.create_table('models',
|
||||||
|
sa.Column('modelnumberid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('modelnumber', sa.String(length=100), nullable=False),
|
||||||
|
sa.Column('machinetypeid', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('vendorid', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('description', sa.Text(), nullable=True),
|
||||||
|
sa.Column('imageurl', sa.String(length=500), nullable=True, comment='URL to product image'),
|
||||||
|
sa.Column('documentationurl', sa.String(length=500), nullable=True, comment='URL to documentation'),
|
||||||
|
sa.Column('notes', sa.Text(), nullable=True),
|
||||||
|
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['machinetypeid'], ['machinetypes.machinetypeid'], ),
|
||||||
|
sa.ForeignKeyConstraint(['vendorid'], ['vendors.vendorid'], ),
|
||||||
|
sa.PrimaryKeyConstraint('modelnumberid'),
|
||||||
|
sa.UniqueConstraint('modelnumber', 'vendorid', name='uq_model_vendor')
|
||||||
|
)
|
||||||
|
op.create_table('notifications',
|
||||||
|
sa.Column('notificationid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('notificationtypeid', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('businessunitid', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('appid', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('notification', sa.Text(), nullable=False, comment='The message content'),
|
||||||
|
sa.Column('starttime', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('endtime', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('ticketnumber', sa.String(length=50), nullable=True),
|
||||||
|
sa.Column('link', sa.String(length=500), nullable=True),
|
||||||
|
sa.Column('isactive', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('isshopfloor', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('employeesso', sa.String(length=100), nullable=True),
|
||||||
|
sa.Column('employeename', sa.String(length=100), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['notificationtypeid'], ['notificationtypes.notificationtypeid'], ),
|
||||||
|
sa.PrimaryKeyConstraint('notificationid')
|
||||||
|
)
|
||||||
|
op.create_table('rolepermissions',
|
||||||
|
sa.Column('roleid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('permissionid', sa.Integer(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['permissionid'], ['permissions.permissionid'], ),
|
||||||
|
sa.ForeignKeyConstraint(['roleid'], ['roles.roleid'], ),
|
||||||
|
sa.PrimaryKeyConstraint('roleid', 'permissionid')
|
||||||
|
)
|
||||||
|
op.create_table('subnets',
|
||||||
|
sa.Column('subnetid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('cidr', sa.String(length=18), nullable=False, comment='CIDR notation (e.g., 10.1.1.0/24)'),
|
||||||
|
sa.Column('name', sa.String(length=100), nullable=False, comment='Subnet name'),
|
||||||
|
sa.Column('description', sa.Text(), nullable=True),
|
||||||
|
sa.Column('gatewayip', sa.String(length=15), nullable=True, comment='Default gateway IP address'),
|
||||||
|
sa.Column('subnetmask', sa.String(length=15), nullable=True, comment='Subnet mask (e.g., 255.255.255.0)'),
|
||||||
|
sa.Column('networkaddress', sa.String(length=15), nullable=True, comment='Network address (e.g., 10.1.1.0)'),
|
||||||
|
sa.Column('broadcastaddress', sa.String(length=15), nullable=True, comment='Broadcast address (e.g., 10.1.1.255)'),
|
||||||
|
sa.Column('vlanid', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('subnettype', sa.String(length=50), nullable=True, comment='Type: production, development, management, dmz, etc.'),
|
||||||
|
sa.Column('locationid', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('dhcpenabled', sa.Boolean(), nullable=True, comment='DHCP enabled for this subnet'),
|
||||||
|
sa.Column('dhcprangestart', sa.String(length=15), nullable=True, comment='DHCP range start IP'),
|
||||||
|
sa.Column('dhcprangeend', sa.String(length=15), nullable=True, comment='DHCP range end IP'),
|
||||||
|
sa.Column('dns1', sa.String(length=15), nullable=True, comment='Primary DNS server'),
|
||||||
|
sa.Column('dns2', sa.String(length=15), nullable=True, comment='Secondary DNS server'),
|
||||||
|
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['locationid'], ['locations.locationid'], ),
|
||||||
|
sa.ForeignKeyConstraint(['vlanid'], ['vlans.vlanid'], ),
|
||||||
|
sa.PrimaryKeyConstraint('subnetid'),
|
||||||
|
sa.UniqueConstraint('cidr')
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('subnets', schema=None) as batch_op:
|
||||||
|
batch_op.create_index('idx_subnet_cidr', ['cidr'], unique=False)
|
||||||
|
batch_op.create_index('idx_subnet_location', ['locationid'], unique=False)
|
||||||
|
batch_op.create_index('idx_subnet_vlan', ['vlanid'], unique=False)
|
||||||
|
|
||||||
|
op.create_table('supportteams',
|
||||||
|
sa.Column('supportteamid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('teamname', sa.String(length=100), nullable=False),
|
||||||
|
sa.Column('teamurl', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('appownerid', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['appownerid'], ['appowners.appownerid'], ),
|
||||||
|
sa.PrimaryKeyConstraint('supportteamid')
|
||||||
|
)
|
||||||
|
op.create_table('usbdevices',
|
||||||
|
sa.Column('usbdeviceid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('serialnumber', sa.String(length=100), nullable=False),
|
||||||
|
sa.Column('label', sa.String(length=100), nullable=True, comment='Human-readable label'),
|
||||||
|
sa.Column('assetnumber', sa.String(length=50), nullable=True, comment='Optional asset tag'),
|
||||||
|
sa.Column('usbdevicetypeid', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('capacitygb', sa.Integer(), nullable=True, comment='Capacity in GB'),
|
||||||
|
sa.Column('vendorid', sa.String(length=10), nullable=True, comment='USB Vendor ID (hex)'),
|
||||||
|
sa.Column('productid', sa.String(length=10), nullable=True, comment='USB Product ID (hex)'),
|
||||||
|
sa.Column('manufacturer', sa.String(length=100), nullable=True),
|
||||||
|
sa.Column('productname', sa.String(length=100), nullable=True),
|
||||||
|
sa.Column('ischeckedout', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('currentuserid', sa.String(length=50), nullable=True, comment='SSO of current user'),
|
||||||
|
sa.Column('currentusername', sa.String(length=100), nullable=True, comment='Name of current user'),
|
||||||
|
sa.Column('currentcheckoutdate', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('storagelocation', sa.String(length=200), nullable=True, comment='Where device is stored when not checked out'),
|
||||||
|
sa.Column('pin', sa.String(length=50), nullable=True, comment='PIN for encrypted devices'),
|
||||||
|
sa.Column('notes', sa.Text(), nullable=True),
|
||||||
|
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||||
|
sa.Column('createdby', sa.String(length=100), nullable=True),
|
||||||
|
sa.Column('modifiedby', sa.String(length=100), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['usbdevicetypeid'], ['usbdevicetypes.usbdevicetypeid'], ),
|
||||||
|
sa.PrimaryKeyConstraint('usbdeviceid'),
|
||||||
|
sa.UniqueConstraint('serialnumber')
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('usbdevices', schema=None) as batch_op:
|
||||||
|
batch_op.create_index('idx_usb_checkedout', ['ischeckedout'], unique=False)
|
||||||
|
batch_op.create_index('idx_usb_currentuser', ['currentuserid'], unique=False)
|
||||||
|
batch_op.create_index('idx_usb_serial', ['serialnumber'], unique=False)
|
||||||
|
batch_op.create_index('idx_usb_type', ['usbdevicetypeid'], unique=False)
|
||||||
|
|
||||||
|
op.create_table('userroles',
|
||||||
|
sa.Column('userid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('roleid', sa.Integer(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['roleid'], ['roles.roleid'], ),
|
||||||
|
sa.ForeignKeyConstraint(['userid'], ['users.userid'], ),
|
||||||
|
sa.PrimaryKeyConstraint('userid', 'roleid')
|
||||||
|
)
|
||||||
|
op.create_table('applications',
|
||||||
|
sa.Column('appid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('appname', sa.String(length=100), nullable=False),
|
||||||
|
sa.Column('appdescription', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('supportteamid', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('isinstallable', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('applicationnotes', sa.Text(), nullable=True),
|
||||||
|
sa.Column('installpath', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('applicationlink', sa.String(length=512), nullable=True),
|
||||||
|
sa.Column('documentationpath', sa.String(length=512), nullable=True),
|
||||||
|
sa.Column('ishidden', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('isprinter', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('islicenced', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('image', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['supportteamid'], ['supportteams.supportteamid'], ),
|
||||||
|
sa.PrimaryKeyConstraint('appid'),
|
||||||
|
sa.UniqueConstraint('appname')
|
||||||
|
)
|
||||||
|
op.create_table('assetrelationships',
|
||||||
|
sa.Column('relationshipid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('sourceassetid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('targetassetid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('relationshiptypeid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('notes', sa.Text(), nullable=True),
|
||||||
|
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['relationshiptypeid'], ['relationshiptypes.relationshiptypeid'], ),
|
||||||
|
sa.ForeignKeyConstraint(['sourceassetid'], ['assets.assetid'], ),
|
||||||
|
sa.ForeignKeyConstraint(['targetassetid'], ['assets.assetid'], ),
|
||||||
|
sa.PrimaryKeyConstraint('relationshipid'),
|
||||||
|
sa.UniqueConstraint('sourceassetid', 'targetassetid', 'relationshiptypeid', name='uq_asset_relationship')
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('assetrelationships', schema=None) as batch_op:
|
||||||
|
batch_op.create_index('idx_asset_rel_source', ['sourceassetid'], unique=False)
|
||||||
|
batch_op.create_index('idx_asset_rel_target', ['targetassetid'], unique=False)
|
||||||
|
|
||||||
|
op.create_table('computers',
|
||||||
|
sa.Column('computerid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('assetid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('computertypeid', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('hostname', sa.String(length=100), nullable=True, comment='Network hostname'),
|
||||||
|
sa.Column('osid', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('loggedinuser', sa.String(length=100), nullable=True),
|
||||||
|
sa.Column('lastreporteddate', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('lastboottime', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('isvnc', sa.Boolean(), nullable=True, comment='VNC remote access enabled'),
|
||||||
|
sa.Column('iswinrm', sa.Boolean(), nullable=True, comment='WinRM enabled'),
|
||||||
|
sa.Column('isshopfloor', sa.Boolean(), nullable=True, comment='Shopfloor PC (vs office PC)'),
|
||||||
|
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['assetid'], ['assets.assetid'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['computertypeid'], ['computertypes.computertypeid'], ),
|
||||||
|
sa.ForeignKeyConstraint(['osid'], ['operatingsystems.osid'], ),
|
||||||
|
sa.PrimaryKeyConstraint('computerid')
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('computers', schema=None) as batch_op:
|
||||||
|
batch_op.create_index('idx_computer_hostname', ['hostname'], unique=False)
|
||||||
|
batch_op.create_index('idx_computer_os', ['osid'], unique=False)
|
||||||
|
batch_op.create_index('idx_computer_type', ['computertypeid'], unique=False)
|
||||||
|
batch_op.create_index(batch_op.f('ix_computers_assetid'), ['assetid'], unique=True)
|
||||||
|
batch_op.create_index(batch_op.f('ix_computers_hostname'), ['hostname'], unique=False)
|
||||||
|
|
||||||
|
op.create_table('equipment',
|
||||||
|
sa.Column('equipmentid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('assetid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('equipmenttypeid', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('vendorid', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('modelnumberid', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('requiresmanualconfig', sa.Boolean(), nullable=True, comment='Multi-PC machine needs manual configuration'),
|
||||||
|
sa.Column('islocationonly', sa.Boolean(), nullable=True, comment='Virtual location marker (not actual equipment)'),
|
||||||
|
sa.Column('lastmaintenancedate', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('nextmaintenancedate', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('maintenanceintervaldays', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('controllervendorid', sa.Integer(), nullable=True, comment='Controller vendor (e.g., FANUC)'),
|
||||||
|
sa.Column('controllermodelid', sa.Integer(), nullable=True, comment='Controller model (e.g., 31B)'),
|
||||||
|
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['assetid'], ['assets.assetid'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['controllermodelid'], ['models.modelnumberid'], ),
|
||||||
|
sa.ForeignKeyConstraint(['controllervendorid'], ['vendors.vendorid'], ),
|
||||||
|
sa.ForeignKeyConstraint(['equipmenttypeid'], ['equipmenttypes.equipmenttypeid'], ),
|
||||||
|
sa.ForeignKeyConstraint(['modelnumberid'], ['models.modelnumberid'], ),
|
||||||
|
sa.ForeignKeyConstraint(['vendorid'], ['vendors.vendorid'], ),
|
||||||
|
sa.PrimaryKeyConstraint('equipmentid')
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('equipment', schema=None) as batch_op:
|
||||||
|
batch_op.create_index('idx_equipment_type', ['equipmenttypeid'], unique=False)
|
||||||
|
batch_op.create_index('idx_equipment_vendor', ['vendorid'], unique=False)
|
||||||
|
batch_op.create_index(batch_op.f('ix_equipment_assetid'), ['assetid'], unique=True)
|
||||||
|
|
||||||
|
op.create_table('machines',
|
||||||
|
sa.Column('machineid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('machinenumber', sa.String(length=50), nullable=False, comment='Business identifier (e.g., CMM01, G5QX1GT3ESF)'),
|
||||||
|
sa.Column('alias', sa.String(length=100), nullable=True, comment='Friendly name'),
|
||||||
|
sa.Column('hostname', sa.String(length=100), nullable=True, comment='Network hostname (for PCs)'),
|
||||||
|
sa.Column('serialnumber', sa.String(length=100), nullable=True, comment='Hardware serial number'),
|
||||||
|
sa.Column('machinetypeid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('pctypeid', sa.Integer(), nullable=True, comment='Set for PCs, NULL for equipment'),
|
||||||
|
sa.Column('businessunitid', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('modelnumberid', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('vendorid', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('statusid', sa.Integer(), nullable=True, comment='In Use, Spare, Retired, etc.'),
|
||||||
|
sa.Column('locationid', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('mapleft', sa.Integer(), nullable=True, comment='X coordinate on floor map'),
|
||||||
|
sa.Column('maptop', sa.Integer(), nullable=True, comment='Y coordinate on floor map'),
|
||||||
|
sa.Column('islocationonly', sa.Boolean(), nullable=True, comment='Virtual location marker (not actual machine)'),
|
||||||
|
sa.Column('osid', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('loggedinuser', sa.String(length=100), nullable=True),
|
||||||
|
sa.Column('lastreporteddate', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('lastboottime', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('isvnc', sa.Boolean(), nullable=True, comment='VNC remote access enabled'),
|
||||||
|
sa.Column('iswinrm', sa.Boolean(), nullable=True, comment='WinRM enabled'),
|
||||||
|
sa.Column('isshopfloor', sa.Boolean(), nullable=True, comment='Shopfloor PC'),
|
||||||
|
sa.Column('requiresmanualconfig', sa.Boolean(), nullable=True, comment='Multi-PC machine needs manual configuration'),
|
||||||
|
sa.Column('notes', sa.Text(), nullable=True),
|
||||||
|
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||||
|
sa.Column('deleteddate', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('deletedby', sa.String(length=100), nullable=True),
|
||||||
|
sa.Column('createdby', sa.String(length=100), nullable=True),
|
||||||
|
sa.Column('modifiedby', sa.String(length=100), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['businessunitid'], ['businessunits.businessunitid'], ),
|
||||||
|
sa.ForeignKeyConstraint(['locationid'], ['locations.locationid'], ),
|
||||||
|
sa.ForeignKeyConstraint(['machinetypeid'], ['machinetypes.machinetypeid'], ),
|
||||||
|
sa.ForeignKeyConstraint(['modelnumberid'], ['models.modelnumberid'], ),
|
||||||
|
sa.ForeignKeyConstraint(['osid'], ['operatingsystems.osid'], ),
|
||||||
|
sa.ForeignKeyConstraint(['pctypeid'], ['pctypes.pctypeid'], ),
|
||||||
|
sa.ForeignKeyConstraint(['statusid'], ['machinestatuses.statusid'], ),
|
||||||
|
sa.ForeignKeyConstraint(['vendorid'], ['vendors.vendorid'], ),
|
||||||
|
sa.PrimaryKeyConstraint('machineid')
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('machines', schema=None) as batch_op:
|
||||||
|
batch_op.create_index('idx_machine_active', ['isactive'], unique=False)
|
||||||
|
batch_op.create_index('idx_machine_hostname', ['hostname'], unique=False)
|
||||||
|
batch_op.create_index('idx_machine_location', ['locationid'], unique=False)
|
||||||
|
batch_op.create_index('idx_machine_type_bu', ['machinetypeid', 'businessunitid'], unique=False)
|
||||||
|
batch_op.create_index(batch_op.f('ix_machines_hostname'), ['hostname'], unique=False)
|
||||||
|
batch_op.create_index(batch_op.f('ix_machines_machinenumber'), ['machinenumber'], unique=True)
|
||||||
|
batch_op.create_index(batch_op.f('ix_machines_serialnumber'), ['serialnumber'], unique=False)
|
||||||
|
|
||||||
|
op.create_table('networkdevices',
|
||||||
|
sa.Column('networkdeviceid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('assetid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('networkdevicetypeid', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('vendorid', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('hostname', sa.String(length=100), nullable=True, comment='Network hostname'),
|
||||||
|
sa.Column('firmwareversion', sa.String(length=100), nullable=True),
|
||||||
|
sa.Column('portcount', sa.Integer(), nullable=True, comment='Number of ports (for switches)'),
|
||||||
|
sa.Column('ispoe', sa.Boolean(), nullable=True, comment='Power over Ethernet capable'),
|
||||||
|
sa.Column('ismanaged', sa.Boolean(), nullable=True, comment='Managed device (SNMP, web interface, etc.)'),
|
||||||
|
sa.Column('rackunit', sa.String(length=20), nullable=True, comment='Rack unit position (e.g., U1, U5)'),
|
||||||
|
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['assetid'], ['assets.assetid'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['networkdevicetypeid'], ['networkdevicetypes.networkdevicetypeid'], ),
|
||||||
|
sa.ForeignKeyConstraint(['vendorid'], ['vendors.vendorid'], ),
|
||||||
|
sa.PrimaryKeyConstraint('networkdeviceid')
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('networkdevices', schema=None) as batch_op:
|
||||||
|
batch_op.create_index('idx_netdev_hostname', ['hostname'], unique=False)
|
||||||
|
batch_op.create_index('idx_netdev_type', ['networkdevicetypeid'], unique=False)
|
||||||
|
batch_op.create_index('idx_netdev_vendor', ['vendorid'], unique=False)
|
||||||
|
batch_op.create_index(batch_op.f('ix_networkdevices_assetid'), ['assetid'], unique=True)
|
||||||
|
batch_op.create_index(batch_op.f('ix_networkdevices_hostname'), ['hostname'], unique=False)
|
||||||
|
|
||||||
|
op.create_table('printers',
|
||||||
|
sa.Column('printerid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('assetid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('printertypeid', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('vendorid', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('modelnumberid', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('hostname', sa.String(length=100), nullable=True, comment='Network hostname'),
|
||||||
|
sa.Column('windowsname', sa.String(length=255), nullable=True, comment='Windows printer name (e.g., \\\\server\\printer)'),
|
||||||
|
sa.Column('sharename', sa.String(length=100), nullable=True, comment='CSF/share name'),
|
||||||
|
sa.Column('iscsf', sa.Boolean(), nullable=True, comment='Is CSF printer'),
|
||||||
|
sa.Column('installpath', sa.String(length=255), nullable=True, comment='Driver install path'),
|
||||||
|
sa.Column('pin', sa.String(length=20), nullable=True),
|
||||||
|
sa.Column('iscolor', sa.Boolean(), nullable=True, comment='Color capable'),
|
||||||
|
sa.Column('isduplex', sa.Boolean(), nullable=True, comment='Duplex capable'),
|
||||||
|
sa.Column('isnetwork', sa.Boolean(), nullable=True, comment='Network connected'),
|
||||||
|
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['assetid'], ['assets.assetid'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['modelnumberid'], ['models.modelnumberid'], ),
|
||||||
|
sa.ForeignKeyConstraint(['printertypeid'], ['printertypes.printertypeid'], ),
|
||||||
|
sa.ForeignKeyConstraint(['vendorid'], ['vendors.vendorid'], ),
|
||||||
|
sa.PrimaryKeyConstraint('printerid')
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('printers', schema=None) as batch_op:
|
||||||
|
batch_op.create_index('idx_printer_hostname', ['hostname'], unique=False)
|
||||||
|
batch_op.create_index('idx_printer_type', ['printertypeid'], unique=False)
|
||||||
|
batch_op.create_index('idx_printer_windowsname', ['windowsname'], unique=False)
|
||||||
|
batch_op.create_index(batch_op.f('ix_printers_assetid'), ['assetid'], unique=True)
|
||||||
|
batch_op.create_index(batch_op.f('ix_printers_hostname'), ['hostname'], unique=False)
|
||||||
|
|
||||||
|
op.create_table('usbcheckouts',
|
||||||
|
sa.Column('checkoutid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('usbdeviceid', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('machineid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('sso', sa.String(length=20), nullable=False, comment='SSO of user'),
|
||||||
|
sa.Column('checkoutname', sa.String(length=100), nullable=True, comment='Name of user'),
|
||||||
|
sa.Column('checkouttime', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('checkintime', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('checkoutreason', sa.Text(), nullable=True, comment='Reason for checkout'),
|
||||||
|
sa.Column('checkinnotes', sa.Text(), nullable=True),
|
||||||
|
sa.Column('waswiped', sa.Boolean(), nullable=True, comment='Was device wiped after return'),
|
||||||
|
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['usbdeviceid'], ['usbdevices.usbdeviceid'], ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('checkoutid')
|
||||||
|
)
|
||||||
|
op.create_table('appversions',
|
||||||
|
sa.Column('appversionid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('appid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('version', sa.String(length=50), nullable=False),
|
||||||
|
sa.Column('releasedate', sa.Date(), nullable=True),
|
||||||
|
sa.Column('notes', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('dateadded', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('isactive', sa.Boolean(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['appid'], ['applications.appid'], ),
|
||||||
|
sa.PrimaryKeyConstraint('appversionid'),
|
||||||
|
sa.UniqueConstraint('appid', 'version', name='uq_app_version')
|
||||||
|
)
|
||||||
|
op.create_table('communications',
|
||||||
|
sa.Column('communicationid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('assetid', sa.Integer(), nullable=True, comment='FK to assets table (new architecture)'),
|
||||||
|
sa.Column('machineid', sa.Integer(), nullable=True, comment='DEPRECATED: FK to machines table - use assetid instead'),
|
||||||
|
sa.Column('comtypeid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('ipaddress', sa.String(length=50), nullable=True),
|
||||||
|
sa.Column('subnetmask', sa.String(length=50), nullable=True),
|
||||||
|
sa.Column('gateway', sa.String(length=50), nullable=True),
|
||||||
|
sa.Column('dns1', sa.String(length=50), nullable=True),
|
||||||
|
sa.Column('dns2', sa.String(length=50), nullable=True),
|
||||||
|
sa.Column('macaddress', sa.String(length=50), nullable=True),
|
||||||
|
sa.Column('isdhcp', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('comport', sa.String(length=20), nullable=True),
|
||||||
|
sa.Column('baudrate', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('databits', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('stopbits', sa.String(length=10), nullable=True),
|
||||||
|
sa.Column('parity', sa.String(length=20), nullable=True),
|
||||||
|
sa.Column('flowcontrol', sa.String(length=20), nullable=True),
|
||||||
|
sa.Column('port', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('username', sa.String(length=100), nullable=True),
|
||||||
|
sa.Column('pathname', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('pathname2', sa.String(length=255), nullable=True, comment='Secondary path for dualpath'),
|
||||||
|
sa.Column('isprimary', sa.Boolean(), nullable=True, comment='Primary communication method'),
|
||||||
|
sa.Column('ismachinenetwork', sa.Boolean(), nullable=True, comment='On machine network vs office network'),
|
||||||
|
sa.Column('notes', sa.Text(), nullable=True),
|
||||||
|
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['assetid'], ['assets.assetid'], ),
|
||||||
|
sa.ForeignKeyConstraint(['comtypeid'], ['communicationtypes.comtypeid'], ),
|
||||||
|
sa.ForeignKeyConstraint(['machineid'], ['machines.machineid'], ),
|
||||||
|
sa.PrimaryKeyConstraint('communicationid')
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('communications', schema=None) as batch_op:
|
||||||
|
batch_op.create_index('idx_comm_asset', ['assetid'], unique=False)
|
||||||
|
batch_op.create_index('idx_comm_ip', ['ipaddress'], unique=False)
|
||||||
|
batch_op.create_index('idx_comm_machine', ['machineid'], unique=False)
|
||||||
|
batch_op.create_index(batch_op.f('ix_communications_assetid'), ['assetid'], unique=False)
|
||||||
|
|
||||||
|
op.create_table('knowledgebase',
|
||||||
|
sa.Column('linkid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('appid', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('shortdescription', sa.String(length=500), nullable=False),
|
||||||
|
sa.Column('linkurl', sa.String(length=2000), nullable=True),
|
||||||
|
sa.Column('keywords', sa.String(length=500), nullable=True),
|
||||||
|
sa.Column('clicks', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('lastupdated', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['appid'], ['applications.appid'], ),
|
||||||
|
sa.PrimaryKeyConstraint('linkid')
|
||||||
|
)
|
||||||
|
op.create_table('machinerelationships',
|
||||||
|
sa.Column('relationshipid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('parentmachineid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('childmachineid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('relationshiptypeid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('notes', sa.Text(), nullable=True),
|
||||||
|
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['childmachineid'], ['machines.machineid'], ),
|
||||||
|
sa.ForeignKeyConstraint(['parentmachineid'], ['machines.machineid'], ),
|
||||||
|
sa.ForeignKeyConstraint(['relationshiptypeid'], ['relationshiptypes.relationshiptypeid'], ),
|
||||||
|
sa.PrimaryKeyConstraint('relationshipid'),
|
||||||
|
sa.UniqueConstraint('parentmachineid', 'childmachineid', 'relationshiptypeid', name='uq_machine_relationship')
|
||||||
|
)
|
||||||
|
op.create_table('printerdata',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('machineid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('windowsname', sa.String(length=255), nullable=True, comment='Windows printer name (e.g., \\\\server\\printer)'),
|
||||||
|
sa.Column('sharename', sa.String(length=100), nullable=True, comment='CSF/share name'),
|
||||||
|
sa.Column('iscsf', sa.Boolean(), nullable=True, comment='Is CSF printer'),
|
||||||
|
sa.Column('installpath', sa.String(length=255), nullable=True, comment='Driver install path'),
|
||||||
|
sa.Column('pin', sa.String(length=20), nullable=True),
|
||||||
|
sa.Column('createddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['machineid'], ['machines.machineid'], ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('printerdata', schema=None) as batch_op:
|
||||||
|
batch_op.create_index('idx_printerdata_windowsname', ['windowsname'], unique=False)
|
||||||
|
batch_op.create_index(batch_op.f('ix_printerdata_machineid'), ['machineid'], unique=True)
|
||||||
|
|
||||||
|
op.create_table('computerinstalledapps',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('computerid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('appid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('appversionid', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||||
|
sa.Column('installeddate', sa.DateTime(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['appid'], ['applications.appid'], ),
|
||||||
|
sa.ForeignKeyConstraint(['appversionid'], ['appversions.appversionid'], ),
|
||||||
|
sa.ForeignKeyConstraint(['computerid'], ['computers.computerid'], ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('computerid', 'appid', name='uq_computer_app')
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('computerinstalledapps', schema=None) as batch_op:
|
||||||
|
batch_op.create_index('idx_compapp_app', ['appid'], unique=False)
|
||||||
|
batch_op.create_index('idx_compapp_computer', ['computerid'], unique=False)
|
||||||
|
|
||||||
|
op.create_table('installedapps',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('machineid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('appid', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('appversionid', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('isactive', sa.Boolean(), nullable=False),
|
||||||
|
sa.Column('installeddate', sa.DateTime(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['appid'], ['applications.appid'], ),
|
||||||
|
sa.ForeignKeyConstraint(['appversionid'], ['appversions.appversionid'], ),
|
||||||
|
sa.ForeignKeyConstraint(['machineid'], ['machines.machineid'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('machineid', 'appid', name='uq_machine_app')
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('installedapps')
|
||||||
|
with op.batch_alter_table('computerinstalledapps', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index('idx_compapp_computer')
|
||||||
|
batch_op.drop_index('idx_compapp_app')
|
||||||
|
|
||||||
|
op.drop_table('computerinstalledapps')
|
||||||
|
with op.batch_alter_table('printerdata', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_printerdata_machineid'))
|
||||||
|
batch_op.drop_index('idx_printerdata_windowsname')
|
||||||
|
|
||||||
|
op.drop_table('printerdata')
|
||||||
|
op.drop_table('machinerelationships')
|
||||||
|
op.drop_table('knowledgebase')
|
||||||
|
with op.batch_alter_table('communications', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_communications_assetid'))
|
||||||
|
batch_op.drop_index('idx_comm_machine')
|
||||||
|
batch_op.drop_index('idx_comm_ip')
|
||||||
|
batch_op.drop_index('idx_comm_asset')
|
||||||
|
|
||||||
|
op.drop_table('communications')
|
||||||
|
op.drop_table('appversions')
|
||||||
|
op.drop_table('usbcheckouts')
|
||||||
|
with op.batch_alter_table('printers', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_printers_hostname'))
|
||||||
|
batch_op.drop_index(batch_op.f('ix_printers_assetid'))
|
||||||
|
batch_op.drop_index('idx_printer_windowsname')
|
||||||
|
batch_op.drop_index('idx_printer_type')
|
||||||
|
batch_op.drop_index('idx_printer_hostname')
|
||||||
|
|
||||||
|
op.drop_table('printers')
|
||||||
|
with op.batch_alter_table('networkdevices', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_networkdevices_hostname'))
|
||||||
|
batch_op.drop_index(batch_op.f('ix_networkdevices_assetid'))
|
||||||
|
batch_op.drop_index('idx_netdev_vendor')
|
||||||
|
batch_op.drop_index('idx_netdev_type')
|
||||||
|
batch_op.drop_index('idx_netdev_hostname')
|
||||||
|
|
||||||
|
op.drop_table('networkdevices')
|
||||||
|
with op.batch_alter_table('machines', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_machines_serialnumber'))
|
||||||
|
batch_op.drop_index(batch_op.f('ix_machines_machinenumber'))
|
||||||
|
batch_op.drop_index(batch_op.f('ix_machines_hostname'))
|
||||||
|
batch_op.drop_index('idx_machine_type_bu')
|
||||||
|
batch_op.drop_index('idx_machine_location')
|
||||||
|
batch_op.drop_index('idx_machine_hostname')
|
||||||
|
batch_op.drop_index('idx_machine_active')
|
||||||
|
|
||||||
|
op.drop_table('machines')
|
||||||
|
with op.batch_alter_table('equipment', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_equipment_assetid'))
|
||||||
|
batch_op.drop_index('idx_equipment_vendor')
|
||||||
|
batch_op.drop_index('idx_equipment_type')
|
||||||
|
|
||||||
|
op.drop_table('equipment')
|
||||||
|
with op.batch_alter_table('computers', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_computers_hostname'))
|
||||||
|
batch_op.drop_index(batch_op.f('ix_computers_assetid'))
|
||||||
|
batch_op.drop_index('idx_computer_type')
|
||||||
|
batch_op.drop_index('idx_computer_os')
|
||||||
|
batch_op.drop_index('idx_computer_hostname')
|
||||||
|
|
||||||
|
op.drop_table('computers')
|
||||||
|
with op.batch_alter_table('assetrelationships', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index('idx_asset_rel_target')
|
||||||
|
batch_op.drop_index('idx_asset_rel_source')
|
||||||
|
|
||||||
|
op.drop_table('assetrelationships')
|
||||||
|
op.drop_table('applications')
|
||||||
|
op.drop_table('userroles')
|
||||||
|
with op.batch_alter_table('usbdevices', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index('idx_usb_type')
|
||||||
|
batch_op.drop_index('idx_usb_serial')
|
||||||
|
batch_op.drop_index('idx_usb_currentuser')
|
||||||
|
batch_op.drop_index('idx_usb_checkedout')
|
||||||
|
|
||||||
|
op.drop_table('usbdevices')
|
||||||
|
op.drop_table('supportteams')
|
||||||
|
with op.batch_alter_table('subnets', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index('idx_subnet_vlan')
|
||||||
|
batch_op.drop_index('idx_subnet_location')
|
||||||
|
batch_op.drop_index('idx_subnet_cidr')
|
||||||
|
|
||||||
|
op.drop_table('subnets')
|
||||||
|
op.drop_table('rolepermissions')
|
||||||
|
op.drop_table('notifications')
|
||||||
|
op.drop_table('models')
|
||||||
|
with op.batch_alter_table('auditlogs', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_auditlogs_timestamp'))
|
||||||
|
batch_op.drop_index(batch_op.f('ix_auditlogs_entitytype'))
|
||||||
|
batch_op.drop_index(batch_op.f('ix_auditlogs_action'))
|
||||||
|
|
||||||
|
op.drop_table('auditlogs')
|
||||||
|
with op.batch_alter_table('assets', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_assets_serialnumber'))
|
||||||
|
batch_op.drop_index(batch_op.f('ix_assets_assetnumber'))
|
||||||
|
batch_op.drop_index('idx_asset_type_bu')
|
||||||
|
batch_op.drop_index('idx_asset_status')
|
||||||
|
batch_op.drop_index('idx_asset_location')
|
||||||
|
batch_op.drop_index('idx_asset_active')
|
||||||
|
|
||||||
|
op.drop_table('assets')
|
||||||
|
with op.batch_alter_table('vlans', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index('idx_vlan_number')
|
||||||
|
|
||||||
|
op.drop_table('vlans')
|
||||||
|
op.drop_table('vendors')
|
||||||
|
with op.batch_alter_table('users', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_users_username'))
|
||||||
|
|
||||||
|
op.drop_table('users')
|
||||||
|
op.drop_table('usbdevicetypes')
|
||||||
|
with op.batch_alter_table('settings', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_settings_key'))
|
||||||
|
|
||||||
|
op.drop_table('settings')
|
||||||
|
op.drop_table('roles')
|
||||||
|
op.drop_table('relationshiptypes')
|
||||||
|
op.drop_table('printertypes')
|
||||||
|
op.drop_table('permissions')
|
||||||
|
op.drop_table('pctypes')
|
||||||
|
op.drop_table('operatingsystems')
|
||||||
|
op.drop_table('notificationtypes')
|
||||||
|
op.drop_table('networkdevicetypes')
|
||||||
|
op.drop_table('machinetypes')
|
||||||
|
op.drop_table('machinestatuses')
|
||||||
|
op.drop_table('locations')
|
||||||
|
op.drop_table('equipmenttypes')
|
||||||
|
op.drop_table('computertypes')
|
||||||
|
op.drop_table('communicationtypes')
|
||||||
|
op.drop_table('businessunits')
|
||||||
|
op.drop_table('assettypes')
|
||||||
|
op.drop_table('assetstatuses')
|
||||||
|
op.drop_table('appowners')
|
||||||
|
# ### end Alembic commands ###
|
||||||
Reference in New Issue
Block a user