Skip to content

Commit 2033d8d

Browse files
committed
Add exafs-db-init and exafs-create-admin console scripts
- Move script logic into flowapp/scripts/ package so it ships with pip - Add [project.scripts] entry points in pyproject.toml: exafs-db-init, exafs-create-admin - Shared _load_config() and _create_app() helpers in __init__.py (adds cwd to sys.path so config.py is found regardless of invocation method) - scripts/db-init.py and scripts/create-admin.py reduced to thin wrappers - Update docs (INSTALL.md, DB_MIGRATIONS.md, CLAUDE.md) to document both PyPI and source install paths
1 parent 9d7a954 commit 2033d8d

File tree

9 files changed

+344
-230
lines changed

9 files changed

+344
-230
lines changed

CLAUDE.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -798,10 +798,16 @@ flask db migrate -m "message" # Create migration
798798
flask db upgrade # Apply migrations
799799
flake8 . # Lint code
800800

801-
# Database
801+
# Database (source install)
802802
python scripts/db-init.py # Initialize database (runs migrations)
803803
python scripts/db-init.py --reset # Drop all tables and recreate (dev only)
804804
python scripts/create-admin.py # Create first admin user interactively
805+
806+
# Database (PyPI install — run from directory containing config.py)
807+
exafs-db-init # Initialize database (runs migrations)
808+
exafs-db-init --reset # Drop all tables and recreate (dev only)
809+
exafs-create-admin # Create first admin user interactively
810+
805811
flask db stamp 001_baseline # Mark existing DB as baseline
806812
flask db current # Show current migration
807813
flask db history # Show migration history

docs/DB_MIGRATIONS.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,18 @@ For a fresh database, run the migrations to create all tables and seed data:
1010
flask db upgrade
1111
```
1212

13-
Or use the init script:
13+
Or use the init script (source install):
1414

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

19+
Or the installed command (PyPI install):
20+
21+
```bash
22+
exafs-db-init
23+
```
24+
1925
## Upgrading Between Versions
2026

2127
When upgrading ExaFS to a new version, apply any new migrations:
@@ -102,7 +108,8 @@ Commit the migration file to git so other deployments can apply it.
102108
To completely reset the database during development:
103109

104110
```bash
105-
python scripts/db-init.py --reset
111+
python scripts/db-init.py --reset # source install
112+
exafs-db-init --reset # PyPI install
106113
```
107114

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

docs/INSTALL.md

Lines changed: 69 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -67,19 +67,52 @@ FLUSH PRIVILEGES;
6767
exit;
6868
```
6969

70-
#### App instalation
71-
Create new user called **deploy** in the system.
70+
#### App installation
71+
72+
Create a user called **deploy** in the system, then switch to that user.
7273

73-
As deploy user pull the source codes from GH, create virtualenv and install required python dependencies.
7474
```
7575
su - deploy
76-
clone source from repository: git clone https://github.com/CESNET/exafs.git www
76+
```
77+
78+
There are two ways to install ExaFS. Choose the one that fits your workflow:
79+
80+
---
81+
82+
**Option A — Install from PyPI (recommended)**
83+
84+
This is the simplest approach. The `flowapp` package is installed into the virtualenv. All templates, static files, migrations, and setup commands (`exafs-db-init`, `exafs-create-admin`) are included automatically.
85+
86+
```
87+
mkdir ~/www && cd ~/www
88+
virtualenv --python=python3.9 venv
89+
source venv/bin/activate
90+
pip install exafs
91+
```
92+
93+
You still need `config.py` and `run.py` in the working directory. Download the example files from the repository:
94+
95+
```
96+
curl -O https://raw.githubusercontent.com/CESNET/exafs/master/config.example.py
97+
curl -O https://raw.githubusercontent.com/CESNET/exafs/master/run.example.py
98+
```
99+
100+
---
101+
102+
**Option B — Install from source**
103+
104+
Use this if you want to track a specific branch, contribute changes, or pin to a git commit.
105+
106+
```
107+
git clone https://github.com/CESNET/exafs.git www
77108
cd www
78109
virtualenv --python=python3.9 venv
79110
source venv/bin/activate
80-
pip install -r requirements.txt
111+
pip install -e .
81112
```
82113

114+
---
115+
83116
#### Next steps (as root)
84117

85118
Now lets continue as root user once again.
@@ -126,19 +159,38 @@ You can skip this section if you are using a different deployment method, such a
126159

127160
#### Final steps - as deploy user
128161

129-
1. Copy config.example.py to config.py and fill out the DB credentials.
162+
1. Copy `config.example.py` to `config.py` and fill out the DB credentials.
130163

131-
2. Create and populate database tables (roles, actions, rule states):
132-
```
133-
cd ~/www
134-
source venv/bin/activate
135-
python scripts/db-init.py
136-
```
164+
2. Copy `run.example.py` to `run.py`. This is the WSGI entry point used by uWSGI.
137165

138-
3. Create the first admin user and organization using the interactive setup script:
139-
```
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.
166+
3. Create and populate database tables (roles, actions, rule states).
167+
168+
**PyPI install** — run from `~/www` (where `config.py` lives):
169+
```
170+
cd ~/www && source venv/bin/activate
171+
exafs-db-init
172+
```
173+
174+
**Source install:**
175+
```
176+
cd ~/www && source venv/bin/activate
177+
python scripts/db-init.py
178+
```
179+
180+
4. Create the first admin user and organization using the interactive setup script.
181+
182+
**PyPI install:**
183+
```
184+
exafs-create-admin
185+
```
186+
187+
**Source install:**
188+
```
189+
python scripts/create-admin.py
190+
```
191+
192+
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.
193+
194+
> **Note:** Both commands must be run from the directory containing `config.py`, as they load the database credentials from that file.
143195
144196
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.

flowapp/scripts/__init__.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import sys
2+
from os import environ
3+
4+
5+
def _load_config():
6+
"""Load config.py from the current working directory."""
7+
import os
8+
cwd = os.getcwd()
9+
if cwd not in sys.path:
10+
sys.path.insert(0, cwd)
11+
try:
12+
import config
13+
return config
14+
except ImportError:
15+
print("Error: config.py not found in the current directory.")
16+
print("Copy config.example.py to config.py and fill in your database credentials.")
17+
sys.exit(1)
18+
19+
20+
def _create_app():
21+
"""Load config and create the Flask app."""
22+
config = _load_config()
23+
24+
from flowapp import create_app, db
25+
26+
exafs_env = environ.get("EXAFS_ENV", "Production").lower()
27+
if exafs_env in ("devel", "development"):
28+
app = create_app(config.DevelopmentConfig)
29+
else:
30+
app = create_app(config.ProductionConfig)
31+
32+
db.init_app(app)
33+
return app

flowapp/scripts/create_admin.py

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

0 commit comments

Comments
 (0)