Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
config.py
instance_config_override.py
run.py
migrations/

# PyPi
.pypirc
Expand Down
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,21 @@ All notable changes to ExaFS will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.2.2] - 2026-02-16

### Changed
- **Database migrations now tracked in git** — `migrations/` removed from `.gitignore`
- Replaced `db-init.py` with migration-based initialization (`flask db upgrade`)
- Removed one-time `/admin/set-org-if-zero` endpoint, replaced with standalone `scripts/migrate_v0x_to_v1.py`
- Fixed Flask-SQLAlchemy deprecation warning in Alembic `env.py`
- Template URLs changed to use `url_for` helper, removed unused `rule.html` template

### Added
- Idempotent baseline migration (`001_baseline`) that brings any ExaFS database (from v0.4+ to current) to the v1.2.2 schema
- Optional `scripts/migrate_v0x_to_v1.py` helper for v0.x to v1.0+ data migration (org_id backfill)
- `db-init.py --reset` flag for development database reset
- `PYTHONPATH` set in Docker dev container for easier development

## [1.2.1] - 2026-01-30

### Fixed
Expand Down Expand Up @@ -286,6 +301,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Route Distinguisher for VRF now supported
- See config example and update your `config.py`

[1.2.2]: https://github.com/CESNET/exafs/compare/v1.2.1...v1.2.2
[1.2.1]: https://github.com/CESNET/exafs/compare/v1.2.0...v1.2.1
[1.2.0]: https://github.com/CESNET/exafs/compare/v1.1.9...v1.2.0
[1.1.9]: https://github.com/CESNET/exafs/compare/v1.1.8...v1.1.9
[1.1.8]: https://github.com/CESNET/exafs/compare/v1.1.7...v1.1.8
Expand Down
19 changes: 14 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,9 @@ exafs/
├── config.example.py # Configuration template
├── instance_config_override.example.py # Dashboard override template
├── run.example.py # Application run script template
├── db-init.py # Database initialization script
├── db-init.py # Database initialization (runs flask db upgrade)
├── scripts/
│ └── migrate_v0x_to_v1.py # Optional v0.x to v1.0+ migration helper
├── pyproject.toml # Project metadata and dependencies
├── setup.cfg # Setup configuration
├── CHANGELOG.md # Version history
Expand Down Expand Up @@ -283,7 +285,7 @@ cp run.example.py run.py

# Edit config.py with database credentials and settings

# Initialize database
# Initialize database (runs flask db upgrade)
python db-init.py

# Run tests
Expand All @@ -295,15 +297,20 @@ python run.py

### Database Migrations

Migration files are tracked in `migrations/versions/` and committed to git.

```bash
# Create a new migration
# Create a new migration after model changes
flask db migrate -m "Description of changes"

# Apply migrations
flask db upgrade

# Rollback migration
flask db downgrade

# For existing databases adopting migrations for the first time
flask db stamp 001_baseline
```

### Running Tests
Expand Down Expand Up @@ -788,7 +795,9 @@ flask db upgrade # Apply migrations
flake8 . # Lint code

# Database
python db-init.py # Initialize database
python db-init.py # Initialize database (runs migrations)
python db-init.py --reset # Drop all tables and recreate (dev only)
flask db stamp 001_baseline # Mark existing DB as baseline
flask db current # Show current migration
flask db history # Show migration history

Expand All @@ -804,7 +813,7 @@ supervisorctl status # Check status
When working with this codebase:

1. **Always run tests** after making changes: `pytest`
2. **Create migrations** for model changes: `flask db migrate`
2. **Create migrations** for model changes: `flask db migrate` — commit migration files to git
3. **Follow the service layer pattern** - business logic goes in services, not views
4. **Use existing validators** in `flowapp/validators.py` for validation
5. **Check authentication** - most routes need `@auth_required` decorator
Expand Down
72 changes: 44 additions & 28 deletions db-init.py
Original file line number Diff line number Diff line change
@@ -1,39 +1,55 @@
"""
Initialize the ExaFS database using Alembic migrations.

from flask import Flask
from flowapp import db
from flowapp.models import *
Usage:
python db-init.py # Create database from baseline migration
python db-init.py --reset # Drop all tables first, then recreate (DESTRUCTIVE)
"""

import config
import sys
from os import environ

from flask_migrate import upgrade
from flowapp import create_app, db

import config

def create_app():
app = Flask('FlowSpecDB init')
# Configurations
try:
env = environ['USERNAME']
except KeyError as e:
env = 'Production'

if env == 'albert':
print("DEVEL")
app.config.from_object(config.DevelopmentConfig)
def init_db(reset=False):
exafs_env = environ.get("EXAFS_ENV", "Production").lower()
if exafs_env in ("devel", "development"):
app = create_app(config.DevelopmentConfig)
else:
print("PRODUCTION")
app.config.from_object(config.ProductionConfig)
app = create_app(config.ProductionConfig)

db.init_app(app)

with app.app_context():
print("#: cleaning database")
db.reflect()
db.drop_all()
print("#: creating tables")
db.create_all()


return app


if __name__ == '__main__':
create_app().app_context().push()
if reset:
print("#: WARNING - dropping all tables")
db.reflect()
db.drop_all()
# Also remove alembic_version if it exists
from sqlalchemy import text

try:
db.session.execute(text("DROP TABLE IF EXISTS alembic_version"))
db.session.commit()
except Exception:
db.session.rollback()

print("#: running migrations (flask db upgrade)")
upgrade()
print("#: database initialized successfully")


