Skip to content

Commit 9d7a954

Browse files
committed
add create-admin script, update docs
1 parent 5c16cc3 commit 9d7a954

File tree

7 files changed

+190
-24
lines changed

7 files changed

+190
-24
lines changed

CHANGELOG.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,22 @@ All notable changes to ExaFS will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## [1.2.2] - 2026-02-16
8+
## [1.2.2] - 2026-02-19
99

1010
### Changed
1111
- **Database migrations now tracked in git**`migrations/` removed from `.gitignore`
1212
- Replaced `db-init.py` with migration-based initialization (`flask db upgrade`)
1313
- Removed one-time `/admin/set-org-if-zero` endpoint, replaced with standalone `scripts/migrate_v0x_to_v1.py`
1414
- Fixed Flask-SQLAlchemy deprecation warning in Alembic `env.py`
1515
- Template URLs changed to use `url_for` helper, removed unused `rule.html` template
16+
- **`db-init.py` and `create-admin.py` moved to `scripts/`** — all setup scripts now live under `scripts/`
1617

1718
### Added
1819
- Idempotent baseline migration (`001_baseline`) that brings any ExaFS database (from v0.4+ to current) to the v1.2.2 schema
1920
- Optional `scripts/migrate_v0x_to_v1.py` helper for v0.x to v1.0+ data migration (org_id backfill)
20-
- `db-init.py --reset` flag for development database reset
21+
- `scripts/create-admin.py` — interactive script to create the first admin user and organization (replaces manual SQL inserts)
22+
- `scripts/db-init.py --reset` flag for development database reset
23+
- Migration test suite (`tests/test_migration.py`) — 46 tests covering fresh install, idempotency, upgrade from v0.4/v0.8/v1.0 schemas, and real 2019 production backup upgrade
2124
- `PYTHONPATH` set in Docker dev container for easier development
2225

2326
## [1.2.1] - 2026-01-30

CLAUDE.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,9 @@ exafs/
115115
├── config.example.py # Configuration template
116116
├── instance_config_override.example.py # Dashboard override template
117117
├── run.example.py # Application run script template
118-
├── db-init.py # Database initialization (runs flask db upgrade)
119118
├── scripts/
119+
│ ├── db-init.py # Database initialization (runs flask db upgrade)
120+
│ ├── create-admin.py # Interactive first admin user setup
120121
│ └── migrate_v0x_to_v1.py # Optional v0.x to v1.0+ migration helper
121122
├── pyproject.toml # Project metadata and dependencies
122123
├── setup.cfg # Setup configuration
@@ -286,7 +287,10 @@ cp run.example.py run.py
286287
# Edit config.py with database credentials and settings
287288

288289
# Initialize database (runs flask db upgrade)
289-
python db-init.py
290+
python scripts/db-init.py
291+
292+
# Create the first admin user and organization
293+
python scripts/create-admin.py
290294

291295
# Run tests
292296
pytest
@@ -795,8 +799,9 @@ flask db upgrade # Apply migrations
795799
flake8 . # Lint code
796800

797801
# Database
798-
python db-init.py # Initialize database (runs migrations)
799-
python db-init.py --reset # Drop all tables and recreate (dev only)
802+
python scripts/db-init.py # Initialize database (runs migrations)
803+
python scripts/db-init.py --reset # Drop all tables and recreate (dev only)
804+
python scripts/create-admin.py # Create first admin user interactively
800805
flask db stamp 001_baseline # Mark existing DB as baseline
801806
flask db current # Show current migration
802807
flask db history # Show migration history

docs/DB_MIGRATIONS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ flask db upgrade
1313
Or use the init script:
1414

1515
```bash
16-
python db-init.py
16+
python scripts/db-init.py
1717
```
1818

1919
## Upgrading Between Versions
@@ -102,7 +102,7 @@ Commit the migration file to git so other deployments can apply it.
102102
To completely reset the database during development:
103103

104104
```bash
105-
python db-init.py --reset
105+
python scripts/db-init.py --reset
106106
```
107107

108108
This drops all tables and recreates them from scratch. **Do not use in production.**

docs/INSTALL.md

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -126,26 +126,19 @@ You can skip this section if you are using a different deployment method, such a
126126

127127
#### Final steps - as deploy user
128128

129-
1. Copy config.example.py to config.py and fill out the DB credetials.
129+
1. Copy config.example.py to config.py and fill out the DB credentials.
130130

131-
2. Create and populate database tables.
131+
2. Create and populate database tables (roles, actions, rule states):
132132
```
133133
cd ~/www
134134
source venv/bin/activate
135-
python db-init.py
135+
python scripts/db-init.py
136136
```
137-
DB-init script inserts default roles, actions, rule states and two organizations (TUL and Cesnet). But no users.
138-
139-
3. Before start, **use your favorite mysql admin tool and insert some users into database**.
140-
The **uuid** of user should be set the **eppn** value provided by Shibboleth.
141-
142-
You can use following MYSQL commands to insert the user, give him role 'admin' and add him to the the organization 'Cesnet'.
143137

