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:
1
migrations/README
Normal file
1
migrations/README
Normal file
@@ -0,0 +1 @@
|
||||
Single-database configuration for Flask.
|
||||
@@ -1,173 +0,0 @@
|
||||
# ADR-001: Asset model is the platform contract
|
||||
|
||||
- **Status:** ACCEPTED
|
||||
- **Date:** 2026-05-08
|
||||
- **Deciders:** cproudlock
|
||||
- **Supersedes:** none
|
||||
|
||||
## Context
|
||||
|
||||
shopdb-flask is being shaped as a framework that sister GE Aerospace facilities can adopt. The framework defines a stable core; sites install plugins for the asset classes they care about (equipment, computers, printers, measuring tools, network gear, etc.).
|
||||
|
||||
The codebase ran two parallel object models:
|
||||
|
||||
1. **Legacy `Machine` model** in `shopdb/core/models/machine.py`. Original schema inherited from the classic-ASP shopdb. Tables: `machines`, `pctypes`, `machinetypes`. Plugins like printers stored extension data via `PrinterData` keyed by `machineid`.
|
||||
2. **New `Asset` model** in `shopdb/core/models/asset.py`. Generic asset abstraction with `AssetType`, `AssetStatus`, `AssetRelationship`. Plugins also exposed asset-based tables keyed by `assetid`.
|
||||
|
||||
For the framework to be adoptable, the platform contract has to be one model, documented, versioned, and stable.
|
||||
|
||||
## Decision
|
||||
|
||||
**`Asset` is the platform contract.** Plugin authors target the asset-based API. The `Machine` model and its dependents (`PCType`, `MachineType`, `PrinterData` keyed by `machineid`) are legacy and will be retired through a tracked migration.
|
||||
|
||||
### Platform contract surface
|
||||
|
||||
The following are the public, versioned surface. Plugin authors may depend on them. Breaking changes require a major version bump per ADR-002.
|
||||
|
||||
#### Models
|
||||
|
||||
- `Asset` (core entity)
|
||||
- `AssetType` (asset classification, registered by plugins)
|
||||
- `AssetStatus` (active / inactive / decommissioned / retired)
|
||||
- `AssetRelationship` + `RelationshipType` (cross-plugin links)
|
||||
- `Vendor`, `Location`, `LocationType`, `BusinessUnit`, `Model`, `OperatingSystem` (shared reference data)
|
||||
|
||||
#### Helpers
|
||||
|
||||
- `AuditLog` API: `audit_log(action, entitytype, entityid, ...)` for plugins to record audit entries with consistent schema
|
||||
- `Setting` API: `plugin.get_setting(key)` and `plugin.set_setting(key, value)` for plugin-scoped config persisted via the core `Setting` model
|
||||
|
||||
#### Plugin contract
|
||||
|
||||
- `BasePlugin` ABC and its hooks (search, navigation, dashboard, relationships, collector schema)
|
||||
|
||||
#### Excluded from the contract for v1
|
||||
|
||||
- Event bus (`get_event_handlers` removed from `BasePlugin`). Add later if a real use case appears.
|
||||
|
||||
### Relationship types (seeded core values)
|
||||
|
||||
`RelationshipType` is seeded with three rows. Plugin authors do not add new types in v1.
|
||||
|
||||
| Type | Meaning | Boundary rule |
|
||||
|------|---------|---------------|
|
||||
| `partof` | Composition, siblings, sub-assemblies | Parent dies without it |
|
||||
| `controls` | One asset has operational authority over another | PC commands a machine, measuring tool, etc. |
|
||||
| `connectedto` | Network or data link without operational authority | Cable, switch port, NAS mount |
|
||||
|
||||
### AssetRelationship columns
|
||||
|
||||
```
|
||||
AssetRelationship
|
||||
- relationshipid PK
|
||||
- sourceassetid FK to assets
|
||||
- targetassetid FK to assets
|
||||
- relationshiptypeid FK to relationshiptypes (one of partof, controls, connectedto)
|
||||
- label text, free description (e.g., "ethernet PoE", "DNC feed")
|
||||
- inheritsposition bool, true means resolved-position walk follows this edge
|
||||
- propagatesthroughid FK to relationshiptypes, nullable, see propagation below
|
||||
- notes text
|
||||
- isactive bool
|
||||
- createddate, modifieddate
|
||||
```
|
||||
|
||||
Free-text `label` carries the domain nuance (`controls` with label "DNC feed" vs `controls` with label "operator workstation"). Avoids inflating the type list.
|
||||
|
||||
### Sibling propagation
|
||||
|
||||
When a relationship is created or deleted, the framework checks `RelationshipType.propagatesthroughid`. If set, it finds all assets related to the source via the propagation type and applies the same change.
|
||||
|
||||
Default seeding:
|
||||
|
||||
| RelationshipType | propagatesthroughid |
|
||||
|------------------|---------------------|
|
||||
| `partof` | null (the propagation rail itself) |
|
||||
| `controls` | `partof` (controls relationships propagate across siblings) |
|
||||
| `connectedto` | null (network paths don't propagate) |
|
||||
|
||||
Cycle protection: max walk depth 3, visited-set during traversal.
|
||||
|
||||
### Position resolution
|
||||
|
||||
Resolved map position for any asset follows this priority chain:
|
||||
|
||||
1. Asset-specific override (`assets.mapx`, `assets.mapy` if non-null)
|
||||
2. Walk relationships where `inheritsposition=true`, ordered by relationship type priority (`partof` first, then `controls`), recursively resolve the related asset's position with cycle detection
|
||||
3. Fall back to the asset's location coords (`locations.mapx`, `locations.mapy` via `assets.locationid`)
|
||||
4. If none of the above, asset is "unplaced" and rendered in a tray, not on the map
|
||||
|
||||
API exposes resolved position with a source indicator: `{"mapx": 234, "mapy": 567, "positionsource": "self|related|location|none"}`.
|
||||
|
||||
### Hierarchical locations
|
||||
|
||||
`locations` is extended with `parentlocationid` (self-FK, nullable). Sites define their own location tree (cells, sub-cells, network closets, meeting rooms, labs).
|
||||
|
||||
`locationtypes` lookup is added with seeded values: `section`, `cell`, `subcell`, `meetingroom`, `lab`, `office`, `storage`, `hallway`, `networkcloset`, `building`. Sites can extend.
|
||||
|
||||
Asset-to-location is the existing `assets.locationid` FK only (single primary location for v1). Multi-location and transient placement are deferred.
|
||||
|
||||
## Migration scope (narrowed)
|
||||
|
||||
Only one class of legacy data migrates: physical manufacturing equipment with a `machinenumber` (5-axis mills, lathes, broachers, heat treatment, CMMs, etc.).
|
||||
|
||||
Migration filter:
|
||||
|
||||
```sql
|
||||
SELECT * FROM legacy.machines
|
||||
WHERE category = 'Equipment'
|
||||
AND machinenumber IS NOT NULL
|
||||
```
|
||||
|
||||
Rows without `machinenumber`: **skipped.** Add manually post-migration if any matter.
|
||||
|
||||
Migrated rows become `assets` with `assettype='Equipment'` plus a row in the equipment plugin's `equipment` table. Legacy `machinetypeid` is preserved on the equipment row to enable later reclassification (some "Equipment" rows are actually measuring tools, see ADR-005).
|
||||
|
||||
Skipped from migration:
|
||||
|
||||
- PCs (rebuilt via PXE collector pipeline, see ADR-006)
|
||||
- Printers, USB devices, network gear, notifications, KB articles (operational or re-collectable, decision per class deferred until needed)
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- Plugin authors at sister sites have a single, documented contract to target
|
||||
- Cross-plugin features (relationships, search, map) work uniformly
|
||||
- Three relationship types are easy to learn; free-text label captures nuance
|
||||
- Sibling propagation handles dual-path machines without code changes per use case
|
||||
- Position resolution gives users one map without per-plugin map code
|
||||
|
||||
### Negative
|
||||
|
||||
- Migration of equipment data must run cleanly; `migrating-asset-schema` skill owns the procedure
|
||||
- Frontend `views/machines/` will be repointed or replaced; effort tracked in Phase 5
|
||||
- Legacy `core/api/machines.py` (641 LOC) becomes deprecated; deletion or shim deferred until equipment is migrated
|
||||
|
||||
### Neutral
|
||||
|
||||
- `Machine`, `PCType`, `MachineType`, `PrinterData` remain in the schema until the migration completes, behind a deprecation notice. New code does not touch them.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
1. **Keep both models indefinitely.** Doubles test surface, confuses contract docs, breaks cross-plugin features. Rejected.
|
||||
2. **Make `Machine` the contract; retire `Asset`.** `Machine` has shop-floor-specific assumptions that don't generalize. Rejected.
|
||||
3. **Define a higher abstraction above both.** Yet another layer. `Asset` is already the abstraction. Rejected.
|
||||
4. **More relationship types** (`controls`, `operates`, `monitors`, `dncfeeds`, `mountedon`, `connectedvia`, `inspects`, `siblingbay`). Eight types proved unwieldy. Collapsed to three with free-text labels and per-row inheritance/propagation flags.
|
||||
|
||||
## Open questions deferred
|
||||
|
||||
- Lifecycle relationship type (`replaces`, `replacedby`). Add when first needed.
|
||||
- Multi-location asset placement, transient placement (calibration trip, off-site repair). Add when first needed.
|
||||
- Map editing UI (drag locations onto floor plan). Phase 6 polish.
|
||||
- Plugin-extensible relationship types beyond the three seeded. Add when a real cross-plugin case can't be expressed via labels.
|
||||
|
||||
## References
|
||||
|
||||
- `shopdb/core/models/asset.py`
|
||||
- `shopdb/core/models/machine.py` (legacy, deprecated)
|
||||
- `shopdb/plugins/base.py`
|
||||
- ADR-002 (versioning of the surface)
|
||||
- ADR-003 (plugin distribution)
|
||||
- ADR-004 (deployment topology)
|
||||
- ADR-005 (equipment vs measuring tools split)
|
||||
- ADR-006 (collector contract pattern)
|
||||
@@ -1,60 +0,0 @@
|
||||
# ADR-002: Plugin contract versioning
|
||||
|
||||
- **Status:** ACCEPTED
|
||||
- **Date:** 2026-05-08
|
||||
- **Deciders:** cproudlock
|
||||
- **Supersedes:** none
|
||||
|
||||
## Context
|
||||
|
||||
Once sister sites start writing their own plugins (or pulling community plugins), the framework's plugin contract becomes a public API. Without a versioning story, any change to `BasePlugin` or the core platform models can silently break installed plugins at remote sites.
|
||||
|
||||
The existing `BasePlugin` and `PluginMeta` already declare a `core_version` field (default `">=1.0.0"`), but it is not enforced anywhere. The plugin loader does not check it before instantiation.
|
||||
|
||||
## Decision
|
||||
|
||||
The framework adopts **semantic versioning for the plugin contract**, declared in two places:
|
||||
|
||||
1. **Framework version** (`shopdb/__init__.py`): a single `__contract_version__` constant. This is the version of the platform contract as defined in ADR-001. Bumped according to semver:
|
||||
- **Major**: breaking change to `BasePlugin` ABC, `PluginMeta` schema, or any model in the platform contract (`Asset`, `AssetType`, `AssetStatus`, `AssetRelationship`, `Vendor`, `Location`, `BusinessUnit`, `Model`, `OperatingSystem`).
|
||||
- **Minor**: additive change (new optional hook, new field on a contract model with default).
|
||||
- **Patch**: bug fix, no contract surface change.
|
||||
2. **Plugin requirement** (`plugins/<name>/manifest.json`): the existing `core_version` field, expressed as a semver range (e.g., `">=1.0.0,<2.0.0"`).
|
||||
|
||||
The plugin loader (`shopdb/plugins/loader.py`) checks `core_version` against `__contract_version__` at load time. Mismatch in dev = re-raise (fail loud). Mismatch in prod = log error, mark plugin as incompatible, exclude from registration.
|
||||
|
||||
The `__contract_version__` starts at **`1.0.0`** when ADR-001 is accepted and the `Machine` retirement migration is complete (whichever comes later). Until then, the framework is pre-1.0; plugins should declare `core_version: ">=0.1.0,<1.0.0"`.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- Sister sites can pin a known-good framework version. They will not be silently broken when the framework is upgraded.
|
||||
- Plugin authors know what counts as a breaking change because the contract surface is enumerated in ADR-001.
|
||||
- The loader fails predictably: a mismatched plugin is reported, not silently disabled.
|
||||
|
||||
### Negative / cost
|
||||
|
||||
- Discipline required: every change to the contract surface must be classified (major / minor / patch). Adding a `version-bump` skill (or a check in code review) reduces the chance of mis-classification.
|
||||
- `__contract_version__` becomes a coupling point. Forgetting to bump it after a breaking change means downstream plugins crash silently at runtime instead of failing at install.
|
||||
|
||||
### Neutral
|
||||
|
||||
- Existing plugins (`plugins/printers/`, etc.) ship as part of the framework, so their `core_version` is always the current `__contract_version__`. The discipline matters mostly for external / sister-site plugins.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
1. **No versioning, just trust.** Works for an in-tree-only world. Fails the moment a sister site ships its own plugin. Rejected.
|
||||
2. **Calendar versioning** (e.g., `2026.05.0`). Easier to bump, harder to communicate breaking changes. Rejected; semver is the industry standard for library-like contracts.
|
||||
3. **Per-hook versioning.** Each hook has its own version. Too granular; plugins still couple to multiple hooks. Rejected.
|
||||
|
||||
## Open questions
|
||||
|
||||
- When does the framework declare `1.0.0`? Tied to ADR-001 (Asset retirement of Machine) and the framework being deemed "ready for sister sites". Best-effort target: end of Phase 5 in the refactor plan.
|
||||
- Should `core_version` accept commercial-grade ranges (`^1.0.0`) or stick to PEP 440 / npm-style ranges? Recommend pip-style (`>=,<`) to match Python ecosystem.
|
||||
|
||||
## References
|
||||
|
||||
- `shopdb/plugins/base.py` (PluginMeta declaration)
|
||||
- `shopdb/plugins/loader.py` (where the version check belongs)
|
||||
- ADR-001 (defines what is in the contract)
|
||||
@@ -1,68 +0,0 @@
|
||||
# ADR-003: Plugin distribution model
|
||||
|
||||
- **Status:** ACCEPTED
|
||||
- **Date:** 2026-05-08
|
||||
- **Deciders:** cproudlock
|
||||
|
||||
## Context
|
||||
|
||||
Sister sites adopting shopdb-flask need a way to:
|
||||
|
||||
1. Install the framework
|
||||
2. Pick which plugins they want
|
||||
3. Build their own plugins for site-specific equipment
|
||||
4. Receive updates to both framework and plugins
|
||||
|
||||
Today, every plugin lives in the `plugins/` directory of the framework repo. There is no separation between framework code and plugin code, no install / uninstall, and no way for a site to develop a plugin without forking the whole repo.
|
||||
|
||||
Three viable distribution models:
|
||||
|
||||
| Model | How a site installs a plugin |
|
||||
|---|---|
|
||||
| **In-tree only** | Fork the framework repo, add plugin under `plugins/`, run their own deploy. No separation. |
|
||||
| **Pip-installable plugins** | Each plugin published as a Python package. Site does `pip install shopdb-printers shopdb-network` etc. Discovery via Python entry points. Framework loads any installed plugin that registers itself. |
|
||||
| **Git-based plugins** | Each plugin lives in its own git repo. Site clones / submodules into `plugins/<name>/`. Loader picks them up from the directory. |
|
||||
|
||||
## Decision
|
||||
|
||||
**PROPOSED:** Use a **hybrid model** with two clearly-labeled paths.
|
||||
|
||||
1. **Bundled plugins**: a small set of plugins ships with the framework, in-tree at `plugins/`. These are the reference implementations and the default install (printers, computers, network, equipment, usb, notifications). A site that wants only what's bundled needs no extra work.
|
||||
2. **External plugins**: sister sites or third parties build plugins in their own git repos. The site running the framework drops the plugin into `plugins/<name>/` (clone, submodule, or symlink) and runs `flask plugin install <name>`. No pip packaging required for v1.
|
||||
|
||||
Pip-installable plugins (Python entry-point discovery) are deferred to v2. The complexity is not justified until at least two sites are running their own plugins.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- v1 is simple: filesystem-based discovery (already implemented in `shopdb/plugins/loader.py`), works for both bundled and external plugins.
|
||||
- Sites can develop plugins without changing the framework repo.
|
||||
- The `plugins/` directory is already the canonical location, so no architectural change is needed.
|
||||
|
||||
### Negative / cost
|
||||
|
||||
- No automatic update path for external plugins. Sites must `git pull` in each plugin directory manually. Acceptable for v1; revisit when plugin count grows.
|
||||
- Multiple plugin authors writing in parallel can collide on namespace (e.g., two plugins both registering an `AssetType` named "Equipment"). Need a naming policy: plugin names and asset-type names should be prefixed with the site or org if not in the bundled set.
|
||||
|
||||
### Neutral
|
||||
|
||||
- The existing in-tree pattern keeps working. This decision just formalizes it and clarifies the path for outside-the-tree plugins.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
1. **Pip-installable from day one.** Cleaner for the long term but adds packaging, entry-point registration, and CI steps. Premature for current scale (one site running, no sister-site plugins yet).
|
||||
2. **In-tree only forever.** Forces every site to fork. Doesn't scale beyond two or three sites.
|
||||
3. **Submodules only.** Forces git-submodule discipline on every adopting site. Submodules are notoriously fiddly. Rejected.
|
||||
|
||||
## Open questions
|
||||
|
||||
- For external plugins, should there be a manifest field (`source_url`) declaring where the plugin can be cloned from, so `flask plugin install` could pull it for the site? Defer; manual clone is fine for v1.
|
||||
- Naming convention for non-bundled plugin directory names: prefix with site? (`gea-wjsf-shipping`)? Adopt if and when we hit a name collision.
|
||||
|
||||
## References
|
||||
|
||||
- `shopdb/plugins/loader.py` (filesystem discovery)
|
||||
- `shopdb/plugins/cli.py` (plugin install / uninstall command)
|
||||
- ADR-001 (defines what plugins target)
|
||||
- ADR-002 (defines plugin version compatibility)
|
||||
@@ -1,69 +0,0 @@
|
||||
# ADR-004: Deployment topology (per-site instances)
|
||||
|
||||
- **Status:** ACCEPTED
|
||||
- **Date:** 2026-05-08
|
||||
- **Deciders:** cproudlock
|
||||
|
||||
## Context
|
||||
|
||||
shopdb-flask manages shop-floor inventory. If multiple GE Aerospace sites adopt it, the deployment can take one of two shapes:
|
||||
|
||||
| Model | How it works |
|
||||
|---|---|
|
||||
| **Per-site instances** | Each site runs its own Flask + MySQL + Vue stack. Each site has its own DB, its own users, its own enabled-plugin list, its own deploy. Sites are isolated. |
|
||||
| **Multi-tenant single instance** | One central Flask + MySQL + Vue stack serves all sites. A `siteid` foreign key on every asset partitions data. Auth distinguishes which site a user belongs to. |
|
||||
|
||||
The codebase today is single-tenant per deployment. There is no `siteid` column, no tenant filter, no cross-site auth model. Plugins can be enabled / disabled but only globally for the running instance.
|
||||
|
||||
## Decision
|
||||
|
||||
**PROPOSED:** **Per-site instances.** Each adopting site runs its own dedicated stack. The framework does not support multi-tenancy.
|
||||
|
||||
Each site:
|
||||
|
||||
- Owns its database (own credentials, own backup policy, own retention)
|
||||
- Picks its own enabled plugins
|
||||
- Configures its own JWT secret, CORS allowlist, Zabbix integration, Active Directory binding
|
||||
- Deploys at its own cadence
|
||||
|
||||
The framework provides:
|
||||
|
||||
- A `Dockerfile` and `docker-compose.yml` template suitable for a single-site deploy
|
||||
- A `.env.example` listing all required environment variables
|
||||
- A `docs/DEPLOY.md` walking through a fresh-site install
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- Simpler code: no tenant filter on every query, no cross-tenant auth, no shared-state partitioning bugs.
|
||||
- Sites are independent. A schema change at one site does not affect another. A plugin crash at one site does not blast radius to other sites.
|
||||
- Clear ownership: each site's IT team owns their own stack and data. Compliance and audit boundaries match operational boundaries.
|
||||
- Aligns with how GE Aerospace sites already operate (independent IT, independent shop floors).
|
||||
|
||||
### Negative / cost
|
||||
|
||||
- No cross-site reporting out of the box. If GE corporate ever wants a fleet-wide view, it has to be built on top (e.g., a roll-up dashboard that queries each site's API). That layer is out of scope for the framework.
|
||||
- Each site administers its own stack. Higher operational overhead than a single central instance, but each site already runs its own infrastructure.
|
||||
- Updates require visiting each site's deploy. Fine for the current adoption model; revisit if dozens of sites adopt.
|
||||
|
||||
### Neutral
|
||||
|
||||
- No `siteid` column needed. The existence of one DB per site is the partition.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
1. **Multi-tenant single instance.** Lower operational overhead at scale, easier cross-site reporting, but adds significant code complexity and risk: every query needs a tenant filter, auth gets complex, schema migrations affect every site at once, and a bug at one site can leak data across sites. Rejected for v1; revisit if and only if more than five sites adopt and operational overhead becomes painful.
|
||||
2. **Hybrid: per-site DB but central app server.** Adds the operational complexity of multi-tenancy without isolating the failure domain (one app crash = all sites down). Rejected.
|
||||
|
||||
## Open questions
|
||||
|
||||
- Should the framework provide an optional **read-only fleet roll-up** mode where a "central" instance can pull aggregate metrics from each site's API? Defer. Out of scope for v1.
|
||||
- Backup strategy per site: framework recommendation, or each site decides? Framework should publish a recommended backup runbook (mysqldump + offsite copy) but not enforce.
|
||||
- Auth federation: each site has its own user table, or sites can share an LDAP / SSO? Recommend documenting the LDAP config knob in `.env.example` so sites can plug in their own auth without code change.
|
||||
|
||||
## References
|
||||
|
||||
- `shopdb/config.py` (currently single-tenant, no `siteid`)
|
||||
- ADR-001 (asset model is per-site, not cross-site)
|
||||
- ADR-003 (plugin distribution per site)
|
||||
@@ -1,149 +0,0 @@
|
||||
# ADR-005: Equipment plugin scope vs measuringtools plugin
|
||||
|
||||
- **Status:** ACCEPTED
|
||||
- **Date:** 2026-05-08
|
||||
- **Deciders:** cproudlock
|
||||
- **Supersedes:** none
|
||||
|
||||
## Context
|
||||
|
||||
ADR-001 narrowed the migration to physical manufacturing equipment with a `machinenumber`. In practice, the legacy `category='Equipment'` rows contain two distinct asset classes:
|
||||
|
||||
1. **Manufacturing machinery** (5-axis mills, lathes, broachers, heat treatment ovens). These produce parts.
|
||||
2. **Metrology and inspection instruments** (CMMs, Keyence vision systems, wax-and-trace surface profilometers, GenSpec instruments). These measure parts.
|
||||
|
||||
Both share `Asset` properties (vendor, model, location, controller). They differ in domain fields (axes vs measurement accuracy, cycle time vs calibration interval, controller protocol vs measurement software).
|
||||
|
||||
Mixing them under one plugin pollutes the schema and confuses cross-plugin queries ("show me all measuring tools" requires an enumeration of measuring-instrument equipmenttype values, which scales badly).
|
||||
|
||||
## Decision
|
||||
|
||||
Two plugins, separate concerns, shared platform contract.
|
||||
|
||||
### `equipment` plugin
|
||||
|
||||
Tracks manufacturing machinery. Bundled, in-tree.
|
||||
|
||||
Schema (per ADR-001 contract):
|
||||
|
||||
```
|
||||
equipment
|
||||
- assetid FK to assets, PK
|
||||
- equipmenttypeid FK to equipmenttypes (5-axis mill, lathe, broacher, heat treat, ...)
|
||||
- vendorid FK to vendors (platform)
|
||||
- modelid FK to models (platform)
|
||||
- controllertypeid FK to controllertypes (equipment plugin)
|
||||
- controllerosid FK to controlleros (equipment plugin)
|
||||
- (other shared fields: spindle count, axes, max workpiece size, ...)
|
||||
|
||||
equipmenttypes (lookup, equipment plugin)
|
||||
- equipmenttypeid, name (5-axis mill, lathe, broacher, heat treat, ...)
|
||||
|
||||
controllertypes (lookup, equipment plugin)
|
||||
- controllertypeid, name (Fanuc 31i, Siemens 840D, Mitsubishi M70, Heidenhain TNC640, ...)
|
||||
- vendorid (FK to vendors)
|
||||
|
||||
controlleros (lookup, equipment plugin - separate from PC OS)
|
||||
- controllerosid, name (FAPT, VxWorks, embedded Windows, Linux RT, ...)
|
||||
|
||||
equipmentfocas (subtype, optional, present only when FOCAS-equipped)
|
||||
- assetid PK, FK to equipment
|
||||
- focasipaddress text
|
||||
- focasport integer
|
||||
- focasversion text
|
||||
- focasmachinenumber text
|
||||
|
||||
equipmentclm (subtype, optional, present only when CLM-equipped)
|
||||
- assetid PK, FK to equipment
|
||||
- (CLM-specific: address, port, station ID - finalize when plugin is built)
|
||||
|
||||
equipmentmtconnect (subtype, optional, present only when MTConnect-equipped)
|
||||
- assetid PK, FK to equipment
|
||||
- mtconnectagenturl text
|
||||
- mtconnectdevicename text
|
||||
```
|
||||
|
||||
The `equipment.protocol` enum field is deliberately **not** included. Presence or absence of a subtype row indicates which protocol applies. Avoids a denormalized field that can drift out of sync.
|
||||
|
||||
### `measuringtools` plugin
|
||||
|
||||
Tracks metrology and inspection instruments. Bundled, in-tree (built in Phase 3-4 of the refactor as the first new plugin built using the framework scaffold).
|
||||
|
||||
Schema (initial draft, refined when plugin is built):
|
||||
|
||||
```
|
||||
measuringtools
|
||||
- assetid FK to assets, PK
|
||||
- measuringtooltypeid FK to measuringtooltypes (CMM, vision system, profilometer, surface tester, ...)
|
||||
- vendorid FK to vendors (platform)
|
||||
- modelid FK to models (platform)
|
||||
- measurementaxes integer (e.g., 3 for a 3-axis CMM)
|
||||
- accuracyspec text (e.g., "+/-0.5um")
|
||||
- calibrationintervaldays integer
|
||||
- lastcalibrationdate date
|
||||
- nextcalibrationdate date (computed)
|
||||
- (other domain fields as needed)
|
||||
|
||||
measuringtooltypes (lookup, measuringtools plugin)
|
||||
- measuringtooltypeid, name (CMM, vision system, surface profilometer, gage block, ...)
|
||||
```
|
||||
|
||||
Future extension: subtype tables for measurement-software integrations (PC-DMIS, Keyence, GenSpec). Same pattern as equipment subtype tables.
|
||||
|
||||
### Subtype-table pattern (general)
|
||||
|
||||
Both plugins use the same pattern for protocol- or software-specific fields:
|
||||
|
||||
- Core plugin table carries shared, common fields
|
||||
- Optional subtype tables (one per protocol or software) hold extension fields
|
||||
- Each subtype table is keyed by `assetid` (PK), one-to-one with the parent
|
||||
- Subtype row exists if and only if the asset uses that protocol or software
|
||||
- Sister sites add new subtype tables for their own integrations without touching core
|
||||
|
||||
## Reclassification of legacy data
|
||||
|
||||
ADR-001's migration moves all legacy `category='Equipment' AND machinenumber IS NOT NULL` rows to `assets` with `assettype='Equipment'` and into the equipment plugin's `equipment` table. This includes both manufacturing machinery and measuring tools.
|
||||
|
||||
After the equipment migration, when the measuringtools plugin is built:
|
||||
|
||||
1. Build a mapping table: legacy `machinetypeid` values that are measuring tools (CMM type, Keyence type, etc.)
|
||||
2. Run a reclassification script:
|
||||
- For each `assets` row where the original `machinetypeid` is in the measuring-tool mapping
|
||||
- Change `assets.assettype` to `'MeasuringTool'`
|
||||
- Move the row from `equipment` to `measuringtools`
|
||||
- Map domain fields where they differ (e.g., legacy `axes` field maps to `measurementaxes`)
|
||||
3. Verify counts pre- and post-reclassification
|
||||
4. Audit log entry per reclassified row
|
||||
|
||||
Reclassification is one-shot, run once, archived. Like the original migration script.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- Manufacturing machinery and measuring tools are first-class plugins, each with appropriate domain fields
|
||||
- Sister sites can install one or both depending on what they track
|
||||
- Subtype-table pattern is the canonical example for protocol-specific data and extends naturally to other plugins
|
||||
- Building `measuringtools` mid-refactor validates the plugin scaffold tooling against a real new plugin
|
||||
|
||||
### Negative
|
||||
|
||||
- Reclassification is a second migration step. Lower risk than the initial migration because it is data-only (no schema change beyond moving rows between two tables that share the same `assetid` link).
|
||||
- Sites that adopt the framework before `measuringtools` ships need to either keep measuring tools in `equipment` (workable but suboptimal) or wait for the plugin
|
||||
|
||||
### Neutral
|
||||
|
||||
- Legacy `machinetypeid` is preserved on the equipment row during migration to enable reclassification
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
1. **Single equipment plugin with sub-typed assets.** Use `equipment.equipmenttypeid` to discriminate manufacturing vs metrology. Rejected: domain fields differ enough that a single table is wide and full of NULLs.
|
||||
2. **Migrate split (build mapping before initial migration).** Cleaner end state but requires the `measuringtools` plugin to exist before the migration runs, which delays Phase 5. Rejected.
|
||||
3. **JSON blob for protocol data instead of subtype tables.** Considered for both plugins. Rejected: weak typing, awkward queries, no schema validation.
|
||||
|
||||
## References
|
||||
|
||||
- ADR-001 (Asset is platform contract)
|
||||
- ADR-002 (versioning of the surface)
|
||||
- `plugins/equipment/` (current placeholder)
|
||||
- `plugins/computers/` (existing example of plugin pattern)
|
||||
@@ -1,131 +0,0 @@
|
||||
# ADR-006: Plugin collector contract pattern
|
||||
|
||||
- **Status:** ACCEPTED
|
||||
- **Date:** 2026-05-08
|
||||
- **Deciders:** cproudlock
|
||||
- **Supersedes:** none
|
||||
|
||||
## Context
|
||||
|
||||
PC inventory data was collected by PowerShell scripts pushing to `/api/collector/pc` (`shopdb/core/api/collector.py`, ~374 LOC). The endpoint is hardcoded for PCs: it accepts a fixed schema and writes to the legacy `Machine` model.
|
||||
|
||||
Per ADR-001, `Machine` is being retired in favor of `Asset`. Per the project shift to PXE-driven imaging, PC inventory is moving to a new collection pipeline (PXE / GE-Enforce / manifest engine produces JSON about each PC). Other asset classes may want similar collector pipelines (printers via Zabbix, network gear via SNMP scan).
|
||||
|
||||
This calls for a generalizable contract: any plugin that wants to accept external collector input declares a JSON schema, and the framework wires the endpoint, auth, and idempotency.
|
||||
|
||||
## Decision
|
||||
|
||||
`BasePlugin` gets one new optional hook:
|
||||
|
||||
```python
|
||||
def get_collector_schema(self) -> Optional[dict]:
|
||||
"""Return JSON Schema describing the collector payload for this plugin.
|
||||
Return None if the plugin does not accept collector input.
|
||||
|
||||
The schema must include:
|
||||
- 'identityfield': name of the field that uniquely identifies an asset
|
||||
across submissions (e.g., 'hostname' for PCs, 'macaddress' for network
|
||||
devices). Used for idempotent upsert.
|
||||
- 'fields': JSON Schema definitions for the rest of the payload.
|
||||
"""
|
||||
return None
|
||||
```
|
||||
|
||||
Plugin loader auto-registers an endpoint at `/api/collector/<pluginname>` for each plugin returning a schema. Auth is API-key, separate from JWT. Per-plugin keys via env vars:
|
||||
|
||||
- `COLLECTOR_API_KEY_<PLUGINNAME>` (preferred, plugin-specific)
|
||||
- `COLLECTOR_API_KEY` (fallback, shared)
|
||||
|
||||
### Idempotent upsert
|
||||
|
||||
The endpoint uses the `identityfield` to find an existing `Asset` for the same identity. Found = update. Not found = insert. Existing relationships are preserved on update.
|
||||
|
||||
### Response contract
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"action": "created" | "updated" | "noop",
|
||||
"assetid": 12345,
|
||||
"identityvalue": "PC-1234",
|
||||
"warnings": []
|
||||
}
|
||||
```
|
||||
|
||||
### Audit logging
|
||||
|
||||
Every collector submission produces an audit log entry: `{action, plugin, identityvalue, before/after diff}`. Audit retention per site policy.
|
||||
|
||||
### Schema discovery
|
||||
|
||||
The framework exposes the registered schemas at `/api/collector/_schemas` (read-only, JWT-protected) so external collector authors can introspect what payloads are accepted by which plugins.
|
||||
|
||||
## Concrete first user: computers plugin
|
||||
|
||||
The `computers` plugin is the first to implement `get_collector_schema`. The PXE pipeline conforms.
|
||||
|
||||
Initial computers collector schema (sketch, finalized when plugin is built):
|
||||
|
||||
```json
|
||||
{
|
||||
"identityfield": "hostname",
|
||||
"fields": {
|
||||
"hostname": "string, required",
|
||||
"macaddress": "string, optional, secondary identity",
|
||||
"osname": "string",
|
||||
"osversion": "string",
|
||||
"lastboottime": "datetime",
|
||||
"currentuser": "string",
|
||||
"ipaddress": "string",
|
||||
"memorygb": "number",
|
||||
"cputype": "string",
|
||||
"imagename": "string (PXE image deployed)",
|
||||
"imageappliedat": "datetime",
|
||||
"installedsoftware": "array of {name, version}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The PC re-image case is handled by the identity field: a freshly imaged PC keeps its hostname, so the existing `Asset` row is updated rather than duplicated. Existing `AssetRelationship` rows pointing at that PC (e.g., `controls` to a machine) are preserved across re-images.
|
||||
|
||||
## Migration of the existing endpoint
|
||||
|
||||
`shopdb/core/api/collector.py` (`/api/collector/pc`) is **deprecated** in v1 and **removed** before v1.0.
|
||||
|
||||
Migration path:
|
||||
|
||||
1. Implement `get_collector_schema` on the `computers` plugin. New endpoint `/api/collector/computers` is auto-registered.
|
||||
2. Run both endpoints in parallel for one cycle of PXE imaging across the floor. PXE pipeline switches to `/api/collector/computers`.
|
||||
3. Remove `shopdb/core/api/collector.py` and the legacy blueprint registration.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- Generalizable across plugins. Sister sites adopting `printers`, `network`, etc. can wire their own collectors with no core change.
|
||||
- Identity-based idempotency makes PC re-imaging safe by default.
|
||||
- Audit logging is uniform across plugins.
|
||||
- Schema discovery enables external tools to validate before submission.
|
||||
|
||||
### Negative
|
||||
|
||||
- Plugin authors must write a JSON schema. Slight learning curve, but JSON Schema is widely understood and the framework can ship a few examples.
|
||||
- The `/api/collector/_schemas` endpoint plus per-plugin endpoints expand the public API surface; minor maintenance cost.
|
||||
|
||||
### Neutral
|
||||
|
||||
- API-key auth pattern stays as it is today (separate from JWT). Sites manage their own collector keys per plugin via env vars.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
1. **Keep `/api/collector/pc` and add new plugin-specific endpoints alongside.** Two ways to send PC data, plugin authors confused. Rejected.
|
||||
2. **Use JWT for collectors instead of API key.** Collectors are headless processes (PXE pipeline, scripts), not interactive users. JWT lifecycle (refresh tokens, expiry) is the wrong tool. API key is simpler. Rejected.
|
||||
3. **Plugins write directly to the database, no collector endpoint.** Skips audit logging and schema validation. Rejected.
|
||||
|
||||
## References
|
||||
|
||||
- `shopdb/core/api/collector.py` (legacy endpoint to be removed)
|
||||
- `shopdb/plugins/base.py` (`get_collector_schema` hook to be added)
|
||||
- ADR-001 (asset model the collectors target)
|
||||
- ADR-002 (collector schema is part of plugin contract; changes to the hook signature are major bumps)
|
||||
- The PXE project (`/home/camp/projects/pxe/`) which feeds the computers collector
|
||||
@@ -1,25 +0,0 @@
|
||||
# Architecture Decision Records
|
||||
|
||||
Each ADR captures a single architectural decision: the context, the decision itself, the consequences, and the alternatives considered. ADRs are immutable once accepted. Superseded ADRs stay in this folder with a pointer to the newer ADR.
|
||||
|
||||
## Status definitions
|
||||
|
||||
- **PROPOSED**: drafted, awaiting decision
|
||||
- **ACCEPTED**: decision is in effect
|
||||
- **SUPERSEDED**: replaced by a later ADR (link forward)
|
||||
- **DEPRECATED**: no longer in effect, no replacement
|
||||
|
||||
## Index
|
||||
|
||||
| ADR | Title | Status |
|
||||
|-----|-------|--------|
|
||||
| [001](ADR-001-asset-as-platform-contract.md) | Asset model is the platform contract | ACCEPTED |
|
||||
| [002](ADR-002-plugin-versioning.md) | Plugin contract versioning (semver) | ACCEPTED |
|
||||
| [003](ADR-003-plugin-distribution.md) | Plugin distribution model | ACCEPTED |
|
||||
| [004](ADR-004-deployment-topology.md) | Deployment topology (per-site instances) | ACCEPTED |
|
||||
| [005](ADR-005-equipment-vs-measuringtools.md) | Equipment vs measuringtools plugin scope | ACCEPTED |
|
||||
| [006](ADR-006-collector-contract.md) | Plugin collector contract pattern | ACCEPTED |
|
||||
|
||||
## Authoring
|
||||
|
||||
When proposing a new decision, copy the most recent ADR as a template, increment the number, and update this index. Do not edit accepted ADRs in place; supersede them with a new one.
|
||||
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