if __name__ == "__main__":
reset = "--reset" in sys.argv
if reset:
print("Reset mode: all existing data will be DESTROYED.")
confirm = input("Are you sure? (yes/no): ")
if confirm.lower() != "yes":
print("Aborted.")
sys.exit(0)

init_db(reset=reset)
109 changes: 91 additions & 18 deletions docs/DB_MIGRATIONS.md
Original file line number Diff line number Diff line change
@@ -1,35 +1,108 @@
# How to Upgrade the Database
# Database Migrations

## General Guidelines
Migrations can be inconsistent. To avoid issues, we removed migrations from git repostory. To start the migration on your server, it is recomended reset the migration state on the server and run the migration based on the updated database models when switching application versions via Git.
ExaFS uses [Flask-Migrate](https://flask-migrate.readthedocs.io/) (Alembic) for database schema management. Migration files are shipped inside the `flowapp` package (`flowapp/migrations/`) and are found automatically — no `flask db init` is needed.

## New Installation

For a fresh database, run the migrations to create all tables and seed data:

```bash
rm -rf migrations/
flask db upgrade
```

```SQL
DROP TABLE alembic_version;
Or use the init script:

```bash
python db-init.py
```

## Upgrading Between Versions

When upgrading ExaFS to a new version, apply any new migrations:

```bash
flask db upgrade
```

This will apply only the migrations that haven't been applied yet.

## Existing Installation (One-Time Setup)

If you already have a running ExaFS database from any previous version, the baseline migration is idempotent — it will create missing tables, add missing columns, and skip anything that already exists.

### Deployments that used `flask db init` (self-managed migrations)

Some deployments previously ran `flask db init` to create a local `migrations/` directory and auto-generated migration files. Starting with v1.2.2, migration files are tracked in git and shipped with the project. To switch to the official migrations:

1. **Delete the local migrations directory** created by `flask db init`:
```bash
rm -rf migrations/
```
Migrations are now bundled inside the `flowapp` pip package — no local directory needed.

2. **Clear the old alembic_version** and **stamp the baseline** to register with the official migration track (your schema is already up to date):
```sql
DELETE FROM alembic_version;
```
```bash
flask db stamp 001_baseline
```

3. From now on, just run `flask db upgrade` when updating ExaFS.

### Deployments without any migration tracking

If your database has an `alembic_version` table from a previous migration setup but no local `migrations/` directory, clear it first:

```sql
DELETE FROM alembic_version;
```

Then run the upgrade:

```bash
flask db init
flask db migrate -m "Initial migration based on current DB state"
flask db upgrade
```

## Steps for Upgrading to v1.0.x
Limits for number of rules were introduced. Some database engines (Mariadb 10.x for example) have issue to set Non Null foreigin key to 0 and automatic migrations fail. The solution may be in diferent version (Mariadb 11.x works fine), or to set limits in db manually later.
The baseline migration will inspect your database and bring it up to the current schema without affecting existing data.

To set the limit to 0 for existing organizations run
## Upgrading from v0.x to v1.0+

```SQL
UPDATE organization
SET limit_flowspec4 = 0, limit_flowspec6 = 0, limit_rtbh = 0
WHERE limit_flowspec4 IS NULL OR limit_flowspec6 IS NULL OR limit_rtbh IS NULL;
If you are upgrading from a pre-1.0 version, the baseline migration will add the missing `org_id` columns and organization limit columns automatically. However, existing rules still need to be linked to organizations. An optional helper script is provided for this:

```bash
python scripts/migrate_v0x_to_v1.py
```

In all cases we need later assign rules to organizations. There's an admin endpoint for this:
This script:
1. Sets NULL organization limits to 0
2. Helps assign existing rules to organizations based on users' organizations
3. Reports users with multiple organizations or ambiguous rule ownership that need manual assignment

Feel free to contact jiri.vrany@cesnet.cz if you need help with the migration.

## Creating New Migrations

When you modify a database model, create a new migration:

```bash
flask db migrate -m "Description of changes"
```

`https://yourexafs.url/admin/set-org-if-zero`
Review the generated file in `flowapp/migrations/versions/`, then apply it:

```bash
flask db upgrade
```

Commit the migration file to git so other deployments can apply it.

## Development Reset

To completely reset the database during development:

```bash
python db-init.py --reset
```

Or you can start with clean database and manually migrate data by SQL dump later. Feel free to contact jiri.vrany@cesnet.cz if you need help with the DB migration to 1.0.x.
This drops all tables and recreates them from scratch. **Do not use in production.**
2 changes: 1 addition & 1 deletion flowapp/__about__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = "1.2.1"
__version__ = "1.2.2"
__title__ = "ExaFS"
__description__ = "Tool for creation, validation, and execution of ExaBGP messages."
__author__ = "CESNET / Jiri Vrany, Petr Adamec, Josef Verich, Jakub Man"
Expand Down
6 changes: 5 additions & 1 deletion flowapp/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# -*- coding: utf-8 -*-
import os

from flask import Flask, redirect, render_template, session, url_for, flash

from flask_sso import SSO
Expand All @@ -13,9 +15,11 @@
from .__about__ import __version__
from .instance_config import InstanceConfig

# Migrations directory lives inside the package so it ships with pip install
_migrations_dir = os.path.join(os.path.dirname(__file__), "migrations")

db = SQLAlchemy()
migrate = Migrate()
migrate = Migrate(directory=_migrations_dir)
csrf = CSRFProtect()
ext = SSO()
sess = Session()
Expand Down
1 change: 1 addition & 0 deletions flowapp/migrations/README
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Single-database configuration for Flask.
50 changes: 50 additions & 0 deletions flowapp/migrations/alembic.ini
Original file line number Diff line number Diff line change
@@ -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
Loading