138+
3. Create the first admin user and organization using the interactive setup script:
144139
```
145-
insert into user (uuid,email,name) values ('example@cesnet.cz', 'example@cesnet.cz', 'Mr. Example Admin');
146-
insert into user_role (user_id,role_id) values (1, 3);
147-
insert into user_organization (user_id,organization_id) values (1, 2);
148-
```
149-
You can also modify the models.py for your own default values for db-init.
140+
python scripts/create-admin.py
141+
```
142+
The script will prompt you for the admin's UUID (Shibboleth eppn), name, email, phone, and then create or select an organization with its network address range. It assigns the admin role automatically.
150143

151144
The application is installed and should be working now. The next step is to configure ExaBGP and connect it to the ExaAPI application. We also provide simple service called guarda to reload all the rules in case of ExaBGP restart.

scripts/create-admin.py

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
"""
2+
Create the initial admin user and organization for ExaFS.
3+
4+
Run this once after 'python db-init.py' to set up the first administrator
5+
and their organization. Without at least one admin user, the application
6+
cannot be managed through the web interface.
7+
8+
Usage:
9+
python create-admin.py
10+
"""
11+
12+
import sys
13+
from os import environ
14+
15+
from flowapp import create_app, db
16+
from flowapp.models import Organization, Role, User
17+
18+
import config
19+
20+
21+
def prompt(label, required=True, default=None):
22+
"""Prompt for input, optionally with a default value."""
23+
if default:
24+
display = f"{label} [{default}]: "
25+
else:
26+
display = f"{label}: "
27+
28+
while True:
29+
value = input(display).strip()
30+
if not value and default:
31+
return default
32+
if value:
33+
return value
34+
if not required:
35+
return ""
36+
print(f" {label} is required.")
37+
38+
39+
def create_admin():
40+
exafs_env = environ.get("EXAFS_ENV", "Production").lower()
41+
if exafs_env in ("devel", "development"):
42+
app = create_app(config.DevelopmentConfig)
43+
else:
44+
app = create_app(config.ProductionConfig)
45+
46+
db.init_app(app)
47+
48+
with app.app_context():
49+
# Verify migrations have been run
50+
admin_role = Role.query.filter_by(name="admin").first()
51+
if not admin_role:
52+
print("Error: roles not found in database.")
53+
print("Please run 'python db-init.py' first.")
54+
sys.exit(1)
55+
56+
print()
57+
print("ExaFS initial admin setup")
58+
print("=" * 40)
59+
60+
# --- User ---
61+
print()
62+
print("Admin user")
63+
print("-" * 20)
64+
print("UUID is the unique identifier used for authentication.")
65+
print("For SSO (Shibboleth), this is typically the eppn attribute.")
66+
print("For local auth, use any unique string (e.g. email address).")
67+
print()
68+
69+
while True:
70+
uuid = prompt("UUID (e.g. user@example.edu)")
71+
existing = User.query.filter_by(uuid=uuid).first()
72+
if existing:
73+
print(f" A user with UUID '{uuid}' already exists.")
74+
overwrite = input(" Update this user's roles and org? (yes/no): ").strip().lower()
75+
if overwrite == "yes":
76+
user = existing
77+
break
78+
continue
79+
user = None
80+
break
81+
82+
name = prompt("Full name", required=False)
83+
email = prompt("Email", default=uuid if "@" in uuid else None)
84+
phone = prompt("Phone", required=False)
85+
86+
# --- Organization ---
87+
print()
88+
print("Organization")
89+
print("-" * 20)
90+
print("Address ranges (arange) are whitespace-separated CIDR prefixes.")
91+
print("Example: 192.0.2.0/24 2001:db8::/32")
92+
print()
93+
94+
orgs = Organization.query.all()
95+
if orgs:
96+
print("Existing organizations:")
97+
for org in orgs:
98+
print(f" [{org.id}] {org.name}")
99+
print()
100+
choice = input("Use existing organization ID, or press Enter to create new: ").strip()
101+
if choice.isdigit():
102+
org = Organization.query.get(int(choice))
103+
if not org:
104+
print(f" Organization {choice} not found, creating new.")
105+
org = None
106+
else:
107+
org = None
108+
else:
109+
org = None
110+
111+
if org is None:
112+
org_name = prompt("Organization name")
113+
org_arange = prompt("Address ranges (CIDR, space-separated)")
114+
org = Organization(name=org_name, arange=org_arange)
115+
db.session.add(org)
116+
db.session.flush() # get org.id before commit
117+
print(f" Created organization: {org.name}")
118+
119+
# --- Confirm ---
120+
print()
121+
print("Summary")
122+
print("=" * 40)
123+
print(f" UUID: {uuid}")
124+
print(f" Name: {name or '(not set)'}")
125+
print(f" Email: {email or '(not set)'}")
126+
print(f" Phone: {phone or '(not set)'}")
127+
print(f" Role: admin")
128+
print(f" Organization: {org.name}")
129+
print()
130+
131+
confirm = input("Create admin user? (yes/no): ").strip().lower()
132+
if confirm != "yes":
133+
print("Aborted.")
134+
db.session.rollback()
135+
sys.exit(0)
136+
137+
# --- Create or update user ---
138+
if user is None:
139+
user = User(uuid=uuid, name=name or None, email=email or None, phone=phone or None)
140+
db.session.add(user)
141+
else:
142+
if name:
143+
user.name = name
144+
if email:
145+
user.email = email
146+
if phone:
147+
user.phone = phone
148+
149+
# Assign admin role (avoid duplicates)
150+
if not user.role.filter_by(name="admin").first():
151+
user.role.append(admin_role)
152+
153+
# Assign organization (avoid duplicates)
154+
if not user.organization.filter_by(id=org.id).first():
155+
user.organization.append(org)
156+
157+
db.session.commit()
158+
159+
print()
160+
print(f"Admin user '{uuid}' created successfully.")
161+
print(f"Organization: {org.name}")
162+
print()
163+
print("You can now log in and manage ExaFS through the web interface.")
164+
165+
166+
if __name__ == "__main__":
167+
create_admin()
File renamed without changes.

tests/test_migration.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@
1111
- Preserving existing data during migration
1212
"""
1313

14-
import os
15-
1614
import pytest
1715
from flask import Flask
1816
from flask_sqlalchemy import SQLAlchemy

0 commit comments

Comments
 (0)