Start work on easy migration from one version to another using alembic

This commit is contained in:
Théophile Diot 2024-12-20 15:26:07 +01:00
parent b97257c7c3
commit 9bc64d515d
No known key found for this signature in database
GPG key ID: FA995104A0BA376A
83 changed files with 6620 additions and 678 deletions

View file

@ -48,6 +48,7 @@ repos:
- id: flake8
name: Flake8 Python Linter
args: ["--max-line-length=160", "--ignore=E266,E402,E501,E722,W503"]
exclude: ^src/common/db/alembic/(mariadb|mysql|postgresql|sqlite)_versions
- repo: https://github.com/dosisod/refurb
rev: 2e31f0033b6c00bf99912fc6a8b5fd00460c9ba0 # frozen: v2.0.0

34
misc/migration/Dockerfile Normal file
View file

@ -0,0 +1,34 @@
FROM python:3.10-alpine@sha256:cf8ad3b55fcf5fa0cfeecabc65878f7504c8e6ec5c8af5511b0a039cc46a1ac3
# Install python dependencies
RUN apk add --no-cache --virtual .build-deps \
build-base libffi-dev postgresql-dev cargo
# Copy python requirements
COPY src/deps/requirements.txt /tmp/requirements-deps.txt
COPY src/common/db/requirements.txt /tmp/req/requirements-db.txt
WORKDIR /usr/share/migration
# Install python requirements
RUN export MAKEFLAGS="-j$(nproc)" && \
pip install --no-cache-dir --require-hashes --break-system-packages -r /tmp/requirements-deps.txt && \
pip install --no-cache-dir --require-hashes -r /tmp/req/requirements-db.txt
# Install dependencies
RUN apk add --no-cache bash sed curl mariadb-connector-c mariadb-client postgresql-client sqlite tzdata
# Cleanup
RUN apk del .build-deps && \
rm -rf /var/cache/apk/*
# Copy files
COPY src/common/db/alembic/alembic.ini alembic.ini
COPY src/common/db/alembic/env.py env.py
COPY src/common/db/alembic/script.py.mako script.py.mako
COPY src/common/db/model.py latest_model.py
COPY misc/migration/entrypoint.sh entrypoint.sh
RUN chmod +x entrypoint.sh
ENTRYPOINT [ "./entrypoint.sh" ]

175
misc/migration/create.sh Executable file
View file

@ -0,0 +1,175 @@
#!/bin/bash
# Optimized migration script
set -euo pipefail
log() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*"
}
# Fetch and process tags
log "🌐 Fetching tags from GitHub"
tags=$(curl -s https://api.github.com/repos/bunkerity/bunkerweb/tags |
jq -r '.[].name | sub("^v"; "")' |
jq -R -s -c 'split("\n")[:-1] | map(select(test("^[1-9]+\\.(5|[6-9]|[1-9][0-9]+)")))' |
jq -c 'reverse')
current_dir=$(basename "$(pwd)")
# Navigate to the root directory if in a subdirectory
case "$current_dir" in
migration) cd ../.. ;;
misc) cd .. ;;
esac
if [[ ! -f src/VERSION ]]; then
log "❌ src/VERSION file not found"
exit 1
fi
# Read and validate the current version
current_version=$(<src/VERSION)
if [[ "$current_version" != "dev" && "$current_version" != "testing" ]]; then
tags=$(echo "$tags" | jq -c --arg version "$current_version" 'if index($version) == null then . + [$version] else . end')
fi
# Build the Docker image
log "🐳 Building Docker image for migration"
docker build -t local/bw-migration -f misc/migration/Dockerfile .
# Ensure we're in the migration directory
cd misc/migration || exit 1
db_dir=$(realpath ../../src/common/db)
# Process each tag and database combination
log "🏗️ Processing migration tags and databases"
NEXT_TAG="dev"
jq -r 'to_entries[] | "\(.key) \(.value)"' databases.json | while read -r database database_uri; do
started=0
for tag in $(echo "$tags" | jq -r '.[]'); do
if [ "$tag" == "$NEXT_TAG" ]; then
continue
fi
export DATABASE="$database"
export DATABASE_URI="${database_uri//+psycopg}"
if [[ "$started" -eq 0 ]]; then
tag_index=$(echo "$tags" | jq -r --arg current_tag "$tag" 'index($current_tag)')
next_tag_index=$((tag_index + 1))
export TAG="$tag"
NEXT_TAG=$(echo "$tags" | jq -r --argjson idx "$next_tag_index" '.[$idx] // empty')
export NEXT_TAG
if [[ -z "$NEXT_TAG" ]]; then
log "🔚 Skipping migration for the last tag $tag"
continue
fi
log "✨ Creating migration scripts from version $TAG to $NEXT_TAG and database $database"
started=1
# Start the database stack if not SQLite
if [[ "$database" != "sqlite" ]]; then
log "🚀 Starting Docker stack for $database"
if ! docker compose -f "$database.yml" up -d; then
log "❌ Failed to start the Docker stack for $database"
docker compose down -v --remove-orphans
find "$db_dir" -type d -name "__pycache__" -exec rm -rf {} +
exit 1
fi
fi
log "🚀 Starting Docker stack for BunkerWeb"
if ! docker compose up -d; then
log "❌ Failed to start the Docker stack for BunkerWeb"
docker compose down -v --remove-orphans
find "$db_dir" -type d -name "__pycache__" -exec rm -rf {} +
exit 1
fi
# Wait for the scheduler to be healthy
log "⏳ Waiting for the scheduler to become healthy"
timeout=60
until docker compose ps bw-scheduler | grep -q "(healthy)" || [[ $timeout -le 0 ]]; do
sleep 5
timeout=$((timeout - 5))
done
if [[ $timeout -le 0 ]]; then
log "❌ Timeout waiting for the scheduler to be healthy"
docker compose logs bw-scheduler
docker compose down -v --remove-orphans
find "$db_dir" -type d -name "__pycache__" -exec rm -rf {} +
exit 1
fi
log "✅ Scheduler is healthy"
docker compose stop bw-scheduler bunkerweb || true
else
export NEXT_TAG="$tag"
if [[ -z "$NEXT_TAG" ]]; then
log "🔚 Skipping migration for the last tag $tag"
continue
fi
log "✨ Creating migration scripts from version $TAG to $NEXT_TAG and database $database"
fi
transformed_tag="${NEXT_TAG//[.-]/_}.py"
migration_dir="${db_dir}/alembic/${database}_versions"
# Skip if migration script already exists
export ONLY_UPDATE=0
if compgen -G "$migration_dir"/*_"$transformed_tag" > /dev/null; then
log "🔄 Migration scripts for version $tag and database $database already exist"
export ONLY_UPDATE=1
fi
export DATABASE_URI="$database_uri"
# Run the migration script
log "🦃 Running migration script for $tag and $database"
if ! docker run --rm \
--network=bw-db \
-v bw-data:/data \
-v bw-db:/db \
-v bw-sqlite:/var/lib/bunkerweb \
-v "$migration_dir":/usr/share/migration/versions \
-e TAG \
-e DATABASE \
-e DATABASE_URI \
-e NEXT_TAG \
-e ONLY_UPDATE \
-e UID="$(id -u)" \
-e GID="$(id -g)" \
local/bw-migration; then
log "❌ Failed to run the migration script"
docker compose down -v --remove-orphans
find "$db_dir" -type d -name "__pycache__" -exec rm -rf {} +
exit 1
fi
export TAG="$tag"
echo ""
done
# Clean up Docker stack
log "🧹 Cleaning up Docker stack"
docker compose down -v --remove-orphans
done
log "🎉 Migration scripts generation completed"
# Final cleanup
log "🛑 Stopping and cleaning up any remaining Docker stacks"
docker compose down -v --remove-orphans || true
find "$db_dir" -type d -name "__pycache__" -exec rm -rf {} +
cd "$current_dir" || exit

View file

@ -0,0 +1,3 @@
{
"postgresql": "postgresql+psycopg://bunkerweb:secret@bw-db:5432/db"
}

View file

@ -0,0 +1,75 @@
services:
bunkerweb:
image: bunkerity/bunkerweb:${TAG}
labels:
bunkerweb.INSTANCE: "yes"
environment:
SERVER_NAME: ""
API_WHITELIST_IP: "127.0.0.0/24 10.20.30.0/24"
USE_BUNKERNET: "no"
USE_BLACKLIST: "no"
USE_WHITELIST: "no"
SEND_ANONYMOUS_REPORT: "no"
networks:
bw-universe:
aliases:
- bunkerweb
bw-scheduler:
image: bunkerity/bunkerweb-scheduler:${TAG}
environment:
BUNKERWEB_INSTANCES: "bunkerweb"
SERVER_NAME: ""
API_WHITELIST_IP: "127.0.0.0/24 10.20.30.0/24"
DATABASE_URI: "${DATABASE_URI}"
USE_BUNKERNET: "no"
USE_BLACKLIST: "no"
USE_WHITELIST: "no"
SEND_ANONYMOUS_REPORT: "no"
DOCKER_HOST: "tcp://bw-docker:2375"
volumes:
- bw-data:/data
- bw-db:/usr/share/bunkerweb/db
- bw-sqlite:/var/lib/bunkerweb
networks:
bw-universe:
aliases:
- bw-scheduler
bw-db:
aliases:
- bw-scheduler
bw-docker:
aliases:
- bw-scheduler
bw-docker:
image: tecnativa/docker-socket-proxy:nightly
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
CONTAINERS: "1"
LOG_LEVEL: "warning"
networks:
bw-docker:
aliases:
- bw-docker
volumes:
bw-data:
name: bw-data
bw-db:
name: bw-db
bw-sqlite:
name: bw-sqlite
networks:
bw-universe:
name: bw-universe
ipam:
driver: default
config:
- subnet: 10.20.30.0/24
bw-db:
name: bw-db
bw-docker:
name: bw-docker

View file

@ -0,0 +1,99 @@
#!/bin/bash
# Function for printing error messages and exiting
function exit_with_error() {
echo "$1"
exit 1
}
# Function for checking required files
function check_file_exists() {
[ ! -f "$1" ] && exit_with_error "$1 file not found"
}
# Check required files
check_file_exists "/db/model.py"
check_file_exists "alembic.ini"
check_file_exists "env.py"
check_file_exists "script.py.mako"
# Check required environment variables
[ -z "$DATABASE" ] && exit_with_error "DATABASE environment variable is not set"
[ -z "$DATABASE_URI" ] && exit_with_error "DATABASE_URI environment variable is not set"
[ -z "$TAG" ] && exit_with_error "TAG environment variable is not set"
[ -z "$NEXT_TAG" ] && exit_with_error "NEXT_TAG environment variable is not set"
[ -z "$ONLY_UPDATE" ] && exit_with_error "ONLY_UPDATE environment variable is not set"
# Validate database type
case "$DATABASE" in
sqlite|mariadb|mysql|postgresql)
;;
*)
exit_with_error "Unsupported database type: $DATABASE"
;;
esac
# Configure SQLAlchemy URL in alembic.ini
echo "🔧 Configuring SQLAlchemy URL in alembic.ini"
sed -i "s|^sqlalchemy\\.url =.*$|sqlalchemy.url = ${DATABASE_URI}|" alembic.ini || exit_with_error "Failed to update SQLAlchemy URL in alembic.ini"
# Test database connection
echo "🔗 Testing database connection..."
python3 -c "from sqlalchemy import create_engine; create_engine('${DATABASE_URI}').connect()" || exit_with_error "Unable to connect to the database at $DATABASE_URI"
echo "✅ Database connection successful"
# Download the next tag model file
echo "📥 Downloading the model file for version $NEXT_TAG"
if ! curl -f -s -o /db/model.py "https://raw.githubusercontent.com/bunkerity/bunkerweb/refs/tags/v${NEXT_TAG}/src/common/db/model.py"; then
echo "⚠️ Failed to download model file, using latest_model.py instead"
if [ -f "latest_model.py" ]; then
mv latest_model.py /db/model.py || exit_with_error "Failed to move latest_model.py to /db/model.py"
else
exit_with_error "Neither model download nor latest_model.py are available"
fi
fi
# Verify the downloaded file is not an error page
if grep -q "404: Not Found" "/db/model.py"; then
rm -f /db/model.py
if [ -f "latest_model.py" ]; then
mv latest_model.py /db/model.py || exit_with_error "Failed to move latest_model.py to /db/model.py"
else
exit_with_error "Neither model download nor latest_model.py are available"
fi
fi
if [ "$ONLY_UPDATE" -eq 0 ]; then
echo "🦃 Auto-generating the migration script to upgrade from $TAG to $NEXT_TAG"
# Generate the migration script
alembic revision --autogenerate -m "Upgrade to version $NEXT_TAG" --version-path versions || exit_with_error "Failed to create migration script for $DATABASE. Check alembic configuration or database connection."
# Set ownership for alembic directory (optional step)
if command -v chown &>/dev/null; then
echo "🔧 Setting ownership for alembic directory"
chown -R "$UID:$GID" versions || echo "⚠️ Failed to change ownership, continuing..."
else
echo "⚠️ 'chown' command not available, skipping ownership adjustment"
fi
# Apply the migration to update the database
echo "🔄 Applying the migration..."
alembic upgrade head || exit_with_error "Failed to apply the migration to the latest version"
echo "✅ Migration script created successfully"
else
# Apply the migration to update the database but only to the next version
echo "🔄 Applying the migration to the next version..."
alembic upgrade +1 || exit_with_error "Failed to apply the migration to the next version"
# Set ownership for alembic directory (optional step)
if command -v chown &>/dev/null; then
echo "🔧 Setting ownership for alembic directory"
chown -R "$UID:$GID" versions || echo "⚠️ Failed to change ownership, continuing..."
else
echo "⚠️ 'chown' command not available, skipping ownership adjustment"
fi
echo "✅ Successfully applied migration to the next version: $NEXT_TAG"
fi

View file

@ -0,0 +1,16 @@
services:
bw-db:
image: mariadb:11
environment:
MYSQL_RANDOM_ROOT_PASSWORD: "yes"
MYSQL_DATABASE: "db"
MYSQL_USER: "bunkerweb"
MYSQL_PASSWORD: "secret"
networks:
bw-db:
aliases:
- bw-db
networks:
bw-db:
name: bw-db

16
misc/migration/mysql.yml Normal file
View file

@ -0,0 +1,16 @@
services:
bw-db:
image: mysql:8
environment:
MYSQL_RANDOM_ROOT_PASSWORD: "yes"
MYSQL_DATABASE: "db"
MYSQL_USER: "bunkerweb"
MYSQL_PASSWORD: "secret"
networks:
bw-db:
aliases:
- bw-db
networks:
bw-db:
name: bw-db

View file

@ -0,0 +1,15 @@
services:
bw-db:
image: postgres:16-alpine
environment:
POSTGRES_USER: "bunkerweb"
POSTGRES_PASSWORD: "secret"
POSTGRES_DB: "db"
networks:
bw-db:
aliases:
- bw-db
networks:
bw-db:
name: bw-db

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,117 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
# Use forward slashes (/) also on windows to provide an os agnostic path
script_location = .
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
# for all available tokens
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
prepend_sys_path = .,/db
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python>=3.9 or backports.zoneinfo library.
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
# string value is passed to ZoneInfo()
# leave blank for localtime
# timezone =
# max length of characters to apply to the "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; This defaults
# to alembic_mariadb/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "version_path_separator" below.
version_locations = versions
# version path separator; As mentioned above, this is the character used to split
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
# Valid values for version_path_separator are:
#
# version_path_separator = :
# version_path_separator = ;
# version_path_separator = space
# version_path_separator = newline
version_path_separator = :
# set to 'true' to search source files recursively
# in each "version_locations" directory
# new in Alembic version 1.10
# recursive_version_locations = false
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
sqlalchemy.url =
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
# hooks = ruff
# ruff.type = exec
# ruff.executable = %(here)s/.venv/bin/ruff
# ruff.options = --fix REVISION_SCRIPT_FILENAME
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARNING
handlers = console
qualname =
[logger_sqlalchemy]
level = WARNING
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[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

View file

@ -0,0 +1,81 @@
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from sqlalchemy.schema import Identity
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.
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
from model import Base
target_metadata = Base.metadata
# Custom function to exclude Identity columns
def include_object(object, name, type_, reflected, compare_to):
if type_ == "column" and isinstance(object.server_default, Identity):
# Exclude Identity columns
return False
return True
def run_migrations_offline() -> None:
"""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=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
include_object=include_object, # Add include_object hook
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = engine_from_config(config.get_section(config.config_ini_section, {}), prefix="sqlalchemy.", poolclass=pool.NullPool)
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
include_object=include_object, # Add include_object hook
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

View file

@ -0,0 +1,29 @@
"""Upgrade to version 1.5.12
Revision ID: 0229aafe5e96
Revises: bf07e30a9b65
Create Date: 2024-12-17 15:06:45.746107
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision: str = "0229aafe5e96"
down_revision: Union[str, None] = "bf07e30a9b65"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Update version to 1.5.12
op.execute("UPDATE bw_metadata SET version = '1.5.12' WHERE id = 1")
def downgrade() -> None:
# Revert version to 1.5.11
op.execute("UPDATE bw_metadata SET version = '1.5.11' WHERE id = 1")

View file

@ -0,0 +1,62 @@
"""Upgrade to version 1.5.8
Revision ID: 0949ce7a3875
Revises: 30f52a4357a2
Create Date: 2024-12-17 15:06:33.277221
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision: str = "0949ce7a3875"
down_revision: Union[str, None] = "30f52a4357a2"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Add new columns to 'bw_metadata'
with op.batch_alter_table("bw_metadata") as batch_op:
batch_op.add_column(sa.Column("pro_license", sa.String(length=128), nullable=True))
batch_op.add_column(sa.Column("last_custom_configs_change", sa.DateTime(), nullable=True))
batch_op.add_column(sa.Column("last_external_plugins_change", sa.DateTime(), nullable=True))
batch_op.add_column(sa.Column("last_pro_plugins_change", sa.DateTime(), nullable=True))
batch_op.add_column(sa.Column("last_instances_change", sa.DateTime(), nullable=True))
batch_op.add_column(sa.Column("failover", sa.Boolean(), nullable=True))
batch_op.drop_column("config_changed")
# Add new columns to 'bw_plugins'
with op.batch_alter_table("bw_plugins") as batch_op:
batch_op.add_column(sa.Column("config_changed", sa.Boolean(), nullable=True))
batch_op.add_column(sa.Column("last_config_change", sa.DateTime(), nullable=True))
# Set default value for 'config_changed' in 'bw_plugins'
op.execute("UPDATE bw_plugins SET config_changed = false")
# Update version in 'bw_metadata'
op.execute("UPDATE bw_metadata SET version = '1.5.8' WHERE id = 1")
def downgrade() -> None:
# Revert 'bw_plugins' changes
with op.batch_alter_table("bw_plugins") as batch_op:
batch_op.drop_column("last_config_change")
batch_op.drop_column("config_changed")
# Revert 'bw_metadata' changes
with op.batch_alter_table("bw_metadata") as batch_op:
batch_op.add_column(sa.Column("config_changed", sa.Boolean(), nullable=True))
batch_op.drop_column("failover")
batch_op.drop_column("last_instances_change")
batch_op.drop_column("last_pro_plugins_change")
batch_op.drop_column("last_external_plugins_change")
batch_op.drop_column("last_custom_configs_change")
batch_op.drop_column("pro_license")
# Revert version in 'bw_metadata'
op.execute("UPDATE bw_metadata SET version = '1.5.7', config_changed = false WHERE id = 1")

View file

@ -0,0 +1,118 @@
"""Upgrade to version 1.5.6
Revision ID: 18e9d2191dcc
Revises: d4d8df48d14d
Create Date: 2024-12-17 10:19:55.586854
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision: str = "18e9d2191dcc"
down_revision: Union[str, None] = "d4d8df48d14d"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade():
# Drop the foreign key constraint with the correct name
with op.batch_alter_table("bw_jobs_cache") as batch_op:
batch_op.drop_constraint("bw_jobs_cache_ibfk_1", type_="foreignkey") # Replace with actual name
batch_op.drop_index("job_name")
batch_op.create_foreign_key("fk_bw_jobs_cache_job_name", "bw_jobs", ["job_name"], ["name"])
# Other migration operations
op.add_column("bw_metadata", sa.Column("is_pro", sa.Boolean(), nullable=False, server_default=sa.false()))
op.add_column("bw_metadata", sa.Column("pro_expire", sa.DateTime(), nullable=True))
op.add_column(
"bw_metadata",
sa.Column("pro_status", sa.Enum("active", "invalid", "expired", "suspended", name="pro_status_enum"), nullable=False, server_default="invalid"),
)
op.add_column("bw_metadata", sa.Column("pro_services", sa.Integer(), nullable=False, server_default="0"))
op.add_column("bw_metadata", sa.Column("pro_overlapped", sa.Boolean(), nullable=False, server_default=sa.false()))
op.add_column("bw_metadata", sa.Column("last_pro_check", sa.DateTime(), nullable=True))
op.add_column("bw_metadata", sa.Column("pro_plugins_changed", sa.Boolean(), nullable=True))
op.add_column("bw_services", sa.Column("is_draft", sa.Boolean(), nullable=False, server_default=sa.false()))
op.add_column("bw_plugins", sa.Column("type", sa.Enum("core", "external", "pro", name="plugin_types_enum"), nullable=False, server_default="core"))
op.alter_column(
"bw_plugins", "stream", existing_type=mysql.VARCHAR(length=16), type_=sa.Enum("no", "yes", "partial", name="stream_types_enum"), existing_nullable=False
)
# Migrate data: Set 'type' to 'external' where 'external' was true
op.execute(
"""
UPDATE bw_plugins
SET type = 'external'
WHERE external = true
"""
)
op.drop_column("bw_plugins", "external")
op.alter_column("bw_global_values", "value", existing_type=mysql.VARCHAR(length=8192), type_=sa.TEXT(), existing_nullable=False)
op.alter_column("bw_services_settings", "value", existing_type=mysql.VARCHAR(length=8192), type_=sa.TEXT(), existing_nullable=False)
op.drop_index("name", table_name="bw_jobs")
op.drop_index("name", table_name="bw_settings")
# Update all new columns and version in a single statement
op.execute(
"""
UPDATE bw_metadata
SET is_pro = false,
pro_status = 'invalid',
pro_services = 0,
pro_overlapped = false,
pro_plugins_changed = false,
version = '1.5.6'
WHERE id = 1
"""
)
def downgrade():
with op.batch_alter_table("bw_jobs_cache") as batch_op:
batch_op.drop_constraint("fk_bw_jobs_cache_job_name", type_="foreignkey")
batch_op.create_index("job_name", ["job_name", "service_id", "file_name"], unique=True)
batch_op.create_foreign_key("bw_jobs_cache_ibfk_1", "bw_jobs", ["job_name"], ["name"]) # Replace with actual name
op.create_index("name", "bw_jobs", ["name", "plugin_id"], unique=True)
op.create_index("name", "bw_settings", ["name"], unique=True)
op.alter_column("bw_global_values", "value", existing_type=sa.TEXT(), type_=mysql.VARCHAR(length=8192), existing_nullable=False)
op.alter_column("bw_services_settings", "value", existing_type=sa.TEXT(), type_=mysql.VARCHAR(length=8192), existing_nullable=False)
op.add_column("bw_plugins", sa.Column("external", mysql.TINYINT(display_width=1), autoincrement=False, nullable=False))
# Migrate data: Set 'external' to true where 'type' was 'external'
op.execute(
"""
UPDATE bw_plugins
SET external = true
WHERE type = 'external'
"""
)
op.drop_column("bw_plugins", "type")
op.alter_column(
"bw_plugins", "stream", existing_type=sa.Enum("no", "yes", "partial", name="stream_types_enum"), type_=mysql.VARCHAR(length=16), existing_nullable=False
)
op.drop_column("bw_services", "is_draft")
op.drop_column("bw_metadata", "pro_plugins_changed")
op.drop_column("bw_metadata", "last_pro_check")
op.drop_column("bw_metadata", "pro_overlapped")
op.drop_column("bw_metadata", "pro_services")
op.drop_column("bw_metadata", "pro_status")
op.drop_column("bw_metadata", "pro_expire")
op.drop_column("bw_metadata", "is_pro")
op.execute("UPDATE bw_metadata SET version = '1.5.5' WHERE id = 1")

View file

@ -0,0 +1,42 @@
"""Upgrade to version 1.5.4
Revision ID: 1cc06aa8335c
Revises: c38183e63472
Create Date: 2024-12-17 10:19:48.503973
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision: str = "1cc06aa8335c"
down_revision: Union[str, None] = "c38183e63472"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Create new table 'bw_ui_users'
op.create_table(
"bw_ui_users",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("username", sa.String(length=256), nullable=False),
sa.Column("password", sa.String(length=60), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("username"),
)
# Update metadata version
op.execute("UPDATE bw_metadata SET version = '1.5.4' WHERE id = 1")
def downgrade() -> None:
# Drop the table 'bw_ui_users'
op.drop_table("bw_ui_users")
# Revert metadata version
op.execute("UPDATE bw_metadata SET version = '1.5.3' WHERE id = 1")

View file

@ -0,0 +1,29 @@
"""Upgrade to version 1.5.2
Revision ID: 24e08b364fa1
Revises: cc61497f1976
Create Date: 2024-12-17 10:19:39.901086
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision: str = "24e08b364fa1"
down_revision: Union[str, None] = "cc61497f1976"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Data migration: Update the version to 1.5.2
op.execute("UPDATE bw_metadata SET version = '1.5.2' WHERE id = 1")
def downgrade() -> None:
# Revert the version back to 1.5.1
op.execute("UPDATE bw_metadata SET version = '1.5.1' WHERE id = 1")

View file

@ -0,0 +1,75 @@
"""Upgrade to version 1.5.7
Revision ID: 30f52a4357a2
Revises: 18e9d2191dcc
Create Date: 2024-12-17 10:19:55.586854
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision: str = "30f52a4357a2"
down_revision: Union[str, None] = "18e9d2191dcc"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Create new table for bw_cli_commands
op.create_table(
"bw_cli_commands",
sa.Column("id", sa.Integer(), sa.Identity(always=False, start=1, increment=1), nullable=False),
sa.Column("name", sa.String(length=64), nullable=False),
sa.Column("plugin_id", sa.String(length=64), nullable=False),
sa.Column("file_name", sa.String(length=256), nullable=False),
sa.ForeignKeyConstraint(["plugin_id"], ["bw_plugins.id"], onupdate="cascade", ondelete="cascade"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("plugin_id", "name"),
)
# Handle foreign key constraints for bw_jobs_cache
op.drop_constraint("fk_bw_jobs_cache_job_name", "bw_jobs_cache", type_="foreignkey")
op.create_foreign_key(None, "bw_jobs_cache", "bw_jobs", ["job_name"], ["name"], onupdate="cascade", ondelete="cascade")
# Add new columns to bw_plugin_pages
op.add_column(
"bw_plugin_pages",
sa.Column("obfuscation_file", mysql.LONGBLOB(), nullable=True),
)
op.add_column(
"bw_plugin_pages",
sa.Column("obfuscation_checksum", sa.String(length=128), nullable=True),
)
# Add the new order column to bw_settings
op.add_column("bw_settings", sa.Column("order", sa.Integer(), nullable=False))
# Update version in bw_metadata
op.execute("UPDATE bw_metadata SET version = '1.5.7' WHERE id = 1")
def downgrade() -> None:
# Reverse the version update
op.execute("UPDATE bw_metadata SET version = '1.5.6' WHERE id = 1")
# Reverse the addition of the order column
op.drop_column("bw_settings", "order")
# Reverse changes in bw_plugin_pages
op.drop_column("bw_plugin_pages", "obfuscation_checksum")
op.drop_column("bw_plugin_pages", "obfuscation_file")
# Restore foreign key constraints for bw_jobs_cache
op.drop_constraint(None, "bw_jobs_cache", type_="foreignkey")
op.create_foreign_key("fk_bw_jobs_cache_job_name", "bw_jobs_cache", "bw_jobs", ["job_name"], ["name"])
# Drop the newly created table bw_cli_commands
op.drop_table("bw_cli_commands")
# Revert the version update
op.execute("UPDATE bw_metadata SET version = '1.5.6' WHERE id = 1")

View file

@ -0,0 +1,29 @@
"""Upgrade to version 1.5.9
Revision ID: 392ec43997fd
Revises: 0949ce7a3875
Create Date: 2024-12-17 15:06:36.162645
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision: str = "392ec43997fd"
down_revision: Union[str, None] = "0949ce7a3875"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Update version in 'bw_metadata'
op.execute("UPDATE bw_metadata SET version = '1.5.9' WHERE id = 1")
def downgrade() -> None:
# Revert version in 'bw_metadata'
op.execute("UPDATE bw_metadata SET version = '1.5.8' WHERE id = 1")

View file

@ -0,0 +1,29 @@
"""Upgrade to version 1.5.10
Revision ID: 431f4dea783e
Revises: 392ec43997fd
Create Date: 2024-12-17 15:06:39.416494
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision: str = "431f4dea783e"
down_revision: Union[str, None] = "392ec43997fd"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Update version in 'bw_metadata'
op.execute("UPDATE bw_metadata SET version = '1.5.10' WHERE id = 1")
def downgrade() -> None:
# Revert version in 'bw_metadata' back to 1.5.9
op.execute("UPDATE bw_metadata SET version = '1.5.9' WHERE id = 1")

View file

@ -0,0 +1,100 @@
"""Upgrade to version 1.6.0-rc1
Revision ID: 8a37fed772e9
Revises: bfa7869e34c3
Create Date: 2024-12-20 10:19:30.469285
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision: str = "8a37fed772e9"
down_revision: Union[str, None] = "bfa7869e34c3"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Drop foreign keys referencing service_id columns before altering them
# Replace these FK names with the actual names in your schema
with op.batch_alter_table("bw_custom_configs") as batch_op:
batch_op.drop_constraint("bw_custom_configs_ibfk_1", type_="foreignkey")
with op.batch_alter_table("bw_jobs_cache") as batch_op:
batch_op.drop_constraint("bw_jobs_cache_ibfk_2", type_="foreignkey")
with op.batch_alter_table("bw_services_settings") as batch_op:
batch_op.drop_constraint("bw_services_settings_ibfk_1", type_="foreignkey")
# Alter columns now that foreign keys are dropped
op.alter_column("bw_custom_configs", "service_id", existing_type=mysql.VARCHAR(length=64), type_=sa.String(length=256), existing_nullable=True)
op.alter_column("bw_jobs_cache", "service_id", existing_type=mysql.VARCHAR(length=64), type_=sa.String(length=256), existing_nullable=True)
op.alter_column("bw_services", "id", existing_type=mysql.VARCHAR(length=64), type_=sa.String(length=256), existing_nullable=False)
op.alter_column("bw_services_settings", "service_id", existing_type=mysql.VARCHAR(length=64), type_=sa.String(length=256), existing_nullable=False)
# After altering, recreate the foreign keys with updated references
with op.batch_alter_table("bw_custom_configs") as batch_op:
batch_op.create_foreign_key("bw_custom_configs_ibfk_1", "bw_services", ["service_id"], ["id"], onupdate="CASCADE", ondelete="CASCADE")
with op.batch_alter_table("bw_jobs_cache") as batch_op:
batch_op.create_foreign_key("bw_jobs_cache_ibfk_2", "bw_services", ["service_id"], ["id"], onupdate="CASCADE", ondelete="CASCADE")
with op.batch_alter_table("bw_services_settings") as batch_op:
batch_op.create_foreign_key("bw_services_settings_ibfk_1", "bw_services", ["service_id"], ["id"], onupdate="CASCADE", ondelete="CASCADE")
# Update bw_settings.default from String(4096) to TEXT
op.alter_column("bw_settings", "default", existing_type=mysql.VARCHAR(length=4096), type_=sa.TEXT(), existing_nullable=True)
# Drop bw_ui_users.id column
op.drop_column("bw_ui_users", "id")
# Update version in bw_metadata
op.execute("UPDATE bw_metadata SET version = '1.6.0-rc1' WHERE id = 1")
def downgrade() -> None:
# Reverse the changes:
# 1. Drop foreign keys
# 2. Revert column types
# 3. Recreate foreign keys
# 4. Revert version
# 5. Re-add bw_ui_users.id column
with op.batch_alter_table("bw_custom_configs") as batch_op:
batch_op.drop_constraint("bw_custom_configs_ibfk_1", type_="foreignkey")
with op.batch_alter_table("bw_jobs_cache") as batch_op:
batch_op.drop_constraint("bw_jobs_cache_ibfk_1", type_="foreignkey")
with op.batch_alter_table("bw_services_settings") as batch_op:
batch_op.drop_constraint("bw_services_settings_ibfk_1", type_="foreignkey")
op.alter_column("bw_services_settings", "service_id", existing_type=sa.String(length=256), type_=mysql.VARCHAR(length=64), existing_nullable=False)
op.alter_column("bw_services", "id", existing_type=sa.String(length=256), type_=mysql.VARCHAR(length=64), existing_nullable=False)
op.alter_column("bw_jobs_cache", "service_id", existing_type=sa.String(length=256), type_=mysql.VARCHAR(length=64), existing_nullable=True)
op.alter_column("bw_custom_configs", "service_id", existing_type=sa.String(length=256), type_=mysql.VARCHAR(length=64), existing_nullable=True)
# Recreate foreign keys with old definitions
with op.batch_alter_table("bw_custom_configs") as batch_op:
batch_op.create_foreign_key("bw_custom_configs_ibfk_1", "bw_services", ["service_id"], ["id"], onupdate="CASCADE", ondelete="CASCADE")
with op.batch_alter_table("bw_jobs_cache") as batch_op:
batch_op.create_foreign_key("bw_jobs_cache_ibfk_1", "bw_services", ["service_id"], ["id"], onupdate="CASCADE", ondelete="CASCADE")
with op.batch_alter_table("bw_services_settings") as batch_op:
batch_op.create_foreign_key("bw_services_settings_ibfk_1", "bw_services", ["service_id"], ["id"], onupdate="CASCADE", ondelete="CASCADE")
op.alter_column("bw_settings", "default", existing_type=sa.TEXT(), type_=mysql.VARCHAR(length=4096), existing_nullable=True)
# Re-add bw_ui_users.id and index on username
op.add_column("bw_ui_users", sa.Column("id", sa.Integer(), autoincrement=True, nullable=False))
# Revert version
op.execute("UPDATE bw_metadata SET version = '1.6.0-beta' WHERE id = 1")

View file

@ -0,0 +1,37 @@
"""Upgrade to version 1.5.0
Revision ID: b46c7ecfba26
Revises:
Create Date: 2024-12-17 10:19:30.419613
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision: str = "b46c7ecfba26"
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Step 1: Drop 'order' column from 'bw_plugins'
with op.batch_alter_table("bw_plugins") as batch_op:
batch_op.drop_column("order")
# Data migration: Update the version to 1.5.0
op.execute("UPDATE bw_metadata SET version = '1.5.0' WHERE id = 1")
def downgrade() -> None:
# Re-add the 'order' column to the 'bw_plugins' table
with op.batch_alter_table("bw_plugins") as batch_op:
batch_op.add_column(sa.Column("order", mysql.INTEGER(display_width=11), autoincrement=False, nullable=False))
# Data migration: Update the version to 1.5.0-beta
op.execute("UPDATE bw_metadata SET version = '1.5.0-beta' WHERE id = 1")

View file

@ -0,0 +1,35 @@
"""Upgrade to version 1.5.11
Revision ID: bf07e30a9b65
Revises: 431f4dea783e
Create Date: 2024-12-17 15:06:42.631670
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision: str = "bf07e30a9b65"
down_revision: Union[str, None] = "431f4dea783e"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Add the new non_draft_services column to bw_metadata
op.add_column("bw_metadata", sa.Column("non_draft_services", sa.Integer(), nullable=False, server_default="0"))
# Update the version in metadata
op.execute("UPDATE bw_metadata SET version = '1.5.11' WHERE id = 1")
def downgrade() -> None:
# Reverse the addition of the non_draft_services column
op.drop_column("bw_metadata", "non_draft_services")
# Revert version to 1.5.10
op.execute("UPDATE bw_metadata SET version = '1.5.10' WHERE id = 1")

View file

@ -0,0 +1,439 @@
"""Upgrade to version 1.6.0-beta
Revision ID: bfa7869e34c3
Revises: 0229aafe5e96
Create Date: 2024-12-17 15:06:49.235416
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision: str = "bfa7869e34c3"
down_revision: Union[str, None] = "0229aafe5e96"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade():
# Define old and new ENUMs for methods_enum (adding "wizard")
old_methods_enum = sa.Enum("ui", "scheduler", "autoconf", "manual", name="methods_enum")
new_methods_enum = sa.Enum("ui", "scheduler", "autoconf", "manual", "wizard", name="methods_enum")
# Alter columns that use methods_enum
op.alter_column("bw_plugins", "method", existing_type=old_methods_enum, type_=new_methods_enum, existing_nullable=False, existing_server_default="manual")
op.alter_column("bw_services", "method", existing_type=old_methods_enum, type_=new_methods_enum, existing_nullable=False)
op.alter_column("bw_custom_configs", "method", existing_type=old_methods_enum, type_=new_methods_enum, existing_nullable=False)
op.alter_column("bw_ui_users", "method", existing_type=old_methods_enum, type_=new_methods_enum, existing_nullable=False, existing_server_default="manual")
# Update custom_configs_types_enum (adding "default_server_stream", "crs_plugins_before", "crs_plugins_after")
old_cct_enum = sa.Enum("http", "stream", "server_http", "server_stream", "default_server_http", "modsec", "modsec_crs", name="custom_configs_types_enum")
new_cct_enum = sa.Enum(
"http",
"stream",
"server_http",
"server_stream",
"default_server_http",
"default_server_stream",
"modsec",
"modsec_crs",
"crs_plugins_before",
"crs_plugins_after",
name="custom_configs_types_enum",
)
op.alter_column("bw_custom_configs", "type", existing_type=old_cct_enum, type_=new_cct_enum, existing_nullable=False)
# Update plugin_types_enum (adding "ui")
old_pt_enum = sa.Enum("core", "external", "pro", name="plugin_types_enum")
new_pt_enum = sa.Enum("core", "external", "ui", "pro", name="plugin_types_enum")
op.alter_column("bw_plugins", "type", existing_type=old_pt_enum, type_=new_pt_enum, existing_nullable=False, existing_server_default="core")
# Create new UI tables
op.create_table(
"bw_ui_permissions",
sa.Column("name", sa.String(64), primary_key=True),
)
op.create_table(
"bw_ui_roles",
sa.Column("name", sa.String(64), primary_key=True),
sa.Column("description", sa.String(256), nullable=False),
sa.Column("update_datetime", sa.DateTime(timezone=True), nullable=False),
)
op.create_table(
"bw_ui_user_recovery_codes",
sa.Column("id", sa.Integer(), sa.Identity(start=1, increment=1), primary_key=True),
sa.Column("user_name", sa.String(256), nullable=False),
sa.Column("code", sa.UnicodeText, nullable=False),
sa.ForeignKeyConstraint(["user_name"], ["bw_ui_users.username"], onupdate="CASCADE", ondelete="CASCADE"),
)
op.create_table(
"bw_ui_roles_permissions",
sa.Column("role_name", sa.String(64), nullable=False),
sa.Column("permission_name", sa.String(64), nullable=False),
sa.ForeignKeyConstraint(["role_name"], ["bw_ui_roles.name"], onupdate="CASCADE", ondelete="CASCADE"),
sa.ForeignKeyConstraint(["permission_name"], ["bw_ui_permissions.name"], onupdate="CASCADE", ondelete="CASCADE"),
sa.PrimaryKeyConstraint("role_name", "permission_name"),
)
op.create_table(
"bw_ui_roles_users",
sa.Column("user_name", sa.String(256), nullable=False),
sa.Column("role_name", sa.String(64), nullable=False),
sa.ForeignKeyConstraint(["user_name"], ["bw_ui_users.username"], onupdate="CASCADE", ondelete="CASCADE"),
sa.ForeignKeyConstraint(["role_name"], ["bw_ui_roles.name"], onupdate="CASCADE", ondelete="CASCADE"),
sa.PrimaryKeyConstraint("user_name", "role_name"),
)
op.create_table(
"bw_ui_user_sessions",
sa.Column("id", sa.Integer(), sa.Identity(start=1, increment=1), primary_key=True),
sa.Column("user_name", sa.String(256), nullable=False),
sa.Column("ip", sa.String(39), nullable=False),
sa.Column("user_agent", sa.TEXT, nullable=False),
sa.Column("creation_date", sa.DateTime(timezone=True), nullable=False),
sa.Column("last_activity", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["user_name"], ["bw_ui_users.username"], onupdate="CASCADE", ondelete="CASCADE"),
)
from model import JSONText
op.create_table(
"bw_ui_user_columns_preferences",
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
sa.Column("user_name", sa.String(256), nullable=False),
sa.Column("table_name", sa.Enum("bans", "configs", "instances", "jobs", "plugins", "reports", "services", name="tables_enum"), nullable=False),
sa.Column("columns", JSONText, nullable=False),
sa.ForeignKeyConstraint(["user_name"], ["bw_ui_users.username"], onupdate="CASCADE", ondelete="CASCADE"),
sa.UniqueConstraint("user_name", "table_name", name="uq_user_columns_preferences"),
)
# Templates tables
op.create_table(
"bw_templates",
sa.Column("id", sa.String(256), primary_key=True),
sa.Column("name", sa.String(256), unique=True, nullable=False),
sa.Column("plugin_id", sa.String(64), nullable=False),
sa.ForeignKeyConstraint(["plugin_id"], ["bw_plugins.id"], onupdate="CASCADE", ondelete="CASCADE"),
)
op.create_table(
"bw_template_steps",
sa.Column("id", sa.Integer(), primary_key=True),
sa.Column("template_id", sa.String(256), nullable=False, primary_key=True),
sa.Column("title", sa.TEXT, nullable=False),
sa.Column("subtitle", sa.TEXT, nullable=True),
sa.ForeignKeyConstraint(["template_id"], ["bw_templates.id"], onupdate="CASCADE", ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id", "template_id"),
)
op.create_table(
"bw_template_settings",
sa.Column("id", sa.Integer(), sa.Identity(start=1, increment=1), primary_key=True),
sa.Column("template_id", sa.String(256), nullable=False),
sa.Column("setting_id", sa.String(256), nullable=False),
sa.Column("step_id", sa.Integer(), nullable=True),
sa.Column("default", sa.TEXT, nullable=False),
sa.Column("suffix", sa.Integer(), nullable=True, default=0),
sa.ForeignKeyConstraint(["template_id"], ["bw_templates.id"], onupdate="CASCADE", ondelete="CASCADE"),
sa.ForeignKeyConstraint(["setting_id"], ["bw_settings.id"], onupdate="CASCADE", ondelete="CASCADE"),
sa.UniqueConstraint("template_id", "setting_id", "step_id", "suffix"),
)
op.create_table(
"bw_template_custom_configs",
sa.Column("id", sa.Integer(), sa.Identity(start=1, increment=1), primary_key=True),
sa.Column("template_id", sa.String(256), nullable=False),
sa.Column("step_id", sa.Integer(), nullable=True),
sa.Column("type", new_cct_enum, nullable=False),
sa.Column("name", sa.String(256), nullable=False),
sa.Column("data", sa.LargeBinary(length=(2**32) - 1), nullable=False),
sa.Column("checksum", sa.String(128), nullable=False),
sa.ForeignKeyConstraint(["template_id"], ["bw_templates.id"], onupdate="CASCADE", ondelete="CASCADE"),
sa.UniqueConstraint("template_id", "step_id", "type", "name"),
)
# bw_jobs changes (add run_async, remove success and last_run)
with op.batch_alter_table("bw_jobs") as batch_op:
batch_op.add_column(sa.Column("run_async", sa.Boolean(), nullable=False, server_default="0"))
batch_op.drop_column("success")
batch_op.drop_column("last_run")
op.create_table(
"bw_jobs_runs",
sa.Column("id", sa.Integer(), sa.Identity(start=1, increment=1), primary_key=True),
sa.Column("job_name", sa.String(128), nullable=False),
sa.Column("success", sa.Boolean(), nullable=True, server_default="0"),
sa.Column("start_date", sa.DateTime(timezone=True), nullable=False),
sa.Column("end_date", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["job_name"], ["bw_jobs.name"], onupdate="CASCADE", ondelete="CASCADE"),
)
# bw_services add creation_date, last_update
with op.batch_alter_table("bw_services") as batch_op:
batch_op.add_column(sa.Column("creation_date", sa.DateTime(timezone=True), nullable=True))
batch_op.add_column(sa.Column("last_update", sa.DateTime(timezone=True), nullable=True))
# Set current timestamp for existing services data
op.execute("UPDATE bw_services SET creation_date = CURRENT_TIMESTAMP, last_update = CURRENT_TIMESTAMP")
with op.batch_alter_table("bw_services") as batch_op:
batch_op.alter_column("creation_date", existing_type=sa.DateTime(timezone=True), nullable=False)
batch_op.alter_column("last_update", existing_type=sa.DateTime(timezone=True), nullable=False)
# bw_ui_users changes
with op.batch_alter_table("bw_ui_users") as batch_op:
batch_op.add_column(sa.Column("email", sa.String(256), nullable=True, unique=True))
batch_op.add_column(sa.Column("admin", sa.Boolean(), nullable=False, server_default="0"))
batch_op.add_column(sa.Column("theme", sa.Enum("light", "dark", name="themes_enum"), nullable=False, server_default="light"))
batch_op.add_column(sa.Column("totp_secret", sa.String(256), nullable=True))
batch_op.add_column(sa.Column("creation_date", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.current_timestamp()))
batch_op.add_column(sa.Column("update_date", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.current_timestamp()))
batch_op.drop_column("is_two_factor_enabled")
batch_op.drop_column("secret_token")
# Set defaults for existing bw_ui_users rows
op.execute("UPDATE bw_ui_users SET admin=1, theme='light', creation_date=CURRENT_TIMESTAMP, update_date=CURRENT_TIMESTAMP")
# bw_plugin_pages changes
with op.batch_alter_table("bw_plugin_pages") as batch_op:
batch_op.add_column(sa.Column("data", sa.LargeBinary(length=(2**32) - 1), nullable=False))
batch_op.add_column(sa.Column("checksum", sa.String(128), nullable=False))
# Add old data to new columns
op.execute(
"""
UPDATE bw_plugin_pages
SET data = template_file, checksum = template_checksum
WHERE template_file IS NOT NULL
"""
)
with op.batch_alter_table("bw_plugin_pages") as batch_op:
batch_op.drop_column("template_file")
batch_op.drop_column("template_checksum")
batch_op.drop_column("actions_file")
batch_op.drop_column("actions_checksum")
batch_op.drop_column("obfuscation_file")
batch_op.drop_column("obfuscation_checksum")
# bw_instances changes
with op.batch_alter_table("bw_instances") as batch_op:
batch_op.add_column(sa.Column("name", sa.String(256), nullable=True))
# Add the 'type' column with the old enum definition (or no enum yet if it didnt exist before)
# For instance, if previously there was no enum, start with the initial values:
old_it_enum = sa.Enum("static", name="instance_type_enum") # The original value set
batch_op.add_column(sa.Column("type", old_it_enum, nullable=True))
# Similarly add the 'status', 'method', 'creation_date', 'last_seen' columns
old_is_enum = sa.Enum("loading", "up", name="instance_status_enum")
batch_op.add_column(sa.Column("status", old_is_enum, nullable=True))
old_methods_enum = sa.Enum("ui", "scheduler", "autoconf", "manual", name="methods_enum")
batch_op.add_column(sa.Column("method", old_methods_enum, nullable=True))
batch_op.add_column(sa.Column("creation_date", sa.DateTime(timezone=True), nullable=True))
batch_op.add_column(sa.Column("last_seen", sa.DateTime(timezone=True), nullable=True))
op.execute("DELETE FROM bw_instances WHERE name IS NULL")
# Now that columns exist and have values, alter their definitions if needed:
new_it_enum = sa.Enum("static", "container", "pod", name="instance_type_enum")
op.alter_column("bw_instances", "type", existing_type=old_it_enum, type_=new_it_enum, existing_nullable=False, existing_server_default="static")
new_is_enum = sa.Enum("loading", "up", "down", name="instance_status_enum")
op.alter_column("bw_instances", "status", existing_type=old_is_enum, type_=new_is_enum, existing_nullable=False, existing_server_default="loading")
# Similarly alter the method column to the new enum with 'wizard' if needed:
extended_methods_enum = sa.Enum("ui", "scheduler", "autoconf", "manual", "wizard", name="methods_enum")
op.alter_column(
"bw_instances", "method", existing_type=old_methods_enum, type_=extended_methods_enum, existing_nullable=False, existing_server_default="manual"
)
# Finally, now set them to NOT NULL
with op.batch_alter_table("bw_instances") as batch_op:
batch_op.alter_column("name", existing_type=sa.String(256), nullable=False)
batch_op.alter_column("type", existing_type=new_it_enum, nullable=False)
batch_op.alter_column("status", existing_type=new_is_enum, nullable=False)
batch_op.alter_column("method", existing_type=extended_methods_enum, nullable=False)
batch_op.alter_column("creation_date", existing_type=sa.DateTime(timezone=True), nullable=False)
batch_op.alter_column("last_seen", existing_type=sa.DateTime(timezone=True), nullable=False)
# Update version
op.execute("UPDATE bw_metadata SET version = '1.6.0-beta' WHERE id = 1")
def downgrade():
# Revert enums to their old definitions by altering columns back
old_methods_enum = sa.Enum("ui", "scheduler", "autoconf", "manual", name="methods_enum")
# methods_enum had "wizard" added, now remove it
op.alter_column(
"bw_plugins",
"method",
existing_type=sa.Enum("ui", "scheduler", "autoconf", "manual", "wizard", name="methods_enum"),
type_=old_methods_enum,
existing_nullable=False,
existing_server_default="manual",
)
op.alter_column(
"bw_services",
"method",
existing_type=sa.Enum("ui", "scheduler", "autoconf", "manual", "wizard", name="methods_enum"),
type_=old_methods_enum,
existing_nullable=False,
)
op.alter_column(
"bw_custom_configs",
"method",
existing_type=sa.Enum("ui", "scheduler", "autoconf", "manual", "wizard", name="methods_enum"),
type_=old_methods_enum,
existing_nullable=False,
)
op.alter_column(
"bw_ui_users",
"method",
existing_type=sa.Enum("ui", "scheduler", "autoconf", "manual", "wizard", name="methods_enum"),
type_=old_methods_enum,
existing_nullable=False,
existing_server_default="manual",
)
# custom_configs_types_enum remove "default_server_stream", "crs_plugins_before", "crs_plugins_after"
old_cct_enum = sa.Enum("http", "default_server_http", "server_http", "modsec", "modsec_crs", "stream", "server_stream", name="custom_configs_types_enum")
op.alter_column(
"bw_custom_configs",
"type",
existing_type=sa.Enum(
"http",
"stream",
"server_http",
"server_stream",
"default_server_http",
"default_server_stream",
"modsec",
"modsec_crs",
"crs_plugins_before",
"crs_plugins_after",
name="custom_configs_types_enum",
),
type_=old_cct_enum,
existing_nullable=False,
)
# plugin_types_enum remove "ui"
old_pt_enum = sa.Enum("core", "external", "pro", name="plugin_types_enum")
op.alter_column(
"bw_plugins",
"type",
existing_type=sa.Enum("core", "external", "ui", "pro", name="plugin_types_enum"),
type_=old_pt_enum,
existing_nullable=False,
existing_server_default="core",
)
# instance_type_enum revert to just "static"
old_it_enum = sa.Enum("static", name="instance_type_enum")
op.alter_column(
"bw_instances",
"type",
existing_type=sa.Enum("static", "container", "pod", name="instance_type_enum"),
type_=old_it_enum,
existing_nullable=False,
existing_server_default="static",
)
# instance_status_enum revert to "loading", "up"
old_is_enum = sa.Enum("loading", "up", name="instance_status_enum")
op.alter_column(
"bw_instances",
"status",
existing_type=sa.Enum("loading", "up", "down", name="instance_status_enum"),
type_=old_is_enum,
existing_nullable=False,
existing_server_default="loading",
)
# Drop newly created UI and templates tables
op.drop_table("bw_template_custom_configs")
op.drop_table("bw_template_settings")
op.drop_table("bw_template_steps")
op.drop_table("bw_templates")
op.drop_table("bw_ui_user_columns_preferences")
op.drop_table("bw_ui_user_sessions")
op.drop_table("bw_ui_roles_users")
op.drop_table("bw_ui_roles_permissions")
op.drop_table("bw_ui_user_recovery_codes")
op.drop_table("bw_ui_roles")
op.drop_table("bw_ui_permissions")
# Revert bw_settings constraints: old primary key was (id,name) and there was a unique constraint on id
op.drop_constraint("bw_settings_name_key", "bw_settings", type_="unique")
op.drop_constraint("bw_settings_pkey", "bw_settings", type_="primary")
# Recreate old constraints:
# PrimaryKeyConstraint("id", "name"), UniqueConstraint("id")
# Note: The original primary key and unique constraints must be re-added as they were initially.
op.create_primary_key("bw_settings_pkey", "bw_settings", ["id", "name"])
op.create_unique_constraint("id", "bw_settings", ["id"])
# bw_jobs revert: drop run_async, add success and last_run
with op.batch_alter_table("bw_jobs") as batch_op:
batch_op.drop_column("run_async")
batch_op.add_column(sa.Column("success", sa.Boolean(), nullable=True))
batch_op.add_column(sa.Column("last_run", sa.DateTime(), nullable=True))
# Drop bw_jobs_runs table
op.drop_table("bw_jobs_runs")
# bw_services revert: remove creation_date, last_update
with op.batch_alter_table("bw_services") as batch_op:
batch_op.drop_column("last_update")
batch_op.drop_column("creation_date")
# bw_ui_users revert: drop email, admin, theme, totp_secret, creation_date, update_date
# add back is_two_factor_enabled, secret_token
with op.batch_alter_table("bw_ui_users") as batch_op:
batch_op.drop_column("email")
batch_op.drop_column("admin")
batch_op.drop_column("theme")
batch_op.drop_column("totp_secret")
batch_op.drop_column("creation_date")
batch_op.drop_column("update_date")
batch_op.add_column(sa.Column("is_two_factor_enabled", sa.Boolean, nullable=False, server_default="0"))
batch_op.add_column(sa.Column("secret_token", sa.String(32), nullable=True, unique=True, default=None))
# bw_plugin_pages revert: remove data, checksum, add template_file, template_checksum, actions_file, actions_checksum, obfuscation_file, obfuscation_checksum
with op.batch_alter_table("bw_plugin_pages") as batch_op:
batch_op.drop_column("data")
batch_op.drop_column("checksum")
batch_op.add_column(sa.Column("template_file", sa.LargeBinary(length=(2**32) - 1), nullable=False))
batch_op.add_column(sa.Column("template_checksum", sa.String(128), nullable=False))
batch_op.add_column(sa.Column("actions_file", sa.LargeBinary(length=(2**32) - 1), nullable=False))
batch_op.add_column(sa.Column("actions_checksum", sa.String(128), nullable=False))
batch_op.add_column(sa.Column("obfuscation_file", sa.LargeBinary(length=(2**32) - 1), nullable=True))
batch_op.add_column(sa.Column("obfuscation_checksum", sa.String(128), nullable=True))
# bw_instances revert: drop name, type, status, method, creation_date, last_seen
with op.batch_alter_table("bw_instances") as batch_op:
batch_op.drop_column("last_seen")
batch_op.drop_column("creation_date")
batch_op.drop_column("method")
batch_op.drop_column("status")
batch_op.drop_column("type")
batch_op.drop_column("name")
# Revert version
op.execute("UPDATE bw_metadata SET version = '1.5.12' WHERE id = 1")

View file

@ -0,0 +1,29 @@
"""Upgrade to version 1.5.3
Revision ID: c38183e63472
Revises: 24e08b364fa1
Create Date: 2024-12-17 10:19:43.986783
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision: str = "c38183e63472"
down_revision: Union[str, None] = "24e08b364fa1"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Data migration: Update the version to 1.5.3
op.execute("UPDATE bw_metadata SET version = '1.5.3' WHERE id = 1")
def downgrade() -> None:
# Revert the version back to 1.5.2
op.execute("UPDATE bw_metadata SET version = '1.5.2' WHERE id = 1")

View file

@ -0,0 +1,56 @@
"""Upgrade to version 1.5.1
Revision ID: cc61497f1976
Revises: b46c7ecfba26
Create Date: 2024-12-17 10:19:35.186344
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision: str = "cc61497f1976"
down_revision: Union[str, None] = "b46c7ecfba26"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Add new columns to bw_metadata."""
with op.batch_alter_table("bw_metadata") as batch_op:
batch_op.add_column(sa.Column("scheduler_first_start", sa.Boolean(), nullable=True))
batch_op.add_column(sa.Column("custom_configs_changed", sa.Boolean(), nullable=True))
batch_op.add_column(sa.Column("external_plugins_changed", sa.Boolean(), nullable=True))
batch_op.add_column(sa.Column("config_changed", sa.Boolean(), nullable=True))
batch_op.add_column(sa.Column("instances_changed", sa.Boolean(), nullable=True))
# Update all new columns and version in a single statement
op.execute(
"""
UPDATE bw_metadata
SET scheduler_first_start = false,
custom_configs_changed = false,
external_plugins_changed = false,
config_changed = false,
instances_changed = false,
version = '1.5.1'
WHERE id = 1
"""
)
def downgrade() -> None:
"""Remove new columns from bw_metadata."""
with op.batch_alter_table("bw_metadata") as batch_op:
batch_op.drop_column("instances_changed")
batch_op.drop_column("config_changed")
batch_op.drop_column("external_plugins_changed")
batch_op.drop_column("custom_configs_changed")
batch_op.drop_column("scheduler_first_start")
# Revert the version back to 1.5.0
op.execute("UPDATE bw_metadata SET version = '1.5.0' WHERE id = 1")

View file

@ -0,0 +1,54 @@
"""Upgrade to version 1.5.5
Revision ID: d4d8df48d14d
Revises: 1cc06aa8335c
Create Date: 2024-12-17 10:19:52.815063
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision: str = "d4d8df48d14d"
down_revision: Union[str, None] = "1cc06aa8335c"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade():
# Add new columns to bw_ui_users
op.add_column("bw_ui_users", sa.Column("is_two_factor_enabled", sa.Boolean(), nullable=False, server_default=sa.false()))
op.add_column("bw_ui_users", sa.Column("secret_token", sa.String(length=32), nullable=True))
op.add_column(
"bw_ui_users", sa.Column("method", sa.Enum("ui", "scheduler", "autoconf", "manual", name="methods_enum"), nullable=False, server_default="manual")
)
op.create_unique_constraint("uq_bw_ui_users_secret_token", "bw_ui_users", ["secret_token"])
# Increase column sizes
op.alter_column("bw_global_values", "value", existing_type=mysql.VARCHAR(length=4096), type_=sa.String(length=8192), existing_nullable=False)
op.alter_column("bw_services_settings", "value", existing_type=mysql.VARCHAR(length=4096), type_=sa.String(length=8192), existing_nullable=False)
# Update all new columns in a single statement
op.execute("UPDATE bw_ui_users SET is_two_factor_enabled = false, method = 'manual'")
# Update version in bw_metadata
op.execute("UPDATE bw_metadata SET version = '1.5.5' WHERE id = 1")
def downgrade():
# Revert changes to 'bw_ui_users'
op.drop_constraint("uq_bw_ui_users_secret_token", "bw_ui_users", type_="unique")
op.drop_column("bw_ui_users", "method")
op.drop_column("bw_ui_users", "secret_token")
op.drop_column("bw_ui_users", "is_two_factor_enabled")
# Revert column sizes for VARCHAR
op.alter_column("bw_global_values", "value", existing_type=sa.String(length=8192), type_=mysql.VARCHAR(length=4096), existing_nullable=False)
op.alter_column("bw_services_settings", "value", existing_type=sa.String(length=8192), type_=mysql.VARCHAR(length=4096), existing_nullable=False)
# Revert version in bw_metadata
op.execute("UPDATE bw_metadata SET version = '1.5.4' WHERE id = 1")

View file

@ -0,0 +1,118 @@
"""Upgrade to version 1.5.6
Revision ID: 021e3123e517
Revises: 0903238e095e
Create Date: 2024-12-19 13:23:02.905461
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision: str = "021e3123e517"
down_revision: Union[str, None] = "0903238e095e"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade():
# Drop the foreign key constraint with the correct name
with op.batch_alter_table("bw_jobs_cache") as batch_op:
batch_op.drop_constraint("bw_jobs_cache_ibfk_1", type_="foreignkey") # Replace with actual name
batch_op.drop_index("job_name")
batch_op.create_foreign_key("fk_bw_jobs_cache_job_name", "bw_jobs", ["job_name"], ["name"])
# Other migration operations
op.add_column("bw_metadata", sa.Column("is_pro", sa.Boolean(), nullable=False, server_default=sa.false()))
op.add_column("bw_metadata", sa.Column("pro_expire", sa.DateTime(), nullable=True))
op.add_column(
"bw_metadata",
sa.Column("pro_status", sa.Enum("active", "invalid", "expired", "suspended", name="pro_status_enum"), nullable=False, server_default="invalid"),
)
op.add_column("bw_metadata", sa.Column("pro_services", sa.Integer(), nullable=False, server_default="0"))
op.add_column("bw_metadata", sa.Column("pro_overlapped", sa.Boolean(), nullable=False, server_default=sa.false()))
op.add_column("bw_metadata", sa.Column("last_pro_check", sa.DateTime(), nullable=True))
op.add_column("bw_metadata", sa.Column("pro_plugins_changed", sa.Boolean(), nullable=True))
op.add_column("bw_services", sa.Column("is_draft", sa.Boolean(), nullable=False, server_default=sa.false()))
op.add_column("bw_plugins", sa.Column("type", sa.Enum("core", "external", "pro", name="plugin_types_enum"), nullable=False, server_default="core"))
op.alter_column(
"bw_plugins", "stream", existing_type=mysql.VARCHAR(length=16), type_=sa.Enum("no", "yes", "partial", name="stream_types_enum"), existing_nullable=False
)
# Migrate data: Set 'type' to 'external' where 'external' was true
op.execute(
"""
UPDATE bw_plugins
SET type = 'external'
WHERE external = true
"""
)
op.drop_column("bw_plugins", "external")
op.alter_column("bw_global_values", "value", existing_type=mysql.VARCHAR(length=8192), type_=sa.TEXT(), existing_nullable=False)
op.alter_column("bw_services_settings", "value", existing_type=mysql.VARCHAR(length=8192), type_=sa.TEXT(), existing_nullable=False)
op.drop_index("name", table_name="bw_jobs")
op.drop_index("name", table_name="bw_settings")
# Update all new columns and version in a single statement
op.execute(
"""
UPDATE bw_metadata
SET is_pro = false,
pro_status = 'invalid',
pro_services = 0,
pro_overlapped = false,
pro_plugins_changed = false,
version = '1.5.6'
WHERE id = 1
"""
)
def downgrade():
with op.batch_alter_table("bw_jobs_cache") as batch_op:
batch_op.drop_constraint("fk_bw_jobs_cache_job_name", type_="foreignkey")
batch_op.create_index("job_name", ["job_name", "service_id", "file_name"], unique=True)
batch_op.create_foreign_key("bw_jobs_cache_ibfk_1", "bw_jobs", ["job_name"], ["name"]) # Replace with actual name
op.create_index("name", "bw_jobs", ["name", "plugin_id"], unique=True)
op.create_index("name", "bw_settings", ["name"], unique=True)
op.alter_column("bw_global_values", "value", existing_type=sa.TEXT(), type_=mysql.VARCHAR(length=8192), existing_nullable=False)
op.alter_column("bw_services_settings", "value", existing_type=sa.TEXT(), type_=mysql.VARCHAR(length=8192), existing_nullable=False)
op.add_column("bw_plugins", sa.Column("external", mysql.TINYINT(display_width=1), autoincrement=False, nullable=False))
# Migrate data: Set 'external' to true where 'type' was 'external'
op.execute(
"""
UPDATE bw_plugins
SET external = true
WHERE type = 'external'
"""
)
op.drop_column("bw_plugins", "type")
op.alter_column(
"bw_plugins", "stream", existing_type=sa.Enum("no", "yes", "partial", name="stream_types_enum"), type_=mysql.VARCHAR(length=16), existing_nullable=False
)
op.drop_column("bw_services", "is_draft")
op.drop_column("bw_metadata", "pro_plugins_changed")
op.drop_column("bw_metadata", "last_pro_check")
op.drop_column("bw_metadata", "pro_overlapped")
op.drop_column("bw_metadata", "pro_services")
op.drop_column("bw_metadata", "pro_status")
op.drop_column("bw_metadata", "pro_expire")
op.drop_column("bw_metadata", "is_pro")
op.execute("UPDATE bw_metadata SET version = '1.5.5' WHERE id = 1")

View file

@ -0,0 +1,54 @@
"""Upgrade to version 1.5.5
Revision ID: 0903238e095e
Revises: 3f152772560d
Create Date: 2024-12-19 13:21:42.890471
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision: str = "0903238e095e"
down_revision: Union[str, None] = "3f152772560d"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade():
# Add new columns to bw_ui_users
op.add_column("bw_ui_users", sa.Column("is_two_factor_enabled", sa.Boolean(), nullable=False, server_default=sa.false()))
op.add_column("bw_ui_users", sa.Column("secret_token", sa.String(length=32), nullable=True))
op.add_column(
"bw_ui_users", sa.Column("method", sa.Enum("ui", "scheduler", "autoconf", "manual", name="methods_enum"), nullable=False, server_default="manual")
)
op.create_unique_constraint("uq_bw_ui_users_secret_token", "bw_ui_users", ["secret_token"])
# Increase column sizes
op.alter_column("bw_global_values", "value", existing_type=mysql.VARCHAR(length=4096), type_=sa.String(length=8192), existing_nullable=False)
op.alter_column("bw_services_settings", "value", existing_type=mysql.VARCHAR(length=4096), type_=sa.String(length=8192), existing_nullable=False)
# Update all new columns in a single statement
op.execute("UPDATE bw_ui_users SET is_two_factor_enabled = false, method = 'manual'")
# Update version in bw_metadata
op.execute("UPDATE bw_metadata SET version = '1.5.5' WHERE id = 1")
def downgrade():
# Revert changes to 'bw_ui_users'
op.drop_constraint("uq_bw_ui_users_secret_token", "bw_ui_users", type_="unique")
op.drop_column("bw_ui_users", "method")
op.drop_column("bw_ui_users", "secret_token")
op.drop_column("bw_ui_users", "is_two_factor_enabled")
# Revert column sizes for VARCHAR
op.alter_column("bw_global_values", "value", existing_type=sa.String(length=8192), type_=mysql.VARCHAR(length=4096), existing_nullable=False)
op.alter_column("bw_services_settings", "value", existing_type=sa.String(length=8192), type_=mysql.VARCHAR(length=4096), existing_nullable=False)
# Revert version in bw_metadata
op.execute("UPDATE bw_metadata SET version = '1.5.4' WHERE id = 1")

View file

@ -0,0 +1,35 @@
"""Upgrade to version 1.5.11
Revision ID: 12ffcd2b9d63
Revises: b03a64d4d34a
Create Date: 2024-12-19 13:30:48.808893
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision: str = "12ffcd2b9d63"
down_revision: Union[str, None] = "b03a64d4d34a"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Add the new non_draft_services column to bw_metadata
op.add_column("bw_metadata", sa.Column("non_draft_services", sa.Integer(), nullable=False, server_default="0"))
# Update the version in metadata
op.execute("UPDATE bw_metadata SET version = '1.5.11' WHERE id = 1")
def downgrade() -> None:
# Reverse the addition of the non_draft_services column
op.drop_column("bw_metadata", "non_draft_services")
# Revert version to 1.5.10
op.execute("UPDATE bw_metadata SET version = '1.5.10' WHERE id = 1")

View file

@ -0,0 +1,29 @@
"""Upgrade to version 1.5.12
Revision ID: 3d6af0bf1bec
Revises: 12ffcd2b9d63
Create Date: 2024-12-19 13:32:27.783961
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision: str = "3d6af0bf1bec"
down_revision: Union[str, None] = "12ffcd2b9d63"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Update version to 1.5.12
op.execute("UPDATE bw_metadata SET version = '1.5.12' WHERE id = 1")
def downgrade() -> None:
# Revert version to 1.5.11
op.execute("UPDATE bw_metadata SET version = '1.5.11' WHERE id = 1")

View file

@ -0,0 +1,42 @@
"""Upgrade to version 1.5.4
Revision ID: 3f152772560d
Revises: e65b18370d91
Create Date: 2024-12-19 13:20:01.915208
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision: str = "3f152772560d"
down_revision: Union[str, None] = "e65b18370d91"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Create new table 'bw_ui_users'
op.create_table(
"bw_ui_users",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("username", sa.String(length=256), nullable=False),
sa.Column("password", sa.String(length=60), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("username"),
)
# Update metadata version
op.execute("UPDATE bw_metadata SET version = '1.5.4' WHERE id = 1")
def downgrade() -> None:
# Drop the table 'bw_ui_users'
op.drop_table("bw_ui_users")
# Revert metadata version
op.execute("UPDATE bw_metadata SET version = '1.5.3' WHERE id = 1")

View file

@ -0,0 +1,37 @@
"""Upgrade to version 1.5.0
Revision ID: 41d395e48cfd
Revises:
Create Date: 2024-12-19 13:15:17.279882
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision: str = "41d395e48cfd"
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Step 1: Drop 'order' column from 'bw_plugins'
with op.batch_alter_table("bw_plugins") as batch_op:
batch_op.drop_column("order")
# Data migration: Update the version to 1.5.0
op.execute("UPDATE bw_metadata SET version = '1.5.0' WHERE id = 1")
def downgrade() -> None:
# Re-add the 'order' column to the 'bw_plugins' table
with op.batch_alter_table("bw_plugins") as batch_op:
batch_op.add_column(sa.Column("order", mysql.INTEGER(display_width=11), autoincrement=False, nullable=False))
# Data migration: Update the version to 1.5.0-beta
op.execute("UPDATE bw_metadata SET version = '1.5.0-beta' WHERE id = 1")

View file

@ -0,0 +1,100 @@
"""Upgrade to version 1.6.0-rc1
Revision ID: 6307fa627563
Revises: 839424d81cf7
Create Date: 2024-12-20 10:41:52.149076
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision: str = "6307fa627563"
down_revision: Union[str, None] = "839424d81cf7"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Drop foreign keys referencing service_id columns before altering them
# Replace these FK names with the actual names in your schema
with op.batch_alter_table("bw_custom_configs") as batch_op:
batch_op.drop_constraint("bw_custom_configs_ibfk_1", type_="foreignkey")
with op.batch_alter_table("bw_jobs_cache") as batch_op:
batch_op.drop_constraint("bw_jobs_cache_ibfk_2", type_="foreignkey")
with op.batch_alter_table("bw_services_settings") as batch_op:
batch_op.drop_constraint("bw_services_settings_ibfk_1", type_="foreignkey")
# Alter columns now that foreign keys are dropped
op.alter_column("bw_custom_configs", "service_id", existing_type=mysql.VARCHAR(length=64), type_=sa.String(length=256), existing_nullable=True)
op.alter_column("bw_jobs_cache", "service_id", existing_type=mysql.VARCHAR(length=64), type_=sa.String(length=256), existing_nullable=True)
op.alter_column("bw_services", "id", existing_type=mysql.VARCHAR(length=64), type_=sa.String(length=256), existing_nullable=False)
op.alter_column("bw_services_settings", "service_id", existing_type=mysql.VARCHAR(length=64), type_=sa.String(length=256), existing_nullable=False)
# After altering, recreate the foreign keys with updated references
with op.batch_alter_table("bw_custom_configs") as batch_op:
batch_op.create_foreign_key("bw_custom_configs_ibfk_1", "bw_services", ["service_id"], ["id"], onupdate="CASCADE", ondelete="CASCADE")
with op.batch_alter_table("bw_jobs_cache") as batch_op:
batch_op.create_foreign_key("bw_jobs_cache_ibfk_2", "bw_services", ["service_id"], ["id"], onupdate="CASCADE", ondelete="CASCADE")
with op.batch_alter_table("bw_services_settings") as batch_op:
batch_op.create_foreign_key("bw_services_settings_ibfk_1", "bw_services", ["service_id"], ["id"], onupdate="CASCADE", ondelete="CASCADE")
# Update bw_settings.default from String(4096) to TEXT
op.alter_column("bw_settings", "default", existing_type=mysql.VARCHAR(length=4096), type_=sa.TEXT(), existing_nullable=True)
# Drop bw_ui_users.id column
op.drop_column("bw_ui_users", "id")
# Update version in bw_metadata
op.execute("UPDATE bw_metadata SET version = '1.6.0-rc1' WHERE id = 1")
def downgrade() -> None:
# Reverse the changes:
# 1. Drop foreign keys
# 2. Revert column types
# 3. Recreate foreign keys
# 4. Revert version
# 5. Re-add bw_ui_users.id column
with op.batch_alter_table("bw_custom_configs") as batch_op:
batch_op.drop_constraint("bw_custom_configs_ibfk_1", type_="foreignkey")
with op.batch_alter_table("bw_jobs_cache") as batch_op:
batch_op.drop_constraint("bw_jobs_cache_ibfk_1", type_="foreignkey")
with op.batch_alter_table("bw_services_settings") as batch_op:
batch_op.drop_constraint("bw_services_settings_ibfk_1", type_="foreignkey")
op.alter_column("bw_services_settings", "service_id", existing_type=sa.String(length=256), type_=mysql.VARCHAR(length=64), existing_nullable=False)
op.alter_column("bw_services", "id", existing_type=sa.String(length=256), type_=mysql.VARCHAR(length=64), existing_nullable=False)
op.alter_column("bw_jobs_cache", "service_id", existing_type=sa.String(length=256), type_=mysql.VARCHAR(length=64), existing_nullable=True)
op.alter_column("bw_custom_configs", "service_id", existing_type=sa.String(length=256), type_=mysql.VARCHAR(length=64), existing_nullable=True)
# Recreate foreign keys with old definitions
with op.batch_alter_table("bw_custom_configs") as batch_op:
batch_op.create_foreign_key("bw_custom_configs_ibfk_1", "bw_services", ["service_id"], ["id"], onupdate="CASCADE", ondelete="CASCADE")
with op.batch_alter_table("bw_jobs_cache") as batch_op:
batch_op.create_foreign_key("bw_jobs_cache_ibfk_1", "bw_services", ["service_id"], ["id"], onupdate="CASCADE", ondelete="CASCADE")
with op.batch_alter_table("bw_services_settings") as batch_op:
batch_op.create_foreign_key("bw_services_settings_ibfk_1", "bw_services", ["service_id"], ["id"], onupdate="CASCADE", ondelete="CASCADE")
op.alter_column("bw_settings", "default", existing_type=sa.TEXT(), type_=mysql.VARCHAR(length=4096), existing_nullable=True)
# Re-add bw_ui_users.id and index on username
op.add_column("bw_ui_users", sa.Column("id", sa.Integer(), autoincrement=True, nullable=False))
# Revert version
op.execute("UPDATE bw_metadata SET version = '1.6.0-beta' WHERE id = 1")

View file

@ -0,0 +1,439 @@
"""Upgrade to version 1.6.0-beta
Revision ID: 839424d81cf7
Revises: 3d6af0bf1bec
Create Date: 2024-12-19 13:34:03.714317
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision: str = "839424d81cf7"
down_revision: Union[str, None] = "3d6af0bf1bec"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade():
# Define old and new ENUMs for methods_enum (adding "wizard")
old_methods_enum = sa.Enum("ui", "scheduler", "autoconf", "manual", name="methods_enum")
new_methods_enum = sa.Enum("ui", "scheduler", "autoconf", "manual", "wizard", name="methods_enum")
# Alter columns that use methods_enum
op.alter_column("bw_plugins", "method", existing_type=old_methods_enum, type_=new_methods_enum, existing_nullable=False, existing_server_default="manual")
op.alter_column("bw_services", "method", existing_type=old_methods_enum, type_=new_methods_enum, existing_nullable=False)
op.alter_column("bw_custom_configs", "method", existing_type=old_methods_enum, type_=new_methods_enum, existing_nullable=False)
op.alter_column("bw_ui_users", "method", existing_type=old_methods_enum, type_=new_methods_enum, existing_nullable=False, existing_server_default="manual")
# Update custom_configs_types_enum (adding "default_server_stream", "crs_plugins_before", "crs_plugins_after")
old_cct_enum = sa.Enum("http", "stream", "server_http", "server_stream", "default_server_http", "modsec", "modsec_crs", name="custom_configs_types_enum")
new_cct_enum = sa.Enum(
"http",
"stream",
"server_http",
"server_stream",
"default_server_http",
"default_server_stream",
"modsec",
"modsec_crs",
"crs_plugins_before",
"crs_plugins_after",
name="custom_configs_types_enum",
)
op.alter_column("bw_custom_configs", "type", existing_type=old_cct_enum, type_=new_cct_enum, existing_nullable=False)
# Update plugin_types_enum (adding "ui")
old_pt_enum = sa.Enum("core", "external", "pro", name="plugin_types_enum")
new_pt_enum = sa.Enum("core", "external", "ui", "pro", name="plugin_types_enum")
op.alter_column("bw_plugins", "type", existing_type=old_pt_enum, type_=new_pt_enum, existing_nullable=False, existing_server_default="core")
# Create new UI tables
op.create_table(
"bw_ui_permissions",
sa.Column("name", sa.String(64), primary_key=True),
)
op.create_table(
"bw_ui_roles",
sa.Column("name", sa.String(64), primary_key=True),
sa.Column("description", sa.String(256), nullable=False),
sa.Column("update_datetime", sa.DateTime(timezone=True), nullable=False),
)
op.create_table(
"bw_ui_user_recovery_codes",
sa.Column("id", sa.Integer(), sa.Identity(start=1, increment=1), primary_key=True),
sa.Column("user_name", sa.String(256), nullable=False),
sa.Column("code", sa.UnicodeText, nullable=False),
sa.ForeignKeyConstraint(["user_name"], ["bw_ui_users.username"], onupdate="CASCADE", ondelete="CASCADE"),
)
op.create_table(
"bw_ui_roles_permissions",
sa.Column("role_name", sa.String(64), nullable=False),
sa.Column("permission_name", sa.String(64), nullable=False),
sa.ForeignKeyConstraint(["role_name"], ["bw_ui_roles.name"], onupdate="CASCADE", ondelete="CASCADE"),
sa.ForeignKeyConstraint(["permission_name"], ["bw_ui_permissions.name"], onupdate="CASCADE", ondelete="CASCADE"),
sa.PrimaryKeyConstraint("role_name", "permission_name"),
)
op.create_table(
"bw_ui_roles_users",
sa.Column("user_name", sa.String(256), nullable=False),
sa.Column("role_name", sa.String(64), nullable=False),
sa.ForeignKeyConstraint(["user_name"], ["bw_ui_users.username"], onupdate="CASCADE", ondelete="CASCADE"),
sa.ForeignKeyConstraint(["role_name"], ["bw_ui_roles.name"], onupdate="CASCADE", ondelete="CASCADE"),
sa.PrimaryKeyConstraint("user_name", "role_name"),
)
op.create_table(
"bw_ui_user_sessions",
sa.Column("id", sa.Integer(), sa.Identity(start=1, increment=1), primary_key=True),
sa.Column("user_name", sa.String(256), nullable=False),
sa.Column("ip", sa.String(39), nullable=False),
sa.Column("user_agent", sa.TEXT, nullable=False),
sa.Column("creation_date", sa.DateTime(timezone=True), nullable=False),
sa.Column("last_activity", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["user_name"], ["bw_ui_users.username"], onupdate="CASCADE", ondelete="CASCADE"),
)
from model import JSONText
op.create_table(
"bw_ui_user_columns_preferences",
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
sa.Column("user_name", sa.String(256), nullable=False),
sa.Column("table_name", sa.Enum("bans", "configs", "instances", "jobs", "plugins", "reports", "services", name="tables_enum"), nullable=False),
sa.Column("columns", JSONText, nullable=False),
sa.ForeignKeyConstraint(["user_name"], ["bw_ui_users.username"], onupdate="CASCADE", ondelete="CASCADE"),
sa.UniqueConstraint("user_name", "table_name", name="uq_user_columns_preferences"),
)
# Templates tables
op.create_table(
"bw_templates",
sa.Column("id", sa.String(256), primary_key=True),
sa.Column("name", sa.String(256), unique=True, nullable=False),
sa.Column("plugin_id", sa.String(64), nullable=False),
sa.ForeignKeyConstraint(["plugin_id"], ["bw_plugins.id"], onupdate="CASCADE", ondelete="CASCADE"),
)
op.create_table(
"bw_template_steps",
sa.Column("id", sa.Integer(), primary_key=True),
sa.Column("template_id", sa.String(256), nullable=False, primary_key=True),
sa.Column("title", sa.TEXT, nullable=False),
sa.Column("subtitle", sa.TEXT, nullable=True),
sa.ForeignKeyConstraint(["template_id"], ["bw_templates.id"], onupdate="CASCADE", ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id", "template_id"),
)
op.create_table(
"bw_template_settings",
sa.Column("id", sa.Integer(), sa.Identity(start=1, increment=1), primary_key=True),
sa.Column("template_id", sa.String(256), nullable=False),
sa.Column("setting_id", sa.String(256), nullable=False),
sa.Column("step_id", sa.Integer(), nullable=True),
sa.Column("default", sa.TEXT, nullable=False),
sa.Column("suffix", sa.Integer(), nullable=True, default=0),
sa.ForeignKeyConstraint(["template_id"], ["bw_templates.id"], onupdate="CASCADE", ondelete="CASCADE"),
sa.ForeignKeyConstraint(["setting_id"], ["bw_settings.id"], onupdate="CASCADE", ondelete="CASCADE"),
sa.UniqueConstraint("template_id", "setting_id", "step_id", "suffix"),
)
op.create_table(
"bw_template_custom_configs",
sa.Column("id", sa.Integer(), sa.Identity(start=1, increment=1), primary_key=True),
sa.Column("template_id", sa.String(256), nullable=False),
sa.Column("step_id", sa.Integer(), nullable=True),
sa.Column("type", new_cct_enum, nullable=False),
sa.Column("name", sa.String(256), nullable=False),
sa.Column("data", sa.LargeBinary(length=(2**32) - 1), nullable=False),
sa.Column("checksum", sa.String(128), nullable=False),
sa.ForeignKeyConstraint(["template_id"], ["bw_templates.id"], onupdate="CASCADE", ondelete="CASCADE"),
sa.UniqueConstraint("template_id", "step_id", "type", "name"),
)
# bw_jobs changes (add run_async, remove success and last_run)
with op.batch_alter_table("bw_jobs") as batch_op:
batch_op.add_column(sa.Column("run_async", sa.Boolean(), nullable=False, server_default="0"))
batch_op.drop_column("success")
batch_op.drop_column("last_run")
op.create_table(
"bw_jobs_runs",
sa.Column("id", sa.Integer(), sa.Identity(start=1, increment=1), primary_key=True),
sa.Column("job_name", sa.String(128), nullable=False),
sa.Column("success", sa.Boolean(), nullable=True, server_default="0"),
sa.Column("start_date", sa.DateTime(timezone=True), nullable=False),
sa.Column("end_date", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["job_name"], ["bw_jobs.name"], onupdate="CASCADE", ondelete="CASCADE"),
)
# bw_services add creation_date, last_update
with op.batch_alter_table("bw_services") as batch_op:
batch_op.add_column(sa.Column("creation_date", sa.DateTime(timezone=True), nullable=True))
batch_op.add_column(sa.Column("last_update", sa.DateTime(timezone=True), nullable=True))
# Set current timestamp for existing services data
op.execute("UPDATE bw_services SET creation_date = CURRENT_TIMESTAMP, last_update = CURRENT_TIMESTAMP")
with op.batch_alter_table("bw_services") as batch_op:
batch_op.alter_column("creation_date", existing_type=sa.DateTime(timezone=True), nullable=False)
batch_op.alter_column("last_update", existing_type=sa.DateTime(timezone=True), nullable=False)
# bw_ui_users changes
with op.batch_alter_table("bw_ui_users") as batch_op:
batch_op.add_column(sa.Column("email", sa.String(256), nullable=True, unique=True))
batch_op.add_column(sa.Column("admin", sa.Boolean(), nullable=False, server_default="0"))
batch_op.add_column(sa.Column("theme", sa.Enum("light", "dark", name="themes_enum"), nullable=False, server_default="light"))
batch_op.add_column(sa.Column("totp_secret", sa.String(256), nullable=True))
batch_op.add_column(sa.Column("creation_date", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.current_timestamp()))
batch_op.add_column(sa.Column("update_date", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.current_timestamp()))
batch_op.drop_column("is_two_factor_enabled")
batch_op.drop_column("secret_token")
# Set defaults for existing bw_ui_users rows
op.execute("UPDATE bw_ui_users SET admin=1, theme='light', creation_date=CURRENT_TIMESTAMP, update_date=CURRENT_TIMESTAMP")
# bw_plugin_pages changes
with op.batch_alter_table("bw_plugin_pages") as batch_op:
batch_op.add_column(sa.Column("data", sa.LargeBinary(length=(2**32) - 1), nullable=False))
batch_op.add_column(sa.Column("checksum", sa.String(128), nullable=False))
# Add old data to new columns
op.execute(
"""
UPDATE bw_plugin_pages
SET data = template_file, checksum = template_checksum
WHERE template_file IS NOT NULL
"""
)
with op.batch_alter_table("bw_plugin_pages") as batch_op:
batch_op.drop_column("template_file")
batch_op.drop_column("template_checksum")
batch_op.drop_column("actions_file")
batch_op.drop_column("actions_checksum")
batch_op.drop_column("obfuscation_file")
batch_op.drop_column("obfuscation_checksum")
# bw_instances changes
with op.batch_alter_table("bw_instances") as batch_op:
batch_op.add_column(sa.Column("name", sa.String(256), nullable=True))
# Add the 'type' column with the old enum definition (or no enum yet if it didnt exist before)
# For instance, if previously there was no enum, start with the initial values:
old_it_enum = sa.Enum("static", name="instance_type_enum") # The original value set
batch_op.add_column(sa.Column("type", old_it_enum, nullable=True))
# Similarly add the 'status', 'method', 'creation_date', 'last_seen' columns
old_is_enum = sa.Enum("loading", "up", name="instance_status_enum")
batch_op.add_column(sa.Column("status", old_is_enum, nullable=True))
old_methods_enum = sa.Enum("ui", "scheduler", "autoconf", "manual", name="methods_enum")
batch_op.add_column(sa.Column("method", old_methods_enum, nullable=True))
batch_op.add_column(sa.Column("creation_date", sa.DateTime(timezone=True), nullable=True))
batch_op.add_column(sa.Column("last_seen", sa.DateTime(timezone=True), nullable=True))
op.execute("DELETE FROM bw_instances WHERE name IS NULL")
# Now that columns exist and have values, alter their definitions if needed:
new_it_enum = sa.Enum("static", "container", "pod", name="instance_type_enum")
op.alter_column("bw_instances", "type", existing_type=old_it_enum, type_=new_it_enum, existing_nullable=False, existing_server_default="static")
new_is_enum = sa.Enum("loading", "up", "down", name="instance_status_enum")
op.alter_column("bw_instances", "status", existing_type=old_is_enum, type_=new_is_enum, existing_nullable=False, existing_server_default="loading")
# Similarly alter the method column to the new enum with 'wizard' if needed:
extended_methods_enum = sa.Enum("ui", "scheduler", "autoconf", "manual", "wizard", name="methods_enum")
op.alter_column(
"bw_instances", "method", existing_type=old_methods_enum, type_=extended_methods_enum, existing_nullable=False, existing_server_default="manual"
)
# Finally, now set them to NOT NULL
with op.batch_alter_table("bw_instances") as batch_op:
batch_op.alter_column("name", existing_type=sa.String(256), nullable=False)
batch_op.alter_column("type", existing_type=new_it_enum, nullable=False)
batch_op.alter_column("status", existing_type=new_is_enum, nullable=False)
batch_op.alter_column("method", existing_type=extended_methods_enum, nullable=False)
batch_op.alter_column("creation_date", existing_type=sa.DateTime(timezone=True), nullable=False)
batch_op.alter_column("last_seen", existing_type=sa.DateTime(timezone=True), nullable=False)
# Update version
op.execute("UPDATE bw_metadata SET version = '1.6.0-beta' WHERE id = 1")
def downgrade():
# Revert enums to their old definitions by altering columns back
old_methods_enum = sa.Enum("ui", "scheduler", "autoconf", "manual", name="methods_enum")
# methods_enum had "wizard" added, now remove it
op.alter_column(
"bw_plugins",
"method",
existing_type=sa.Enum("ui", "scheduler", "autoconf", "manual", "wizard", name="methods_enum"),
type_=old_methods_enum,
existing_nullable=False,
existing_server_default="manual",
)
op.alter_column(
"bw_services",
"method",
existing_type=sa.Enum("ui", "scheduler", "autoconf", "manual", "wizard", name="methods_enum"),
type_=old_methods_enum,
existing_nullable=False,
)
op.alter_column(
"bw_custom_configs",
"method",
existing_type=sa.Enum("ui", "scheduler", "autoconf", "manual", "wizard", name="methods_enum"),
type_=old_methods_enum,
existing_nullable=False,
)
op.alter_column(
"bw_ui_users",
"method",
existing_type=sa.Enum("ui", "scheduler", "autoconf", "manual", "wizard", name="methods_enum"),
type_=old_methods_enum,
existing_nullable=False,
existing_server_default="manual",
)
# custom_configs_types_enum remove "default_server_stream", "crs_plugins_before", "crs_plugins_after"
old_cct_enum = sa.Enum("http", "default_server_http", "server_http", "modsec", "modsec_crs", "stream", "server_stream", name="custom_configs_types_enum")
op.alter_column(
"bw_custom_configs",
"type",
existing_type=sa.Enum(
"http",
"stream",
"server_http",
"server_stream",
"default_server_http",
"default_server_stream",
"modsec",
"modsec_crs",
"crs_plugins_before",
"crs_plugins_after",
name="custom_configs_types_enum",
),
type_=old_cct_enum,
existing_nullable=False,
)
# plugin_types_enum remove "ui"
old_pt_enum = sa.Enum("core", "external", "pro", name="plugin_types_enum")
op.alter_column(
"bw_plugins",
"type",
existing_type=sa.Enum("core", "external", "ui", "pro", name="plugin_types_enum"),
type_=old_pt_enum,
existing_nullable=False,
existing_server_default="core",
)
# instance_type_enum revert to just "static"
old_it_enum = sa.Enum("static", name="instance_type_enum")
op.alter_column(
"bw_instances",
"type",
existing_type=sa.Enum("static", "container", "pod", name="instance_type_enum"),
type_=old_it_enum,
existing_nullable=False,
existing_server_default="static",
)
# instance_status_enum revert to "loading", "up"
old_is_enum = sa.Enum("loading", "up", name="instance_status_enum")
op.alter_column(
"bw_instances",
"status",
existing_type=sa.Enum("loading", "up", "down", name="instance_status_enum"),
type_=old_is_enum,
existing_nullable=False,
existing_server_default="loading",
)
# Drop newly created UI and templates tables
op.drop_table("bw_template_custom_configs")
op.drop_table("bw_template_settings")
op.drop_table("bw_template_steps")
op.drop_table("bw_templates")
op.drop_table("bw_ui_user_columns_preferences")
op.drop_table("bw_ui_user_sessions")
op.drop_table("bw_ui_roles_users")
op.drop_table("bw_ui_roles_permissions")
op.drop_table("bw_ui_user_recovery_codes")
op.drop_table("bw_ui_roles")
op.drop_table("bw_ui_permissions")
# Revert bw_settings constraints: old primary key was (id,name) and there was a unique constraint on id
op.drop_constraint("bw_settings_name_key", "bw_settings", type_="unique")
op.drop_constraint("bw_settings_pkey", "bw_settings", type_="primary")
# Recreate old constraints:
# PrimaryKeyConstraint("id", "name"), UniqueConstraint("id")
# Note: The original primary key and unique constraints must be re-added as they were initially.
op.create_primary_key("bw_settings_pkey", "bw_settings", ["id", "name"])
op.create_unique_constraint("id", "bw_settings", ["id"])
# bw_jobs revert: drop run_async, add success and last_run
with op.batch_alter_table("bw_jobs") as batch_op:
batch_op.drop_column("run_async")
batch_op.add_column(sa.Column("success", sa.Boolean(), nullable=True))
batch_op.add_column(sa.Column("last_run", sa.DateTime(), nullable=True))
# Drop bw_jobs_runs table
op.drop_table("bw_jobs_runs")
# bw_services revert: remove creation_date, last_update
with op.batch_alter_table("bw_services") as batch_op:
batch_op.drop_column("last_update")
batch_op.drop_column("creation_date")
# bw_ui_users revert: drop email, admin, theme, totp_secret, creation_date, update_date
# add back is_two_factor_enabled, secret_token
with op.batch_alter_table("bw_ui_users") as batch_op:
batch_op.drop_column("email")
batch_op.drop_column("admin")
batch_op.drop_column("theme")
batch_op.drop_column("totp_secret")
batch_op.drop_column("creation_date")
batch_op.drop_column("update_date")
batch_op.add_column(sa.Column("is_two_factor_enabled", sa.Boolean, nullable=False, server_default="0"))
batch_op.add_column(sa.Column("secret_token", sa.String(32), nullable=True, unique=True, default=None))
# bw_plugin_pages revert: remove data, checksum, add template_file, template_checksum, actions_file, actions_checksum, obfuscation_file, obfuscation_checksum
with op.batch_alter_table("bw_plugin_pages") as batch_op:
batch_op.drop_column("data")
batch_op.drop_column("checksum")
batch_op.add_column(sa.Column("template_file", sa.LargeBinary(length=(2**32) - 1), nullable=False))
batch_op.add_column(sa.Column("template_checksum", sa.String(128), nullable=False))
batch_op.add_column(sa.Column("actions_file", sa.LargeBinary(length=(2**32) - 1), nullable=False))
batch_op.add_column(sa.Column("actions_checksum", sa.String(128), nullable=False))
batch_op.add_column(sa.Column("obfuscation_file", sa.LargeBinary(length=(2**32) - 1), nullable=True))
batch_op.add_column(sa.Column("obfuscation_checksum", sa.String(128), nullable=True))
# bw_instances revert: drop name, type, status, method, creation_date, last_seen
with op.batch_alter_table("bw_instances") as batch_op:
batch_op.drop_column("last_seen")
batch_op.drop_column("creation_date")
batch_op.drop_column("method")
batch_op.drop_column("status")
batch_op.drop_column("type")
batch_op.drop_column("name")
# Revert version
op.execute("UPDATE bw_metadata SET version = '1.5.12' WHERE id = 1")

View file

@ -0,0 +1,29 @@
"""Upgrade to version 1.5.10
Revision ID: b03a64d4d34a
Revises: c6fcd4f6971d
Create Date: 2024-12-19 13:28:59.190577
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision: str = "b03a64d4d34a"
down_revision: Union[str, None] = "c6fcd4f6971d"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Update version in 'bw_metadata'
op.execute("UPDATE bw_metadata SET version = '1.5.10' WHERE id = 1")
def downgrade() -> None:
# Revert version in 'bw_metadata' back to 1.5.9
op.execute("UPDATE bw_metadata SET version = '1.5.9' WHERE id = 1")

View file

@ -0,0 +1,62 @@
"""Upgrade to version 1.5.8
Revision ID: b45f98a50b5c
Revises: dd15cf86a200
Create Date: 2024-12-19 13:26:03.280603
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision: str = "b45f98a50b5c"
down_revision: Union[str, None] = "dd15cf86a200"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Add new columns to 'bw_metadata'
with op.batch_alter_table("bw_metadata") as batch_op:
batch_op.add_column(sa.Column("pro_license", sa.String(length=128), nullable=True))
batch_op.add_column(sa.Column("last_custom_configs_change", sa.DateTime(), nullable=True))
batch_op.add_column(sa.Column("last_external_plugins_change", sa.DateTime(), nullable=True))
batch_op.add_column(sa.Column("last_pro_plugins_change", sa.DateTime(), nullable=True))
batch_op.add_column(sa.Column("last_instances_change", sa.DateTime(), nullable=True))
batch_op.add_column(sa.Column("failover", sa.Boolean(), nullable=True))
batch_op.drop_column("config_changed")
# Add new columns to 'bw_plugins'
with op.batch_alter_table("bw_plugins") as batch_op:
batch_op.add_column(sa.Column("config_changed", sa.Boolean(), nullable=True))
batch_op.add_column(sa.Column("last_config_change", sa.DateTime(), nullable=True))
# Set default value for 'config_changed' in 'bw_plugins'
op.execute("UPDATE bw_plugins SET config_changed = false")
# Update version in 'bw_metadata'
op.execute("UPDATE bw_metadata SET version = '1.5.8' WHERE id = 1")
def downgrade() -> None:
# Revert 'bw_plugins' changes
with op.batch_alter_table("bw_plugins") as batch_op:
batch_op.drop_column("last_config_change")
batch_op.drop_column("config_changed")
# Revert 'bw_metadata' changes
with op.batch_alter_table("bw_metadata") as batch_op:
batch_op.add_column(sa.Column("config_changed", sa.Boolean(), nullable=True))
batch_op.drop_column("failover")
batch_op.drop_column("last_instances_change")
batch_op.drop_column("last_pro_plugins_change")
batch_op.drop_column("last_external_plugins_change")
batch_op.drop_column("last_custom_configs_change")
batch_op.drop_column("pro_license")
# Revert version in 'bw_metadata'
op.execute("UPDATE bw_metadata SET version = '1.5.7', config_changed = false WHERE id = 1")

View file

@ -0,0 +1,29 @@
"""Upgrade to version 1.5.9
Revision ID: c6fcd4f6971d
Revises: b45f98a50b5c
Create Date: 2024-12-19 13:27:29.252130
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision: str = "c6fcd4f6971d"
down_revision: Union[str, None] = "b45f98a50b5c"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Update version in 'bw_metadata'
op.execute("UPDATE bw_metadata SET version = '1.5.9' WHERE id = 1")
def downgrade() -> None:
# Revert version in 'bw_metadata'
op.execute("UPDATE bw_metadata SET version = '1.5.8' WHERE id = 1")

View file

@ -0,0 +1,75 @@
"""Upgrade to version 1.5.7
Revision ID: dd15cf86a200
Revises: 021e3123e517
Create Date: 2024-12-19 13:24:36.941477
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision: str = "dd15cf86a200"
down_revision: Union[str, None] = "021e3123e517"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Create new table for bw_cli_commands
op.create_table(
"bw_cli_commands",
sa.Column("id", sa.Integer(), sa.Identity(always=False, start=1, increment=1), nullable=False),
sa.Column("name", sa.String(length=64), nullable=False),
sa.Column("plugin_id", sa.String(length=64), nullable=False),
sa.Column("file_name", sa.String(length=256), nullable=False),
sa.ForeignKeyConstraint(["plugin_id"], ["bw_plugins.id"], onupdate="cascade", ondelete="cascade"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("plugin_id", "name"),
)
# Handle foreign key constraints for bw_jobs_cache
op.drop_constraint("fk_bw_jobs_cache_job_name", "bw_jobs_cache", type_="foreignkey")
op.create_foreign_key(None, "bw_jobs_cache", "bw_jobs", ["job_name"], ["name"], onupdate="cascade", ondelete="cascade")
# Add new columns to bw_plugin_pages
op.add_column(
"bw_plugin_pages",
sa.Column("obfuscation_file", mysql.LONGBLOB(), nullable=True),
)
op.add_column(
"bw_plugin_pages",
sa.Column("obfuscation_checksum", sa.String(length=128), nullable=True),
)
# Add the new order column to bw_settings
op.add_column("bw_settings", sa.Column("order", sa.Integer(), nullable=False))
# Update version in bw_metadata
op.execute("UPDATE bw_metadata SET version = '1.5.7' WHERE id = 1")
def downgrade() -> None:
# Reverse the version update
op.execute("UPDATE bw_metadata SET version = '1.5.6' WHERE id = 1")
# Reverse the addition of the order column
op.drop_column("bw_settings", "order")
# Reverse changes in bw_plugin_pages
op.drop_column("bw_plugin_pages", "obfuscation_checksum")
op.drop_column("bw_plugin_pages", "obfuscation_file")
# Restore foreign key constraints for bw_jobs_cache
op.drop_constraint(None, "bw_jobs_cache", type_="foreignkey")
op.create_foreign_key("fk_bw_jobs_cache_job_name", "bw_jobs_cache", "bw_jobs", ["job_name"], ["name"])
# Drop the newly created table bw_cli_commands
op.drop_table("bw_cli_commands")
# Revert the version update
op.execute("UPDATE bw_metadata SET version = '1.5.6' WHERE id = 1")

View file

@ -0,0 +1,29 @@
"""Upgrade to version 1.5.2
Revision ID: e4ccc523fea5
Revises: edcde398c829
Create Date: 2024-12-19 13:17:25.789125
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision: str = "e4ccc523fea5"
down_revision: Union[str, None] = "edcde398c829"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Data migration: Update the version to 1.5.2
op.execute("UPDATE bw_metadata SET version = '1.5.2' WHERE id = 1")
def downgrade() -> None:
# Revert the version back to 1.5.1
op.execute("UPDATE bw_metadata SET version = '1.5.1' WHERE id = 1")

View file

@ -0,0 +1,29 @@
"""Upgrade to version 1.5.3
Revision ID: e65b18370d91
Revises: e4ccc523fea5
Create Date: 2024-12-19 13:18:36.153688
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision: str = "e65b18370d91"
down_revision: Union[str, None] = "e4ccc523fea5"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Data migration: Update the version to 1.5.3
op.execute("UPDATE bw_metadata SET version = '1.5.3' WHERE id = 1")
def downgrade() -> None:
# Revert the version back to 1.5.2
op.execute("UPDATE bw_metadata SET version = '1.5.2' WHERE id = 1")

View file

@ -0,0 +1,56 @@
"""Upgrade to version 1.5.1
Revision ID: edcde398c829
Revises: 41d395e48cfd
Create Date: 2024-12-19 13:16:34.086006
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision: str = "edcde398c829"
down_revision: Union[str, None] = "41d395e48cfd"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Add new columns to bw_metadata."""
with op.batch_alter_table("bw_metadata") as batch_op:
batch_op.add_column(sa.Column("scheduler_first_start", sa.Boolean(), nullable=True))
batch_op.add_column(sa.Column("custom_configs_changed", sa.Boolean(), nullable=True))
batch_op.add_column(sa.Column("external_plugins_changed", sa.Boolean(), nullable=True))
batch_op.add_column(sa.Column("config_changed", sa.Boolean(), nullable=True))
batch_op.add_column(sa.Column("instances_changed", sa.Boolean(), nullable=True))
# Update all new columns and version in a single statement
op.execute(
"""
UPDATE bw_metadata
SET scheduler_first_start = false,
custom_configs_changed = false,
external_plugins_changed = false,
config_changed = false,
instances_changed = false,
version = '1.5.1'
WHERE id = 1
"""
)
def downgrade() -> None:
"""Remove new columns from bw_metadata."""
with op.batch_alter_table("bw_metadata") as batch_op:
batch_op.drop_column("instances_changed")
batch_op.drop_column("config_changed")
batch_op.drop_column("external_plugins_changed")
batch_op.drop_column("custom_configs_changed")
batch_op.drop_column("scheduler_first_start")
# Revert the version back to 1.5.0
op.execute("UPDATE bw_metadata SET version = '1.5.0' WHERE id = 1")

View file

@ -0,0 +1,29 @@
"""Upgrade to version 1.5.10
Revision ID: 0a2e336b02e7
Revises: fe047f892d6b
Create Date: 2024-12-19 14:42:04.961295
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "0a2e336b02e7"
down_revision: Union[str, None] = "fe047f892d6b"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Update version in 'bw_metadata'
op.execute("UPDATE bw_metadata SET version = '1.5.10' WHERE id = 1")
def downgrade() -> None:
# Revert version in 'bw_metadata' back to 1.5.9
op.execute("UPDATE bw_metadata SET version = '1.5.9' WHERE id = 1")

View file

@ -0,0 +1,365 @@
"""Upgrade to version 1.6.0-beta
Revision ID: 0b08c406d820
Revises: fbd680c6ffeb
Create Date: 2024-12-19 14:47:13.427196
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = "0b08c406d820"
down_revision: Union[str, None] = "fbd680c6ffeb"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade():
# Use create_type=False since these enums already exist
old_methods_enum = postgresql.ENUM("ui", "scheduler", "autoconf", "manual", name="methods_enum", create_type=False)
new_methods_enum = postgresql.ENUM("ui", "scheduler", "autoconf", "manual", "wizard", name="methods_enum", create_type=False)
old_cct_enum = postgresql.ENUM(
"http", "stream", "server_http", "server_stream", "default_server_http", "modsec", "modsec_crs", name="custom_configs_types_enum", create_type=False
)
new_cct_enum = postgresql.ENUM(
"http",
"stream",
"server_http",
"server_stream",
"default_server_http",
"default_server_stream",
"modsec",
"modsec_crs",
"crs_plugins_before",
"crs_plugins_after",
name="custom_configs_types_enum",
create_type=False,
)
old_pt_enum = postgresql.ENUM("core", "external", "pro", name="plugin_types_enum", create_type=False)
new_pt_enum = postgresql.ENUM("core", "external", "ui", "pro", name="plugin_types_enum", create_type=False)
# Add new enum values if not already present
op.execute("ALTER TYPE custom_configs_types_enum ADD VALUE IF NOT EXISTS 'default_server_stream'")
op.execute("ALTER TYPE custom_configs_types_enum ADD VALUE IF NOT EXISTS 'crs_plugins_before'")
op.execute("ALTER TYPE custom_configs_types_enum ADD VALUE IF NOT EXISTS 'crs_plugins_after'")
op.execute("ALTER TYPE plugin_types_enum ADD VALUE IF NOT EXISTS 'ui'")
# Alter columns that rely on these enums
op.alter_column("bw_plugins", "method", existing_type=old_methods_enum, type_=new_methods_enum, existing_nullable=False, existing_server_default="manual")
op.alter_column("bw_services", "method", existing_type=old_methods_enum, type_=new_methods_enum, existing_nullable=False)
op.alter_column("bw_custom_configs", "method", existing_type=old_methods_enum, type_=new_methods_enum, existing_nullable=False)
op.alter_column("bw_ui_users", "method", existing_type=old_methods_enum, type_=new_methods_enum, existing_nullable=False, existing_server_default="manual")
op.alter_column("bw_custom_configs", "type", existing_type=old_cct_enum, type_=new_cct_enum, existing_nullable=False)
op.alter_column("bw_plugins", "type", existing_type=old_pt_enum, type_=new_pt_enum, existing_nullable=False, existing_server_default="core")
# Create new UI tables
op.create_table(
"bw_ui_permissions",
sa.Column("name", sa.String(64), primary_key=True),
)
op.create_table(
"bw_ui_roles",
sa.Column("name", sa.String(64), primary_key=True),
sa.Column("description", sa.String(256), nullable=False),
sa.Column("update_datetime", sa.DateTime(timezone=True), nullable=False),
)
op.create_table(
"bw_ui_user_recovery_codes",
sa.Column("id", sa.Integer(), sa.Identity(start=1, increment=1), primary_key=True),
sa.Column("user_name", sa.String(256), nullable=False),
sa.Column("code", sa.UnicodeText, nullable=False),
sa.ForeignKeyConstraint(["user_name"], ["bw_ui_users.username"], onupdate="CASCADE", ondelete="CASCADE"),
)
op.create_table(
"bw_ui_roles_permissions",
sa.Column("role_name", sa.String(64), nullable=False),
sa.Column("permission_name", sa.String(64), nullable=False),
sa.ForeignKeyConstraint(["role_name"], ["bw_ui_roles.name"], onupdate="CASCADE", ondelete="CASCADE"),
sa.ForeignKeyConstraint(["permission_name"], ["bw_ui_permissions.name"], onupdate="CASCADE", ondelete="CASCADE"),
sa.PrimaryKeyConstraint("role_name", "permission_name"),
)
op.create_table(
"bw_ui_roles_users",
sa.Column("user_name", sa.String(256), nullable=False),
sa.Column("role_name", sa.String(64), nullable=False),
sa.ForeignKeyConstraint(["user_name"], ["bw_ui_users.username"], onupdate="CASCADE", ondelete="CASCADE"),
sa.ForeignKeyConstraint(["role_name"], ["bw_ui_roles.name"], onupdate="CASCADE", ondelete="CASCADE"),
sa.PrimaryKeyConstraint("user_name", "role_name"),
)
op.create_table(
"bw_ui_user_sessions",
sa.Column("id", sa.Integer(), sa.Identity(start=1, increment=1), primary_key=True),
sa.Column("user_name", sa.String(256), nullable=False),
sa.Column("ip", sa.String(39), nullable=False),
sa.Column("user_agent", sa.TEXT, nullable=False),
sa.Column("creation_date", sa.DateTime(timezone=True), nullable=False),
sa.Column("last_activity", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["user_name"], ["bw_ui_users.username"], onupdate="CASCADE", ondelete="CASCADE"),
)
from model import JSONText
tables_enum = postgresql.ENUM("bans", "configs", "instances", "jobs", "plugins", "reports", "services", name="tables_enum", create_type=False)
op.execute("CREATE TYPE tables_enum AS ENUM ('bans', 'configs', 'instances', 'jobs', 'plugins', 'reports', 'services')")
op.create_table(
"bw_ui_user_columns_preferences",
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
sa.Column("user_name", sa.String(256), nullable=False),
sa.Column("table_name", tables_enum, nullable=False),
sa.Column("columns", JSONText, nullable=False),
sa.ForeignKeyConstraint(["user_name"], ["bw_ui_users.username"], onupdate="CASCADE", ondelete="CASCADE"),
sa.UniqueConstraint("user_name", "table_name", name="uq_user_columns_preferences"),
)
# Templates tables
op.create_table(
"bw_templates",
sa.Column("id", sa.String(256), primary_key=True),
sa.Column("name", sa.String(256), unique=True, nullable=False),
sa.Column("plugin_id", sa.String(64), nullable=False),
sa.ForeignKeyConstraint(["plugin_id"], ["bw_plugins.id"], onupdate="CASCADE", ondelete="CASCADE"),
)
op.create_table(
"bw_template_steps",
sa.Column("id", sa.Integer(), primary_key=True),
sa.Column("template_id", sa.String(256), nullable=False, primary_key=True),
sa.Column("title", sa.Text, nullable=False),
sa.Column("subtitle", sa.Text, nullable=True),
sa.ForeignKeyConstraint(["template_id"], ["bw_templates.id"], onupdate="CASCADE", ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id", "template_id"),
)
op.create_table(
"bw_template_settings",
sa.Column("id", sa.Integer(), sa.Identity(start=1, increment=1), primary_key=True),
sa.Column("template_id", sa.String(256), nullable=False),
sa.Column("setting_id", sa.String(256), nullable=False),
sa.Column("step_id", sa.Integer(), nullable=True),
sa.Column("default", sa.Text, nullable=False),
sa.Column("suffix", sa.Integer(), nullable=True, default=0),
sa.ForeignKeyConstraint(["template_id"], ["bw_templates.id"], onupdate="CASCADE", ondelete="CASCADE"),
sa.ForeignKeyConstraint(["setting_id"], ["bw_settings.id"], onupdate="CASCADE", ondelete="CASCADE"),
sa.UniqueConstraint("template_id", "setting_id", "step_id", "suffix"),
)
op.create_table(
"bw_template_custom_configs",
sa.Column("id", sa.Integer(), sa.Identity(start=1, increment=1), primary_key=True),
sa.Column("template_id", sa.String(256), nullable=False),
sa.Column("step_id", sa.Integer(), nullable=True),
sa.Column("type", new_cct_enum, nullable=False),
sa.Column("name", sa.String(256), nullable=False),
sa.Column("data", sa.LargeBinary(length=(2**32) - 1), nullable=False),
sa.Column("checksum", sa.String(128), nullable=False),
sa.ForeignKeyConstraint(["template_id"], ["bw_templates.id"], onupdate="CASCADE", ondelete="CASCADE"),
sa.UniqueConstraint("template_id", "step_id", "type", "name"),
)
# bw_jobs changes (add run_async, remove success and last_run)
with op.batch_alter_table("bw_jobs") as batch_op:
batch_op.add_column(sa.Column("run_async", sa.Boolean(), nullable=False, server_default="0"))
batch_op.drop_column("success")
batch_op.drop_column("last_run")
op.create_table(
"bw_jobs_runs",
sa.Column("id", sa.Integer(), sa.Identity(start=1, increment=1), primary_key=True),
sa.Column("job_name", sa.String(128), nullable=False),
sa.Column("success", sa.Boolean(), nullable=True, server_default="0"),
sa.Column("start_date", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("CURRENT_TIMESTAMP")),
sa.Column("end_date", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("CURRENT_TIMESTAMP")),
sa.ForeignKeyConstraint(["job_name"], ["bw_jobs.name"], onupdate="CASCADE", ondelete="CASCADE"),
)
# bw_services add creation_date, last_update
with op.batch_alter_table("bw_services") as batch_op:
batch_op.add_column(sa.Column("creation_date", sa.DateTime(timezone=True), nullable=True, server_default=sa.text("CURRENT_TIMESTAMP")))
batch_op.add_column(sa.Column("last_update", sa.DateTime(timezone=True), nullable=True, server_default=sa.text("CURRENT_TIMESTAMP")))
with op.batch_alter_table("bw_services") as batch_op:
batch_op.alter_column("creation_date", existing_type=sa.DateTime(timezone=True), nullable=False)
batch_op.alter_column("last_update", existing_type=sa.DateTime(timezone=True), nullable=False)
# bw_ui_users changes
themes_enum = postgresql.ENUM("light", "dark", name="themes_enum", create_type=False)
op.execute("CREATE TYPE themes_enum AS ENUM ('light', 'dark')")
with op.batch_alter_table("bw_ui_users") as batch_op:
batch_op.add_column(sa.Column("email", sa.String(256), nullable=True, unique=True))
batch_op.add_column(sa.Column("admin", sa.Boolean(), nullable=False, server_default="0"))
batch_op.add_column(sa.Column("theme", themes_enum, nullable=False, server_default="light"))
batch_op.add_column(sa.Column("totp_secret", sa.String(256), nullable=True))
batch_op.add_column(sa.Column("creation_date", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("CURRENT_TIMESTAMP")))
batch_op.add_column(sa.Column("update_date", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("CURRENT_TIMESTAMP")))
batch_op.drop_column("is_two_factor_enabled")
batch_op.drop_column("secret_token")
op.execute("UPDATE bw_ui_users SET admin=TRUE, theme='light', creation_date=CURRENT_TIMESTAMP, update_date=CURRENT_TIMESTAMP")
# bw_plugin_pages changes
with op.batch_alter_table("bw_plugin_pages") as batch_op:
batch_op.add_column(sa.Column("data", sa.LargeBinary(length=(2**32) - 1), nullable=False))
batch_op.add_column(sa.Column("checksum", sa.String(128), nullable=False))
op.execute(
"""
UPDATE bw_plugin_pages
SET data = template_file, checksum = template_checksum
WHERE template_file IS NOT NULL
"""
)
with op.batch_alter_table("bw_plugin_pages") as batch_op:
batch_op.drop_column("template_file")
batch_op.drop_column("template_checksum")
batch_op.drop_column("actions_file")
batch_op.drop_column("actions_checksum")
batch_op.drop_column("obfuscation_file")
batch_op.drop_column("obfuscation_checksum")
# bw_instances changes
old_methods_enum_for_instances = postgresql.ENUM("ui", "scheduler", "autoconf", "manual", name="methods_enum", create_type=False)
# Add new enum values for instances if needed before altering columns
op.execute("ALTER TYPE methods_enum ADD VALUE IF NOT EXISTS 'wizard'")
new_it_enum = postgresql.ENUM("static", "container", "pod", name="instance_type_enum", create_type=False)
new_is_enum = postgresql.ENUM("loading", "up", "down", name="instance_status_enum", create_type=False)
op.execute("CREATE TYPE instance_type_enum AS ENUM ('static', 'container', 'pod')")
op.execute("CREATE TYPE instance_status_enum AS ENUM ('loading', 'up', 'down')")
extended_methods_enum = postgresql.ENUM("ui", "scheduler", "autoconf", "manual", "wizard", name="methods_enum", create_type=False)
with op.batch_alter_table("bw_instances") as batch_op:
batch_op.add_column(sa.Column("name", sa.String(256), nullable=True))
batch_op.add_column(sa.Column("type", new_it_enum, nullable=True))
batch_op.add_column(sa.Column("status", new_it_enum, nullable=True))
batch_op.add_column(sa.Column("method", old_methods_enum_for_instances, nullable=True))
batch_op.add_column(sa.Column("creation_date", sa.DateTime(timezone=True), nullable=True, server_default=sa.text("CURRENT_TIMESTAMP")))
batch_op.add_column(sa.Column("last_seen", sa.DateTime(timezone=True), nullable=True, server_default=sa.text("CURRENT_TIMESTAMP")))
op.execute("DELETE FROM bw_instances WHERE name IS NULL")
op.alter_column(
"bw_instances",
"method",
existing_type=old_methods_enum_for_instances,
type_=extended_methods_enum,
existing_nullable=False,
existing_server_default="manual",
)
with op.batch_alter_table("bw_instances") as batch_op:
batch_op.alter_column("name", existing_type=sa.String(256), nullable=False)
batch_op.alter_column("type", existing_type=new_it_enum, nullable=False)
batch_op.alter_column("status", existing_type=new_is_enum, nullable=False)
batch_op.alter_column("method", existing_type=extended_methods_enum, nullable=False)
batch_op.alter_column("creation_date", existing_type=sa.DateTime(timezone=True), nullable=False)
batch_op.alter_column("last_seen", existing_type=sa.DateTime(timezone=True), nullable=False)
# Update version
op.execute("UPDATE bw_metadata SET version = '1.6.0-beta' WHERE id = 1")
def downgrade():
# Reverse changes:
# 1. Drop added columns from bw_instances
with op.batch_alter_table("bw_instances") as batch_op:
batch_op.drop_column("last_seen")
batch_op.drop_column("creation_date")
batch_op.drop_column("method")
batch_op.drop_column("status")
batch_op.drop_column("type")
batch_op.drop_column("name")
# 2. Revert bw_plugin_pages to old structure
op.create_table(
"bw_plugin_pages_old",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("plugin_id", sa.String(64), nullable=False),
sa.Column("template_file", sa.LargeBinary(length=(2**32)), nullable=False),
sa.Column("template_checksum", sa.String(128), nullable=False),
sa.Column("actions_file", sa.LargeBinary(length=(2**32)), nullable=False),
sa.Column("actions_checksum", sa.String(128), nullable=False),
sa.ForeignKeyConstraint(["plugin_id"], ["bw_plugins.id"], onupdate="CASCADE", ondelete="CASCADE"),
)
op.execute(
"""
INSERT INTO bw_plugin_pages_old (id, plugin_id, template_file, template_checksum, actions_file, actions_checksum)
SELECT id, plugin_id, NULL, '', NULL, ''
FROM bw_plugin_pages
"""
)
op.drop_table("bw_plugin_pages")
op.rename_table("bw_plugin_pages_old", "bw_plugin_pages")
# 3. Revert bw_ui_users to old structure
op.create_table(
"bw_ui_users_old",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("username", sa.String(256), nullable=False, unique=True),
sa.Column("password", sa.String(60), nullable=False),
sa.Column("is_two_factor_enabled", sa.Boolean(), nullable=False, server_default="0"),
sa.Column("secret_token", sa.String(32), nullable=True, unique=True, default=None),
sa.Column("method", sa.String(16), nullable=False, server_default="manual"),
)
op.execute(
"""
INSERT INTO bw_ui_users_old (id, username, password, is_two_factor_enabled, secret_token, method)
SELECT 1, username, password, '0', NULL, 'manual'
FROM bw_ui_users
"""
)
op.drop_table("bw_ui_users")
op.rename_table("bw_ui_users_old", "bw_ui_users")
# 4. Drop new UI and templates tables
op.drop_table("bw_template_custom_configs")
op.drop_table("bw_template_settings")
op.drop_table("bw_template_steps")
op.drop_table("bw_templates")
op.drop_table("bw_ui_user_columns_preferences")
op.drop_table("bw_ui_user_sessions")
op.drop_table("bw_ui_roles_users")
op.drop_table("bw_ui_roles_permissions")
op.drop_table("bw_ui_user_recovery_codes")
op.drop_table("bw_ui_roles")
op.drop_table("bw_ui_permissions")
# 5. Drop new enums
op.execute("DROP TYPE IF EXISTS themes_enum")
op.execute("DROP TYPE IF EXISTS tables_enum")
op.execute("DROP TYPE IF EXISTS instance_status_enum")
# 6. Drop new enum values
op.execute("ALTER TYPE methods_enum DROP VALUE IF EXISTS 'wizard'")
op.execute("ALTER TYPE custom_configs_types_enum DROP VALUE IF EXISTS 'default_server_stream'")
op.execute("ALTER TYPE custom_configs_types_enum DROP VALUE IF EXISTS 'crs_plugins_before'")
op.execute("ALTER TYPE custom_configs_types_enum DROP VALUE IF EXISTS 'crs_plugins_after'")
op.execute("ALTER TYPE plugin_types_enum DROP VALUE IF EXISTS 'ui'")
# 7. Revert version
op.execute("UPDATE bw_metadata SET version = '1.5.12' WHERE id = 1")

View file

@ -0,0 +1,56 @@
"""Upgrade to version 1.5.1
Revision ID: 3133d7320b63
Revises: 4badb3a66963
Create Date: 2024-12-19 13:39:17.647342
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "3133d7320b63"
down_revision: Union[str, None] = "4badb3a66963"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Add new columns to bw_metadata."""
with op.batch_alter_table("bw_metadata") as batch_op:
batch_op.add_column(sa.Column("scheduler_first_start", sa.Boolean(), nullable=True))
batch_op.add_column(sa.Column("custom_configs_changed", sa.Boolean(), nullable=True))
batch_op.add_column(sa.Column("external_plugins_changed", sa.Boolean(), nullable=True))
batch_op.add_column(sa.Column("config_changed", sa.Boolean(), nullable=True))
batch_op.add_column(sa.Column("instances_changed", sa.Boolean(), nullable=True))
# Update all new columns and version in a single statement
op.execute(
"""
UPDATE bw_metadata
SET scheduler_first_start = false,
custom_configs_changed = false,
external_plugins_changed = false,
config_changed = false,
instances_changed = false,
version = '1.5.1'
WHERE id = 1
"""
)
def downgrade() -> None:
"""Remove new columns from bw_metadata."""
with op.batch_alter_table("bw_metadata") as batch_op:
batch_op.drop_column("instances_changed")
batch_op.drop_column("config_changed")
batch_op.drop_column("external_plugins_changed")
batch_op.drop_column("custom_configs_changed")
batch_op.drop_column("scheduler_first_start")
# Revert the version back to 1.5.0
op.execute("UPDATE bw_metadata SET version = '1.5.0' WHERE id = 1")

View file

@ -0,0 +1,29 @@
"""Upgrade to version 1.5.2
Revision ID: 4a2457daed53
Revises: 3133d7320b63
Create Date: 2024-12-19 13:40:12.477741
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "4a2457daed53"
down_revision: Union[str, None] = "3133d7320b63"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Data migration: Update the version to 1.5.2
op.execute("UPDATE bw_metadata SET version = '1.5.2' WHERE id = 1")
def downgrade() -> None:
# Revert the version back to 1.5.1
op.execute("UPDATE bw_metadata SET version = '1.5.1' WHERE id = 1")

View file

@ -0,0 +1,42 @@
"""Upgrade to version 1.5.0
Revision ID: 4badb3a66963
Revises:
Create Date: 2024-12-19 13:36:21.097290
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "4badb3a66963"
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.drop_column("bw_plugins", "order")
# Data migration: Update the version to 1.5.0
op.execute("UPDATE bw_metadata SET version = '1.5.0' WHERE id = 1")
def downgrade() -> None:
# Step 1: Add 'order' column back to 'bw_plugins' (with a default value of 0 for NOT NULL constraint)
with op.batch_alter_table("bw_plugins") as batch_op:
batch_op.add_column(sa.Column("order", sa.Integer(), autoincrement=False, nullable=True, server_default="0"))
# Step 2: Set default value for existing rows
op.execute("UPDATE bw_plugins SET `order` = 0")
# Step 3: Alter 'order' column to NOT NULL
with op.batch_alter_table("bw_plugins") as batch_op:
batch_op.alter_column("order", nullable=False)
# Data migration: Update the version to 1.5.0-beta
op.execute("UPDATE bw_metadata SET version = '1.5.0-beta' WHERE id = 1")

View file

@ -0,0 +1,105 @@
"""Upgrade to version 1.5.7
Revision ID: 5201c88f004d
Revises: b4abd1acf9f1
Create Date: 2024-12-19 14:36:04.319448
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "5201c88f004d"
down_revision: Union[str, None] = "b4abd1acf9f1"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
conn = op.get_bind()
# Create new table for bw_cli_commands
op.create_table(
"bw_cli_commands",
sa.Column("id", sa.Integer(), sa.Identity(always=False, start=1, increment=1), nullable=False),
sa.Column("name", sa.String(length=64), nullable=False),
sa.Column("plugin_id", sa.String(length=64), nullable=False),
sa.Column("file_name", sa.String(length=256), nullable=False),
sa.ForeignKeyConstraint(["plugin_id"], ["bw_plugins.id"], onupdate="cascade", ondelete="cascade"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("plugin_id", "name"),
)
# Handle foreign key constraints for bw_jobs_cache
fk_result = conn.execute(
sa.text(
"""
SELECT conname
FROM pg_constraint
WHERE conrelid = 'bw_jobs_cache'::regclass
AND confrelid = 'bw_jobs'::regclass
AND conname = 'fk_bw_jobs_cache_job_name'
"""
)
).fetchone()
if fk_result:
op.drop_constraint("fk_bw_jobs_cache_job_name", "bw_jobs_cache", type_="foreignkey")
op.create_foreign_key(None, "bw_jobs_cache", "bw_jobs", ["job_name"], ["name"], onupdate="cascade", ondelete="cascade")
# Add new columns to bw_plugin_pages
op.add_column("bw_plugin_pages", sa.Column("obfuscation_file", sa.LargeBinary(length=4294967295), nullable=True))
op.add_column("bw_plugin_pages", sa.Column("obfuscation_checksum", sa.String(length=128), nullable=True))
# Add the new order column to bw_settings
# Step 1: Add column as nullable
op.add_column("bw_settings", sa.Column("order", sa.Integer(), nullable=True))
# Step 2: Populate default values
op.execute('UPDATE bw_settings SET "order" = 0')
# Step 3: Alter column to NOT NULL
op.alter_column("bw_settings", "order", nullable=False)
# Update version in bw_metadata
op.execute("UPDATE bw_metadata SET version = '1.5.7' WHERE id = 1")
def downgrade() -> None:
conn = op.get_bind()
# Reverse the version update
op.execute("UPDATE bw_metadata SET version = '1.5.6' WHERE id = 1")
# Reverse the addition of the order column
op.drop_column("bw_settings", "order")
# Reverse changes in bw_plugin_pages
op.drop_column("bw_plugin_pages", "obfuscation_checksum")
op.drop_column("bw_plugin_pages", "obfuscation_file")
# Restore foreign key constraints for bw_jobs_cache
fk_result = conn.execute(
sa.text(
"""
SELECT conname
FROM pg_constraint
WHERE conrelid = 'bw_jobs_cache'::regclass
AND confrelid = 'bw_jobs'::regclass
AND conname IS NULL
"""
)
).fetchone()
if fk_result:
op.drop_constraint(None, "bw_jobs_cache", type_="foreignkey")
op.create_foreign_key("fk_bw_jobs_cache_job_name", "bw_jobs_cache", "bw_jobs", ["job_name"], ["name"])
# Drop the newly created table bw_cli_commands
op.drop_table("bw_cli_commands")

View file

@ -0,0 +1,42 @@
"""Upgrade to version 1.5.4
Revision ID: 6b1db23ec3ab
Revises: e9b703e3d747
Create Date: 2024-12-19 13:46:07.676758
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "6b1db23ec3ab"
down_revision: Union[str, None] = "e9b703e3d747"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Create new table 'bw_ui_users'
op.create_table(
"bw_ui_users",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("username", sa.String(length=256), nullable=False),
sa.Column("password", sa.String(length=60), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("username"),
)
# Update metadata version
op.execute("UPDATE bw_metadata SET version = '1.5.4' WHERE id = 1")
def downgrade() -> None:
# Drop the table 'bw_ui_users'
op.drop_table("bw_ui_users")
# Revert metadata version
op.execute("UPDATE bw_metadata SET version = '1.5.3' WHERE id = 1")

View file

@ -0,0 +1,54 @@
"""Upgrade to version 1.5.5
Revision ID: 7deca2941c74
Revises: 6b1db23ec3ab
Create Date: 2024-12-19 13:47:10.128402
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "7deca2941c74"
down_revision: Union[str, None] = "6b1db23ec3ab"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade():
# Add new columns to bw_ui_users
op.add_column("bw_ui_users", sa.Column("is_two_factor_enabled", sa.Boolean(), nullable=False, server_default=sa.false()))
op.add_column("bw_ui_users", sa.Column("secret_token", sa.String(length=32), nullable=True))
op.add_column(
"bw_ui_users", sa.Column("method", sa.Enum("ui", "scheduler", "autoconf", "manual", name="methods_enum"), nullable=False, server_default="manual")
)
op.create_unique_constraint("uq_bw_ui_users_secret_token", "bw_ui_users", ["secret_token"])
# Increase column sizes
op.alter_column("bw_global_values", "value", existing_type=sa.VARCHAR(length=4096), type_=sa.String(length=8192), existing_nullable=False)
op.alter_column("bw_services_settings", "value", existing_type=sa.VARCHAR(length=4096), type_=sa.String(length=8192), existing_nullable=False)
# Update all new columns in a single statement
op.execute("UPDATE bw_ui_users SET is_two_factor_enabled = false, method = 'manual'")
# Update version in bw_metadata
op.execute("UPDATE bw_metadata SET version = '1.5.5' WHERE id = 1")
def downgrade():
# Revert changes to 'bw_ui_users'
op.drop_constraint("uq_bw_ui_users_secret_token", "bw_ui_users", type_="unique")
op.drop_column("bw_ui_users", "method")
op.drop_column("bw_ui_users", "secret_token")
op.drop_column("bw_ui_users", "is_two_factor_enabled")
# Revert column sizes for VARCHAR
op.alter_column("bw_global_values", "value", existing_type=sa.String(length=8192), type_=sa.VARCHAR(length=4096), existing_nullable=False)
op.alter_column("bw_services_settings", "value", existing_type=sa.String(length=8192), type_=sa.VARCHAR(length=4096), existing_nullable=False)
# Revert version in bw_metadata
op.execute("UPDATE bw_metadata SET version = '1.5.4' WHERE id = 1")

View file

@ -0,0 +1,287 @@
"""Upgrade to version 1.6.0-rc1
Revision ID: 940350925f36
Revises: 0b08c406d820
Create Date: 2024-12-20 11:02:59.703530
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = "940350925f36"
down_revision: Union[str, None] = "0b08c406d820"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Drop foreign keys referencing bw_services
with op.batch_alter_table("bw_custom_configs") as batch_op:
batch_op.drop_constraint("bw_custom_configs_service_id_fkey", type_="foreignkey")
with op.batch_alter_table("bw_services_settings") as batch_op:
batch_op.drop_constraint("bw_services_settings_service_id_fkey", type_="foreignkey")
with op.batch_alter_table("bw_jobs_cache") as batch_op:
batch_op.drop_constraint("bw_jobs_cache_service_id_fkey", type_="foreignkey")
# Create the new bw_services table with updated schema
op.create_table(
"bw_services_new",
sa.Column("id", sa.String(256), primary_key=True),
sa.Column("method", postgresql.ENUM("ui", "scheduler", "autoconf", "manual", "wizard", name="methods_enum", create_type=False), nullable=False),
sa.Column("is_draft", sa.Boolean, default=False, nullable=False),
sa.Column("creation_date", sa.DateTime(timezone=True), nullable=False),
sa.Column("last_update", sa.DateTime(timezone=True), nullable=False),
)
# Copy data from old bw_services to bw_services_new
op.execute(
"""
INSERT INTO bw_services_new (id, method, is_draft, creation_date, last_update)
SELECT id, method, is_draft, creation_date, last_update
FROM bw_services
"""
)
# Drop old bw_services table now that foreign keys are removed
op.drop_table("bw_services")
# Rename new table to bw_services
op.rename_table("bw_services_new", "bw_services")
# bw_services_settings
op.create_table(
"bw_services_settings_new",
sa.Column("service_id", sa.String(256), nullable=False),
sa.Column("setting_id", sa.String(256), nullable=False),
sa.Column("value", sa.TEXT, nullable=False),
sa.Column("suffix", sa.Integer, nullable=True, default=0),
sa.Column("method", postgresql.ENUM("ui", "scheduler", "autoconf", "manual", "wizard", name="methods_enum", create_type=False), nullable=False),
sa.PrimaryKeyConstraint("service_id", "setting_id", "suffix"),
)
op.execute(
"""
INSERT INTO bw_services_settings_new (service_id, setting_id, value, suffix, method)
SELECT service_id, setting_id, value, suffix, method FROM bw_services_settings
"""
)
op.drop_table("bw_services_settings")
op.rename_table("bw_services_settings_new", "bw_services_settings")
# bw_custom_configs
op.create_table(
"bw_custom_configs_new",
sa.Column("id", sa.Integer, sa.Identity(start=1, increment=1), primary_key=True),
sa.Column("service_id", sa.String(256), nullable=True),
sa.Column(
"type",
postgresql.ENUM(
"http",
"stream",
"server_http",
"server_stream",
"default_server_http",
"default_server_stream",
"modsec",
"modsec_crs",
"crs_plugins_before",
"crs_plugins_after",
name="custom_configs_types_enum",
create_type=False,
),
nullable=False,
),
sa.Column("name", sa.String(256), nullable=False),
sa.Column("data", sa.LargeBinary(length=(2**32) - 1), nullable=False),
sa.Column("checksum", sa.String(128), nullable=False),
sa.Column("method", postgresql.ENUM("ui", "scheduler", "autoconf", "manual", "wizard", name="methods_enum", create_type=False), nullable=False),
sa.UniqueConstraint("service_id", "type", "name"),
)
op.execute(
"""
INSERT INTO bw_custom_configs_new (id, service_id, type, name, data, checksum, method)
SELECT id, service_id, type, name, data, checksum, method
FROM bw_custom_configs
"""
)
op.drop_table("bw_custom_configs")
op.rename_table("bw_custom_configs_new", "bw_custom_configs")
# bw_jobs_cache
op.create_table(
"bw_jobs_cache_new",
sa.Column("id", sa.Integer, sa.Identity(start=1, increment=1), primary_key=True),
sa.Column("job_name", sa.String(128), nullable=False),
sa.Column("service_id", sa.String(256), nullable=True),
sa.Column("file_name", sa.String(256), nullable=False),
sa.Column("data", sa.LargeBinary(length=(2**32) - 1), nullable=True),
sa.Column("last_update", sa.DateTime(timezone=True), nullable=True),
sa.Column("checksum", sa.String(128), nullable=True),
)
op.execute(
"""
INSERT INTO bw_jobs_cache_new (id, job_name, service_id, file_name, data, last_update, checksum)
SELECT id, job_name, service_id, file_name, data, last_update, checksum FROM bw_jobs_cache
"""
)
op.drop_table("bw_jobs_cache")
op.rename_table("bw_jobs_cache_new", "bw_jobs_cache")
# Recreate foreign keys referencing bw_services now that bw_services is updated
with op.batch_alter_table("bw_custom_configs") as batch_op:
batch_op.create_foreign_key("bw_custom_configs_service_id_fkey", "bw_services", ["service_id"], ["id"], onupdate="CASCADE", ondelete="CASCADE")
with op.batch_alter_table("bw_services_settings") as batch_op:
batch_op.create_foreign_key("bw_services_settings_service_id_fkey", "bw_services", ["service_id"], ["id"], onupdate="CASCADE", ondelete="CASCADE")
with op.batch_alter_table("bw_jobs_cache") as batch_op:
batch_op.create_foreign_key("bw_jobs_cache_service_id_fkey", "bw_services", ["service_id"], ["id"], onupdate="CASCADE", ondelete="CASCADE")
# Update the version in bw_metadata
op.execute("UPDATE bw_metadata SET version = '1.6.0-rc1' WHERE id = 1")
def downgrade() -> None:
# Drop foreign keys referencing bw_services before reverting changes
with op.batch_alter_table("bw_jobs_cache") as batch_op:
batch_op.drop_constraint("bw_jobs_cache_service_id_fkey", type_="foreignkey")
with op.batch_alter_table("bw_services_settings") as batch_op:
batch_op.drop_constraint("bw_services_settings_service_id_fkey", type_="foreignkey")
with op.batch_alter_table("bw_custom_configs") as batch_op:
batch_op.drop_constraint("bw_custom_configs_service_id_fkey", type_="foreignkey")
# Revert bw_jobs_cache
op.create_table(
"bw_jobs_cache_old",
sa.Column("id", sa.Integer, sa.Identity(start=1, increment=1), primary_key=True),
sa.Column("job_name", sa.String(128), nullable=False),
sa.Column("service_id", sa.String(64), nullable=True),
sa.Column("file_name", sa.String(256), nullable=False),
sa.Column("data", sa.LargeBinary(length=(2**32) - 1), nullable=True),
sa.Column("last_update", sa.DateTime(timezone=True), nullable=True),
sa.Column("checksum", sa.String(128), nullable=True),
)
op.execute(
"""
INSERT INTO bw_jobs_cache_old (id, job_name, service_id, file_name, data, last_update, checksum)
SELECT id, job_name, service_id, file_name, data, last_update, checksum FROM bw_jobs_cache
"""
)
op.drop_table("bw_jobs_cache")
op.rename_table("bw_jobs_cache_old", "bw_jobs_cache")
# Revert bw_custom_configs
op.create_table(
"bw_custom_configs_old",
sa.Column("id", sa.Integer, sa.Identity(start=1, increment=1), primary_key=True),
sa.Column("service_id", sa.String(64), nullable=True),
sa.Column(
"type",
postgresql.ENUM(
"http",
"stream",
"server_http",
"server_stream",
"default_server_http",
"default_server_stream",
"modsec",
"modsec_crs",
"crs_plugins_before",
"crs_plugins_after",
name="custom_configs_types_enum",
create_type=False,
),
nullable=False,
),
sa.Column("name", sa.String(256), nullable=False),
sa.Column("data", sa.LargeBinary(length=(2**32) - 1), nullable=False),
sa.Column("checksum", sa.String(128), nullable=False),
sa.Column(
"method",
postgresql.ENUM("ui", "scheduler", "autoconf", "manual", "wizard", name="methods_enum", create_type=False),
nullable=False,
),
sa.UniqueConstraint("service_id", "type", "name"),
)
op.execute(
"""
INSERT INTO bw_custom_configs_old (id, service_id, type, name, data, checksum, method)
SELECT id, service_id, type, name, data, checksum, method FROM bw_custom_configs
"""
)
op.drop_table("bw_custom_configs")
op.rename_table("bw_custom_configs_old", "bw_custom_configs")
# Revert bw_services_settings
op.create_table(
"bw_services_settings_old",
sa.Column("service_id", sa.String(64), nullable=False),
sa.Column("setting_id", sa.String(256), nullable=False),
sa.Column("value", sa.TEXT, nullable=False),
sa.Column("suffix", sa.Integer, nullable=True, default=0),
sa.Column(
"method",
postgresql.ENUM("ui", "scheduler", "autoconf", "manual", "wizard", name="methods_enum", create_type=False),
nullable=False,
),
sa.PrimaryKeyConstraint("service_id", "setting_id", "suffix"),
)
op.execute(
"""
INSERT INTO bw_services_settings_old (service_id, setting_id, value, suffix, method)
SELECT service_id, setting_id, value, suffix, method FROM bw_services_settings
"""
)
op.drop_table("bw_services_settings")
op.rename_table("bw_services_settings_old", "bw_services_settings")
# Revert bw_services
op.create_table(
"bw_services_old",
sa.Column("id", sa.String(64), primary_key=True),
sa.Column("method", postgresql.ENUM("ui", "scheduler", "autoconf", "manual", "wizard", name="methods_enum", create_type=False), nullable=False),
sa.Column("is_draft", sa.Boolean, default=False, nullable=False),
sa.Column("creation_date", sa.DateTime(timezone=True), nullable=False),
sa.Column("last_update", sa.DateTime(timezone=True), nullable=False),
)
op.execute(
"""
INSERT INTO bw_services_old (id, method, is_draft, creation_date, last_update)
SELECT id, method, is_draft, creation_date, last_update FROM bw_services
"""
)
op.drop_table("bw_services")
op.rename_table("bw_services_old", "bw_services")
# Recreate foreign keys referencing bw_services
with op.batch_alter_table("bw_custom_configs") as batch_op:
batch_op.create_foreign_key("bw_custom_configs_service_id_fkey", "bw_services", ["service_id"], ["id"], onupdate="CASCADE", ondelete="CASCADE")
with op.batch_alter_table("bw_services_settings") as batch_op:
batch_op.create_foreign_key("bw_services_settings_service_id_fkey", "bw_services", ["service_id"], ["id"], onupdate="CASCADE", ondelete="CASCADE")
with op.batch_alter_table("bw_jobs_cache") as batch_op:
batch_op.create_foreign_key("bw_jobs_cache_service_id_fkey", "bw_services", ["service_id"], ["id"], onupdate="CASCADE", ondelete="CASCADE")
# Revert version
op.execute("UPDATE bw_metadata SET version = '1.6.0-beta' WHERE id = 1")

View file

@ -0,0 +1,210 @@
"""Upgrade to version 1.5.6
Revision ID: b4abd1acf9f1
Revises: 7deca2941c74
Create Date: 2024-12-19 13:49:55.657684
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "b4abd1acf9f1"
down_revision: Union[str, None] = "7deca2941c74"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade():
# Create enums
op.execute("CREATE TYPE pro_status_enum AS ENUM ('active', 'invalid', 'expired', 'suspended')")
op.execute("CREATE TYPE plugin_types_enum AS ENUM ('core', 'external', 'pro')")
op.execute("CREATE TYPE stream_types_enum AS ENUM ('no', 'yes', 'partial')")
# Drop the foreign key constraint dynamically
conn = op.get_bind()
result = conn.execute(
sa.text(
"""
SELECT conname
FROM pg_constraint
WHERE conrelid = 'bw_jobs_cache'::regclass
AND confrelid = 'bw_jobs'::regclass
AND conname LIKE '%_job_name%'
"""
)
).fetchone()
if result:
constraint_name = result[0]
with op.batch_alter_table("bw_jobs_cache") as batch_op:
batch_op.drop_constraint(constraint_name, type_="foreignkey")
# Drop index dynamically if it exists
index_result = conn.execute(
sa.text(
"""
SELECT indexname
FROM pg_indexes
WHERE tablename = 'bw_jobs_cache'
AND indexname = 'job_name'
"""
)
).fetchone()
if index_result:
with op.batch_alter_table("bw_jobs_cache") as batch_op:
batch_op.drop_index("job_name")
# Add new foreign key
with op.batch_alter_table("bw_jobs_cache") as batch_op:
batch_op.create_foreign_key("fk_bw_jobs_cache_job_name", "bw_jobs", ["job_name"], ["name"])
# Add new columns and alter existing ones
op.add_column("bw_metadata", sa.Column("is_pro", sa.Boolean(), nullable=False, server_default=sa.false()))
op.add_column("bw_metadata", sa.Column("pro_expire", sa.DateTime(), nullable=True))
op.add_column(
"bw_metadata",
sa.Column("pro_status", sa.Enum("active", "invalid", "expired", "suspended", name="pro_status_enum"), nullable=False, server_default="invalid"),
)
op.add_column("bw_metadata", sa.Column("pro_services", sa.Integer(), nullable=False, server_default="0"))
op.add_column("bw_metadata", sa.Column("pro_overlapped", sa.Boolean(), nullable=False, server_default=sa.false()))
op.add_column("bw_metadata", sa.Column("last_pro_check", sa.DateTime(), nullable=True))
op.add_column("bw_metadata", sa.Column("pro_plugins_changed", sa.Boolean(), nullable=True))
op.add_column("bw_services", sa.Column("is_draft", sa.Boolean(), nullable=False, server_default=sa.false()))
op.add_column("bw_plugins", sa.Column("type", sa.Enum("core", "external", "pro", name="plugin_types_enum"), nullable=False, server_default="core"))
# Alter the `stream` column with explicit casting
op.execute(
"""
ALTER TABLE bw_plugins
ALTER COLUMN stream TYPE stream_types_enum
USING stream::text::stream_types_enum
"""
)
# Migrate data: Set 'type' to 'external' where 'external' was true
op.execute(
"""
UPDATE bw_plugins
SET type = 'external'
WHERE external = true
"""
)
op.drop_column("bw_plugins", "external")
op.alter_column("bw_global_values", "value", existing_type=sa.VARCHAR(length=8192), type_=sa.TEXT(), existing_nullable=False)
op.alter_column("bw_services_settings", "value", existing_type=sa.VARCHAR(length=8192), type_=sa.TEXT(), existing_nullable=False)
# Drop indices dynamically if they exist
for table, index_name in (("bw_jobs", "name"), ("bw_settings", "name")):
index_exists = conn.execute(
sa.text(
f"""
SELECT indexname
FROM pg_indexes
WHERE tablename = '{table}'
AND indexname = '{index_name}'
"""
)
).fetchone()
if index_exists:
op.drop_index(index_name, table_name=table)
# Update all new columns and version in a single statement
op.execute(
"""
UPDATE bw_metadata
SET is_pro = false,
pro_status = 'invalid',
pro_services = 0,
pro_overlapped = false,
pro_plugins_changed = false,
version = '1.5.6'
WHERE id = 1
"""
)
def downgrade():
# Drop columns
op.drop_column("bw_metadata", "pro_plugins_changed")
op.drop_column("bw_metadata", "last_pro_check")
op.drop_column("bw_metadata", "pro_overlapped")
op.drop_column("bw_metadata", "pro_services")
op.drop_column("bw_metadata", "pro_status")
op.drop_column("bw_metadata", "pro_expire")
op.drop_column("bw_metadata", "is_pro")
op.drop_column("bw_services", "is_draft")
op.add_column("bw_plugins", sa.Column("external", sa.Boolean(), autoincrement=False, nullable=False))
# Migrate data: Set 'external' to true where 'type' was 'external'
op.execute(
"""
UPDATE bw_plugins
SET external = true
WHERE type = 'external'
"""
)
op.drop_column("bw_plugins", "type")
# Revert the `stream` column back to VARCHAR
op.execute(
"""
ALTER TABLE bw_plugins
ALTER COLUMN stream TYPE VARCHAR(16)
USING stream::text
"""
)
op.alter_column(
"bw_plugins", "stream", existing_type=sa.Enum("no", "yes", "partial", name="stream_types_enum"), type_=sa.VARCHAR(length=16), existing_nullable=False
)
op.alter_column("bw_global_values", "value", existing_type=sa.TEXT(), type_=sa.VARCHAR(length=8192), existing_nullable=False)
op.alter_column("bw_services_settings", "value", existing_type=sa.TEXT(), type_=sa.VARCHAR(length=8192), existing_nullable=False)
# Recreate indices
op.create_index("name", "bw_jobs", ["name", "plugin_id"], unique=True)
op.create_index("name", "bw_settings", ["name"], unique=True)
# Drop the enum types
op.execute("DROP TYPE IF EXISTS pro_status_enum")
op.execute("DROP TYPE IF EXISTS plugin_types_enum")
op.execute("DROP TYPE IF EXISTS stream_types_enum")
# Recreate foreign key dynamically
result = (
op.get_bind()
.execute(
sa.text(
"""
SELECT conname
FROM pg_constraint
WHERE conrelid = 'bw_jobs_cache'::regclass
AND conname = 'fk_bw_jobs_cache_job_name'
"""
)
)
.fetchone()
)
if result:
with op.batch_alter_table("bw_jobs_cache") as batch_op:
batch_op.drop_constraint("fk_bw_jobs_cache_job_name", type_="foreignkey")
with op.batch_alter_table("bw_jobs_cache") as batch_op:
batch_op.create_index("job_name", ["job_name", "service_id", "file_name"], unique=True)
batch_op.create_foreign_key("bw_jobs_cache_ibfk_1", "bw_jobs", ["job_name"], ["name"])
op.execute("UPDATE bw_metadata SET version = '1.5.5' WHERE id = 1")

View file

@ -0,0 +1,62 @@
"""Upgrade to version 1.5.8
Revision ID: ba43081c6f96
Revises: 5201c88f004d
Create Date: 2024-12-19 14:40:08.719321
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "ba43081c6f96"
down_revision: Union[str, None] = "5201c88f004d"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Add new columns to 'bw_metadata'
with op.batch_alter_table("bw_metadata") as batch_op:
batch_op.add_column(sa.Column("pro_license", sa.String(length=128), nullable=True))
batch_op.add_column(sa.Column("last_custom_configs_change", sa.DateTime(), nullable=True))
batch_op.add_column(sa.Column("last_external_plugins_change", sa.DateTime(), nullable=True))
batch_op.add_column(sa.Column("last_pro_plugins_change", sa.DateTime(), nullable=True))
batch_op.add_column(sa.Column("last_instances_change", sa.DateTime(), nullable=True))
batch_op.add_column(sa.Column("failover", sa.Boolean(), nullable=True))
batch_op.drop_column("config_changed")
# Add new columns to 'bw_plugins'
with op.batch_alter_table("bw_plugins") as batch_op:
batch_op.add_column(sa.Column("config_changed", sa.Boolean(), nullable=True))
batch_op.add_column(sa.Column("last_config_change", sa.DateTime(), nullable=True))
# Set default value for 'config_changed' in 'bw_plugins'
op.execute("UPDATE bw_plugins SET config_changed = false")
# Update version in 'bw_metadata'
op.execute("UPDATE bw_metadata SET version = '1.5.8' WHERE id = 1")
def downgrade() -> None:
# Revert 'bw_plugins' changes
with op.batch_alter_table("bw_plugins") as batch_op:
batch_op.drop_column("last_config_change")
batch_op.drop_column("config_changed")
# Revert 'bw_metadata' changes
with op.batch_alter_table("bw_metadata") as batch_op:
batch_op.add_column(sa.Column("config_changed", sa.Boolean(), nullable=True))
batch_op.drop_column("failover")
batch_op.drop_column("last_instances_change")
batch_op.drop_column("last_pro_plugins_change")
batch_op.drop_column("last_external_plugins_change")
batch_op.drop_column("last_custom_configs_change")
batch_op.drop_column("pro_license")
# Revert version in 'bw_metadata'
op.execute("UPDATE bw_metadata SET version = '1.5.7', config_changed = false WHERE id = 1")

View file

@ -0,0 +1,29 @@
"""Upgrade to version 1.5.3
Revision ID: e9b703e3d747
Revises: 4a2457daed53
Create Date: 2024-12-19 13:41:11.126097
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "e9b703e3d747"
down_revision: Union[str, None] = "4a2457daed53"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Data migration: Update the version to 1.5.3
op.execute("UPDATE bw_metadata SET version = '1.5.3' WHERE id = 1")
def downgrade() -> None:
# Revert the version back to 1.5.2
op.execute("UPDATE bw_metadata SET version = '1.5.2' WHERE id = 1")

View file

@ -0,0 +1,35 @@
"""Upgrade to version 1.5.11
Revision ID: efb577b1c25d
Revises: 0a2e336b02e7
Create Date: 2024-12-19 14:42:50.054087
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "efb577b1c25d"
down_revision: Union[str, None] = "0a2e336b02e7"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Add the new non_draft_services column to bw_metadata
op.add_column("bw_metadata", sa.Column("non_draft_services", sa.Integer(), nullable=False, server_default="0"))
# Update the version in metadata
op.execute("UPDATE bw_metadata SET version = '1.5.11' WHERE id = 1")
def downgrade() -> None:
# Reverse the addition of the non_draft_services column
op.drop_column("bw_metadata", "non_draft_services")
# Revert version to 1.5.10
op.execute("UPDATE bw_metadata SET version = '1.5.10' WHERE id = 1")

View file

@ -0,0 +1,29 @@
"""Upgrade to version 1.5.12
Revision ID: fbd680c6ffeb
Revises: efb577b1c25d
Create Date: 2024-12-19 14:46:13.313028
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "fbd680c6ffeb"
down_revision: Union[str, None] = "efb577b1c25d"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Update version to 1.5.12
op.execute("UPDATE bw_metadata SET version = '1.5.12' WHERE id = 1")
def downgrade() -> None:
# Revert version to 1.5.11
op.execute("UPDATE bw_metadata SET version = '1.5.11' WHERE id = 1")

View file

@ -0,0 +1,29 @@
"""Upgrade to version 1.5.9
Revision ID: fe047f892d6b
Revises: ba43081c6f96
Create Date: 2024-12-19 14:41:12.553100
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "fe047f892d6b"
down_revision: Union[str, None] = "ba43081c6f96"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Update version in 'bw_metadata'
op.execute("UPDATE bw_metadata SET version = '1.5.9' WHERE id = 1")
def downgrade() -> None:
# Revert version in 'bw_metadata'
op.execute("UPDATE bw_metadata SET version = '1.5.8' WHERE id = 1")

View file

@ -0,0 +1,26 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: Union[str, None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
def upgrade() -> None:
${upgrades if upgrades else "pass"}
def downgrade() -> None:
${downgrades if downgrades else "pass"}

View file

@ -0,0 +1,119 @@
"""Upgrade to version 1.5.6
Revision ID: 0a4144dd55d4
Revises: c9586782cd77
Create Date: 2024-12-17 08:39:34.882278
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "0a4144dd55d4"
down_revision: Union[str, None] = "c9586782cd77"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
# Define Enums for consistency
PRO_STATUS_ENUM = sa.Enum("active", "invalid", "expired", "suspended", name="pro_status_enum")
PLUGIN_TYPES_ENUM = sa.Enum("core", "external", "pro", name="plugin_types_enum")
STREAM_TYPES_ENUM = sa.Enum("no", "yes", "partial", name="stream_types_enum")
def upgrade():
# Alter value columns to TEXT in bw_global_values and bw_services_settings
with op.batch_alter_table("bw_global_values") as batch_op:
batch_op.alter_column("value", type_=sa.TEXT(), existing_type=sa.VARCHAR(length=8192))
with op.batch_alter_table("bw_services_settings") as batch_op:
batch_op.alter_column("value", type_=sa.TEXT(), existing_type=sa.VARCHAR(length=8192))
# Add new columns to bw_metadata
op.add_column("bw_metadata", sa.Column("is_pro", sa.Boolean(), nullable=False, server_default="0"))
op.add_column("bw_metadata", sa.Column("pro_expire", sa.DateTime(), nullable=True))
op.add_column("bw_metadata", sa.Column("pro_status", PRO_STATUS_ENUM, nullable=False, server_default="invalid"))
op.add_column("bw_metadata", sa.Column("pro_services", sa.Integer(), nullable=False, server_default="0"))
op.add_column("bw_metadata", sa.Column("pro_overlapped", sa.Boolean(), nullable=False, server_default="0"))
op.add_column("bw_metadata", sa.Column("last_pro_check", sa.DateTime(), nullable=True))
op.add_column("bw_metadata", sa.Column("pro_plugins_changed", sa.Boolean(), nullable=True))
# Modify bw_plugins table
# Step 1: Add the new 'type' column with default 'external'
op.add_column("bw_plugins", sa.Column("type", PLUGIN_TYPES_ENUM, nullable=False, server_default="core"))
# Step 2: Migrate data: Set 'type' to 'external' where 'external' was true
op.execute(
"""
UPDATE bw_plugins
SET type = 'external'
WHERE external = true
"""
)
# Step 3: Drop the 'external' column and alter the 'stream' column to STREAM_TYPES_ENUM
with op.batch_alter_table("bw_plugins") as batch_op:
batch_op.drop_column("external")
batch_op.alter_column("stream", type_=STREAM_TYPES_ENUM, existing_type=sa.VARCHAR(length=16))
# Add is_draft column to bw_services
op.add_column("bw_services", sa.Column("is_draft", sa.Boolean(), nullable=False, server_default="0"))
# Update all new columns and version in a single statement
op.execute(
"""
UPDATE bw_metadata
SET is_pro = false,
pro_status = 'invalid',
pro_services = 0,
pro_overlapped = false,
pro_plugins_changed = false,
version = '1.5.6'
WHERE id = 1
"""
)
def downgrade():
# Revert changes in bw_services
op.drop_column("bw_services", "is_draft")
# Revert changes in bw_plugins
with op.batch_alter_table("bw_plugins") as batch_op:
batch_op.add_column(sa.Column("external", sa.Boolean(), nullable=False, server_default="0"))
batch_op.alter_column("stream", type_=sa.VARCHAR(length=16), existing_type=STREAM_TYPES_ENUM)
# Migrate data: Set 'type' to 'external' where 'external' was true
op.execute(
"""
UPDATE bw_plugins
SET external = true
WHERE type = 'external'
"""
)
# Drop new columns from bw_plugins
op.drop_column("bw_plugins", "type")
# Drop new columns from bw_metadata
op.drop_column("bw_metadata", "pro_plugins_changed")
op.drop_column("bw_metadata", "last_pro_check")
op.drop_column("bw_metadata", "pro_overlapped")
op.drop_column("bw_metadata", "pro_services")
op.drop_column("bw_metadata", "pro_status")
op.drop_column("bw_metadata", "pro_expire")
op.drop_column("bw_metadata", "is_pro")
# Revert value columns in bw_global_values and bw_services_settings
with op.batch_alter_table("bw_global_values") as batch_op:
batch_op.alter_column("value", type_=sa.VARCHAR(length=8192), existing_type=sa.TEXT())
with op.batch_alter_table("bw_services_settings") as batch_op:
batch_op.alter_column("value", type_=sa.VARCHAR(length=8192), existing_type=sa.TEXT())
# Revert version in bw_metadata
op.execute("UPDATE bw_metadata SET version = '1.5.5' WHERE id = 1")

View file

@ -0,0 +1,62 @@
"""Upgrade to version 1.5.8
Revision ID: 13fb1f986f11
Revises: 91859f8f75ad
Create Date: 2024-12-17 08:41:34.664880
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "13fb1f986f11"
down_revision: Union[str, None] = "91859f8f75ad"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Add new columns to 'bw_metadata'
with op.batch_alter_table("bw_metadata") as batch_op:
batch_op.add_column(sa.Column("pro_license", sa.String(length=128), nullable=True))
batch_op.add_column(sa.Column("last_custom_configs_change", sa.DateTime(), nullable=True))
batch_op.add_column(sa.Column("last_external_plugins_change", sa.DateTime(), nullable=True))
batch_op.add_column(sa.Column("last_pro_plugins_change", sa.DateTime(), nullable=True))
batch_op.add_column(sa.Column("last_instances_change", sa.DateTime(), nullable=True))
batch_op.add_column(sa.Column("failover", sa.Boolean(), nullable=True))
batch_op.drop_column("config_changed")
# Add new columns to 'bw_plugins'
with op.batch_alter_table("bw_plugins") as batch_op:
batch_op.add_column(sa.Column("config_changed", sa.Boolean(), nullable=True))
batch_op.add_column(sa.Column("last_config_change", sa.DateTime(), nullable=True))
# Set default value for 'config_changed' in 'bw_plugins'
op.execute("UPDATE bw_plugins SET config_changed = false")
# Update version in 'bw_metadata'
op.execute("UPDATE bw_metadata SET version = '1.5.8' WHERE id = 1")
def downgrade() -> None:
# Revert 'bw_plugins' changes
with op.batch_alter_table("bw_plugins") as batch_op:
batch_op.drop_column("last_config_change")
batch_op.drop_column("config_changed")
# Revert 'bw_metadata' changes
with op.batch_alter_table("bw_metadata") as batch_op:
batch_op.add_column(sa.Column("config_changed", sa.Boolean(), nullable=True))
batch_op.drop_column("failover")
batch_op.drop_column("last_instances_change")
batch_op.drop_column("last_pro_plugins_change")
batch_op.drop_column("last_external_plugins_change")
batch_op.drop_column("last_custom_configs_change")
batch_op.drop_column("pro_license")
# Revert version in 'bw_metadata'
op.execute("UPDATE bw_metadata SET version = '1.5.7', config_changed = false WHERE id = 1")

View file

@ -0,0 +1,40 @@
"""Upgrade to version 1.5.4
Revision ID: 17a6fddfddc2
Revises: eb3ca0f3f20c
Create Date: 2024-12-17 08:38:06.860342
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "17a6fddfddc2"
down_revision: Union[str, None] = "eb3ca0f3f20c"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Create bw_ui_users table
op.create_table(
"bw_ui_users",
sa.Column("id", sa.Integer(), primary_key=True, nullable=False),
sa.Column("username", sa.String(length=256), nullable=False, unique=True),
sa.Column("password", sa.String(length=60), nullable=False),
)
# Update metadata version
op.execute("UPDATE bw_metadata SET version = '1.5.4' WHERE id = 1")
def downgrade() -> None:
# Drop bw_ui_users table
op.drop_table("bw_ui_users")
# Revert metadata version
op.execute("UPDATE bw_metadata SET version = '1.5.3' WHERE id = 1")

View file

@ -0,0 +1,502 @@
"""Upgrade to version 1.6.0-beta
Revision ID: 1e1fc017a424
Revises: 75a5d34f9a7d
Create Date: 2024-12-17 08:42:34.116054
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "1e1fc017a424"
down_revision: Union[str, None] = "75a5d34f9a7d"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade():
# --- Enums Changes ---
# METHODS_ENUM now includes "wizard"
# CUSTOM_CONFIGS_TYPES_ENUM now includes "default_server_stream", "crs_plugins_before", "crs_plugins_after"
# PLUGIN_TYPES_ENUM now includes "ui"
# INSTANCE_TYPE_ENUM and INSTANCE_STATUS_ENUM are new
# These changes do not directly translate to SQLite schema changes since SQLite doesn't enforce enum types.
# The application code will handle the new enum values. No direct DDL required for SQLite.
# --- New Tables for UI ---
op.create_table(
"bw_ui_permissions",
sa.Column("name", sa.String(64), primary_key=True),
)
op.create_table(
"bw_ui_roles",
sa.Column("name", sa.String(64), primary_key=True),
sa.Column("description", sa.String(256), nullable=False),
sa.Column("update_datetime", sa.DateTime(timezone=True), nullable=False),
)
op.create_table(
"bw_ui_user_recovery_codes",
sa.Column("id", sa.Integer(), sa.Identity(start=1, increment=1), primary_key=True),
sa.Column("user_name", sa.String(256), nullable=False),
sa.Column("code", sa.UnicodeText, nullable=False),
sa.ForeignKeyConstraint(["user_name"], ["bw_ui_users.username"], onupdate="CASCADE", ondelete="CASCADE"),
)
op.create_table(
"bw_ui_roles_permissions",
sa.Column("role_name", sa.String(64), nullable=False),
sa.Column("permission_name", sa.String(64), nullable=False),
sa.ForeignKeyConstraint(["role_name"], ["bw_ui_roles.name"], onupdate="CASCADE", ondelete="CASCADE"),
sa.ForeignKeyConstraint(["permission_name"], ["bw_ui_permissions.name"], onupdate="CASCADE", ondelete="CASCADE"),
sa.PrimaryKeyConstraint("role_name", "permission_name"),
)
op.create_table(
"bw_ui_roles_users",
sa.Column("user_name", sa.String(256), nullable=False),
sa.Column("role_name", sa.String(64), nullable=False),
sa.ForeignKeyConstraint(["user_name"], ["bw_ui_users.username"], onupdate="CASCADE", ondelete="CASCADE"),
sa.ForeignKeyConstraint(["role_name"], ["bw_ui_roles.name"], onupdate="CASCADE", ondelete="CASCADE"),
sa.PrimaryKeyConstraint("user_name", "role_name"),
)
op.create_table(
"bw_ui_user_sessions",
sa.Column("id", sa.Integer(), sa.Identity(start=1, increment=1), primary_key=True),
sa.Column("user_name", sa.String(256), nullable=False),
sa.Column("ip", sa.String(39), nullable=False),
sa.Column("user_agent", sa.TEXT, nullable=False),
sa.Column("creation_date", sa.DateTime(timezone=True), nullable=False),
sa.Column("last_activity", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["user_name"], ["bw_ui_users.username"], onupdate="CASCADE", ondelete="CASCADE"),
)
from model import JSONText
op.create_table(
"bw_ui_user_columns_preferences",
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
sa.Column("user_name", sa.String(256), nullable=False),
sa.Column("table_name", sa.Enum("bans", "configs", "instances", "jobs", "plugins", "reports", "services", name="tables_enum"), nullable=False),
sa.Column("columns", JSONText, nullable=False),
sa.ForeignKeyConstraint(["user_name"], ["bw_ui_users.username"], onupdate="CASCADE", ondelete="CASCADE"),
sa.UniqueConstraint("user_name", "table_name", name="uq_user_columns_preferences"),
)
# --- Templates Tables ---
op.create_table(
"bw_templates",
sa.Column("id", sa.String(256), primary_key=True),
sa.Column("name", sa.String(256), unique=True, nullable=False),
sa.Column("plugin_id", sa.String(64), nullable=False),
sa.ForeignKeyConstraint(["plugin_id"], ["bw_plugins.id"], onupdate="CASCADE", ondelete="CASCADE"),
)
op.create_table(
"bw_template_steps",
sa.Column("id", sa.Integer(), primary_key=True),
sa.Column("template_id", sa.String(256), nullable=False, primary_key=True),
sa.Column("title", sa.TEXT, nullable=False),
sa.Column("subtitle", sa.TEXT, nullable=True),
sa.ForeignKeyConstraint(["template_id"], ["bw_templates.id"], onupdate="CASCADE", ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id", "template_id"),
)
op.create_table(
"bw_template_settings",
sa.Column("id", sa.Integer(), sa.Identity(start=1, increment=1), primary_key=True),
sa.Column("template_id", sa.String(256), nullable=False),
sa.Column("setting_id", sa.String(256), nullable=False),
sa.Column("step_id", sa.Integer(), nullable=True),
sa.Column("default", sa.TEXT, nullable=False),
sa.Column("suffix", sa.Integer(), nullable=True, default=0),
sa.ForeignKeyConstraint(["template_id"], ["bw_templates.id"], onupdate="CASCADE", ondelete="CASCADE"),
sa.ForeignKeyConstraint(["setting_id"], ["bw_settings.id"], onupdate="CASCADE", ondelete="CASCADE"),
sa.UniqueConstraint("template_id", "setting_id", "step_id", "suffix"),
)
op.create_table(
"bw_template_custom_configs",
sa.Column("id", sa.Integer(), sa.Identity(start=1, increment=1), primary_key=True),
sa.Column("template_id", sa.String(256), nullable=False),
sa.Column("step_id", sa.Integer(), nullable=True),
sa.Column(
"type",
sa.Enum(
"http",
"stream",
"server_http",
"server_stream",
"default_server_http",
"default_server_stream",
"modsec",
"modsec_crs",
"crs_plugins_before",
"crs_plugins_after",
name="custom_configs_types_enum",
),
nullable=False,
),
sa.Column("name", sa.String(256), nullable=False),
sa.Column("data", sa.LargeBinary(length=(2**32) - 1), nullable=False),
sa.Column("checksum", sa.String(128), nullable=False),
sa.ForeignKeyConstraint(["template_id"], ["bw_templates.id"], onupdate="CASCADE", ondelete="CASCADE"),
sa.UniqueConstraint("template_id", "step_id", "type", "name"),
)
# Create a new table with the updated custom_configs schema
op.create_table(
"bw_custom_configs_new",
sa.Column("id", sa.Integer, sa.Identity(start=1, increment=1), primary_key=True),
sa.Column("service_id", sa.String(64), sa.ForeignKey("bw_services.id", onupdate="cascade", ondelete="cascade"), nullable=True),
sa.Column(
"type",
sa.Enum(
"http",
"stream",
"server_http",
"server_stream",
"default_server_http",
"default_server_stream",
"modsec",
"modsec_crs",
"crs_plugins_before",
"crs_plugins_after",
name="custom_configs_types_enum",
),
nullable=False,
),
sa.Column("name", sa.String(256), nullable=False),
sa.Column("data", sa.LargeBinary(length=(2**32) - 1), nullable=False),
sa.Column("checksum", sa.String(128), nullable=False),
sa.Column(
"method",
sa.Enum(
"ui",
"scheduler",
"autoconf",
"manual",
"wizard",
name="methods_enum",
),
nullable=False,
),
sa.UniqueConstraint("service_id", "type", "name"),
)
# Copy data from the old table to the new table
op.execute(
"""
INSERT INTO bw_custom_configs_new (id, service_id, type, name, data, checksum, method)
SELECT id, service_id, type, name, data, checksum, method FROM bw_custom_configs
"""
)
# Drop the old table
op.drop_table("bw_custom_configs")
# Rename the new table to match the original table's name
op.rename_table("bw_custom_configs_new", "bw_custom_configs")
# Create a new bw_settings table with updated schema (only 'id' as PK and 'name' as unique)
op.create_table(
"bw_settings_new",
sa.Column("id", sa.String(256), primary_key=True),
sa.Column("name", sa.String(256), unique=True, nullable=False),
sa.Column("plugin_id", sa.String(64), sa.ForeignKey("bw_plugins.id", onupdate="cascade", ondelete="cascade"), nullable=False),
sa.Column("context", sa.Enum("global", "multisite", name="contexts_enum"), nullable=False),
sa.Column("default", sa.TEXT, nullable=True),
sa.Column("help", sa.String(512), nullable=False),
sa.Column("label", sa.String(256), nullable=True),
sa.Column("regex", sa.String(1024), nullable=False),
sa.Column("type", sa.Enum("password", "text", "check", "select", name="settings_types_enum"), nullable=False),
sa.Column("multiple", sa.String(128), nullable=True),
sa.Column("order", sa.Integer, default=0, nullable=False),
)
# Copy data from the old table to the new table
op.execute(
"""
INSERT INTO bw_settings_new (id, name, plugin_id, context, "default", help, label, regex, type, multiple, "order")
SELECT id, name, plugin_id, context, "default", help, label, regex, type, multiple, "order"
FROM bw_settings
"""
)
# Drop the old table
op.drop_table("bw_settings")
# Rename the new table to match the original name
op.rename_table("bw_settings_new", "bw_settings")
# --- bw_jobs table changes ---
# Add run_async column, drop success and last_run
with op.batch_alter_table("bw_jobs") as batch_op:
batch_op.add_column(sa.Column("run_async", sa.Boolean(), nullable=True, server_default="0"))
# Remove old columns success, last_run
batch_op.drop_column("success")
batch_op.drop_column("last_run")
# set run_async to not nullable now that we have a default
op.execute("UPDATE bw_jobs SET run_async = 0 WHERE run_async IS NULL")
with op.batch_alter_table("bw_jobs") as batch_op:
batch_op.alter_column("run_async", nullable=False)
# New bw_jobs_runs table
op.create_table(
"bw_jobs_runs",
sa.Column("id", sa.Integer(), sa.Identity(start=1, increment=1), primary_key=True),
sa.Column("job_name", sa.String(128), nullable=False),
sa.Column("success", sa.Boolean(), nullable=True, server_default="0"),
sa.Column("start_date", sa.DateTime(timezone=True), nullable=False),
sa.Column("end_date", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["job_name"], ["bw_jobs.name"], onupdate="CASCADE", ondelete="CASCADE"),
)
# --- bw_services changes ---
# Add creation_date, last_update
with op.batch_alter_table("bw_services") as batch_op:
batch_op.add_column(sa.Column("creation_date", sa.DateTime(timezone=True), nullable=True))
batch_op.add_column(sa.Column("last_update", sa.DateTime(timezone=True), nullable=True))
# Set current timestamp for existing services
op.execute("UPDATE bw_services SET creation_date = CURRENT_TIMESTAMP, last_update = CURRENT_TIMESTAMP")
# Now make them non-nullable
with op.batch_alter_table("bw_services") as batch_op:
batch_op.alter_column("creation_date", nullable=False)
batch_op.alter_column("last_update", nullable=False)
# --- bw_ui_users changes ---
# Create a temporary new table
op.create_table(
"bw_ui_users_new",
sa.Column("username", sa.String(256), primary_key=True),
sa.Column("email", sa.String(256), nullable=True, unique=True),
sa.Column("password", sa.String(60), nullable=False),
sa.Column("method", sa.Enum("ui", "scheduler", "autoconf", "manual", "wizard", name="methods_enum"), nullable=False, server_default="manual"),
sa.Column("admin", sa.Boolean(), nullable=False, server_default="0"),
sa.Column("theme", sa.Enum("light", "dark", name="themes_enum"), nullable=False, server_default="light"),
sa.Column("totp_secret", sa.String(256), nullable=True),
sa.Column("creation_date", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.current_timestamp()),
sa.Column("update_date", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.current_timestamp()),
)
# Migrate data from old bw_ui_users
op.execute(
"""
INSERT INTO bw_ui_users_new (username, password, method, admin, theme, creation_date, update_date)
SELECT username, password, method, 1, 'light', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
FROM bw_ui_users
"""
)
# Drop old table and rename new table
op.drop_table("bw_ui_users")
op.rename_table("bw_ui_users_new", "bw_ui_users")
# --- bw_plugin_pages changes ---
# Old had template_file, template_checksum, actions_file, actions_checksum, obfuscation_file, obfuscation_checksum
# New has id, plugin_id(unique), data, checksum
op.create_table(
"bw_plugin_pages_new",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("plugin_id", sa.String(64), nullable=False, unique=True),
sa.Column("data", sa.LargeBinary(length=4294967295), nullable=False),
sa.Column("checksum", sa.String(128), nullable=False),
sa.ForeignKeyConstraint(["plugin_id"], ["bw_plugins.id"], onupdate="CASCADE", ondelete="CASCADE"),
)
op.execute(
"""
INSERT INTO bw_plugin_pages_new (id, plugin_id, data, checksum)
SELECT id, plugin_id, template_file, '' FROM bw_plugin_pages
"""
)
op.drop_table("bw_plugin_pages")
op.rename_table("bw_plugin_pages_new", "bw_plugin_pages")
# --- bw_instances changes ---
with op.batch_alter_table("bw_instances") as batch_op:
batch_op.add_column(sa.Column("name", sa.String(256), nullable=True))
batch_op.add_column(sa.Column("type", sa.Enum("static", "container", "pod", name="instance_type_enum"), nullable=True))
batch_op.add_column(sa.Column("status", sa.Enum("loading", "up", "down", name="instance_status_enum"), nullable=True))
batch_op.add_column(sa.Column("method", sa.Enum("ui", "scheduler", "autoconf", "manual", "wizard", name="methods_enum"), nullable=True))
batch_op.add_column(sa.Column("creation_date", sa.DateTime(timezone=True), nullable=True))
batch_op.add_column(sa.Column("last_seen", sa.DateTime(timezone=True), nullable=True))
op.execute("DELETE FROM bw_instances WHERE name IS NULL")
with op.batch_alter_table("bw_instances") as batch_op:
batch_op.alter_column("name", nullable=False)
batch_op.alter_column("type", nullable=False)
batch_op.alter_column("status", nullable=False)
batch_op.alter_column("method", nullable=False)
batch_op.alter_column("creation_date", nullable=False)
batch_op.alter_column("last_seen", nullable=False)
# Update version to 1.6.0-beta
op.execute("UPDATE bw_metadata SET version = '1.6.0-beta' WHERE id = 1")
def downgrade():
# Downgrade steps: revert all changes that can't be easily undone.
# This will drop newly created tables and attempt to restore old structure.
# Drop new tables
op.drop_table("bw_template_custom_configs")
op.drop_table("bw_template_settings")
op.drop_table("bw_template_steps")
op.drop_table("bw_templates")
op.drop_table("bw_jobs_runs")
op.drop_table("bw_ui_user_sessions")
op.drop_table("bw_ui_roles_users")
op.drop_table("bw_ui_roles_permissions")
op.drop_table("bw_ui_user_recovery_codes")
op.drop_table("bw_ui_user_columns_preferences")
op.drop_table("bw_ui_roles")
op.drop_table("bw_ui_permissions")
# Revert bw_plugin_pages
op.create_table(
"bw_plugin_pages_old",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("plugin_id", sa.String(64), nullable=False),
sa.Column("template_file", sa.LargeBinary(length=(2**32) - 1), nullable=False),
sa.Column("template_checksum", sa.String(128), nullable=False),
sa.Column("actions_file", sa.LargeBinary(length=(2**32) - 1), nullable=False),
sa.Column("actions_checksum", sa.String(128), nullable=False),
sa.Column("obfuscation_file", sa.LargeBinary(length=(2**32) - 1), nullable=True),
sa.Column("obfuscation_checksum", sa.String(128), nullable=True),
sa.ForeignKeyConstraint(["plugin_id"], ["bw_plugins.id"], onupdate="CASCADE", ondelete="CASCADE"),
)
op.drop_table("bw_plugin_pages")
op.rename_table("bw_plugin_pages_old", "bw_plugin_pages")
# bw_jobs revert
with op.batch_alter_table("bw_jobs") as batch_op:
batch_op.add_column(sa.Column("success", sa.Boolean(), nullable=True))
batch_op.add_column(sa.Column("last_run", sa.DateTime(), nullable=True))
batch_op.drop_column("run_async")
# bw_services revert
with op.batch_alter_table("bw_services") as batch_op:
batch_op.drop_column("last_update")
batch_op.drop_column("creation_date")
# bw_instances revert
with op.batch_alter_table("bw_instances") as batch_op:
batch_op.drop_column("last_seen")
batch_op.drop_column("creation_date")
batch_op.drop_column("method")
batch_op.drop_column("status")
batch_op.drop_column("type")
batch_op.drop_column("name")
# Revert custom configs
op.create_table(
"bw_custom_configs_old",
sa.Column("id", sa.Integer, sa.Identity(start=1, increment=1), primary_key=True),
sa.Column("service_id", sa.String(64), sa.ForeignKey("bw_services.id", onupdate="cascade", ondelete="cascade"), nullable=True),
sa.Column(
"type",
sa.Enum(
"http",
"default_server_http",
"server_http",
"modsec",
"modsec_crs",
"stream",
"server_stream",
name="custom_configs_types_enum",
),
nullable=False,
),
sa.Column("name", sa.String(256), nullable=False),
sa.Column("data", sa.LargeBinary(length=(2**32) - 1), nullable=False),
sa.Column("checksum", sa.String(128), nullable=False),
sa.Column(
"method",
sa.Enum(
"ui",
"scheduler",
"autoconf",
"manual",
name="methods_enum",
),
nullable=False,
),
sa.UniqueConstraint("service_id", "type", "name"),
)
op.execute(
"""
INSERT INTO bw_custom_configs_old (id, service_id, type, name, data, checksum, method)
SELECT id, service_id, type, name, data, checksum, method
FROM bw_custom_configs
WHERE type IN ('http', 'default_server_http', 'server_http', 'modsec', 'modsec_crs', 'stream', 'server_stream')
"""
)
op.drop_table("bw_custom_configs")
op.rename_table("bw_custom_configs_old", "bw_custom_configs")
# Revert bw_settings
op.create_table(
"bw_settings_old",
sa.Column("id", sa.String(256), primary_key=True),
sa.Column("name", sa.String(256), primary_key=True),
sa.Column("plugin_id", sa.String(64), sa.ForeignKey("bw_plugins.id", onupdate="cascade", ondelete="cascade"), nullable=False),
sa.Column("context", sa.Enum("global", "multisite", name="contexts_enum"), nullable=False),
sa.Column("default", sa.String(4096), nullable=True, default=""),
sa.Column("help", sa.String(512), nullable=False),
sa.Column("label", sa.String(256), nullable=True),
sa.Column("regex", sa.String(1024), nullable=False),
sa.Column("type", sa.Enum("password", "text", "check", "select", name="settings_types_enum"), nullable=False),
sa.Column("multiple", sa.String(128), nullable=True),
sa.Column("order", sa.Integer, default=0, nullable=False),
sa.PrimaryKeyConstraint("id", "name"),
sa.UniqueConstraint("id"),
)
op.execute(
"""
INSERT INTO bw_settings_old (id, name, plugin_id, context, "default", help, label, regex, type, multiple, "order")
SELECT id, name, plugin_id, context, "default", help, label, regex, type, multiple, "order"
FROM bw_settings
"""
)
op.drop_table("bw_settings")
op.rename_table("bw_settings_old", "bw_settings")
# bw_ui_users revert
op.create_table(
"bw_ui_users_old",
sa.Column("id", sa.Integer, primary_key=True, default=1),
sa.Column("username", sa.String(256), nullable=False, unique=True),
sa.Column("password", sa.String(60), nullable=False),
sa.Column("is_two_factor_enabled", sa.Boolean, nullable=False, default=False),
sa.Column("secret_token", sa.String(32), nullable=True, unique=True, default=None),
sa.Column("method", sa.Enum("ui", "scheduler", "autoconf", "manual", name="methods_enum"), nullable=False, default="manual"),
)
op.execute(
"""
INSERT INTO bw_ui_users_old (username, password, method)
SELECT username, password, method FROM bw_ui_users
"""
)
op.drop_table("bw_ui_users")
op.rename_table("bw_ui_users_old", "bw_ui_users")
# Revert version
op.execute("UPDATE bw_metadata SET version = '1.5.12' WHERE id = 1")

View file

@ -0,0 +1,27 @@
"""Upgrade to version 1.5.2
Revision ID: 259e352699f1
Revises: 6599b34870d1
Create Date: 2024-12-17 08:38:02.818323
"""
from typing import Sequence, Union
from alembic import op
# revision identifiers, used by Alembic.
revision: str = "259e352699f1"
down_revision: Union[str, None] = "6599b34870d1"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Data migration: Update the version to 1.5.2
op.execute("UPDATE bw_metadata SET version = '1.5.2' WHERE id = 1")
def downgrade() -> None:
# Revert the version back to 1.5.1
op.execute("UPDATE bw_metadata SET version = '1.5.1' WHERE id = 1")

View file

@ -0,0 +1,29 @@
"""Upgrade to version 1.5.9
Revision ID: 4f7bdc32c662
Revises: 13fb1f986f11
Create Date: 2024-12-17 08:41:37.205928
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "4f7bdc32c662"
down_revision: Union[str, None] = "13fb1f986f11"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Update version in 'bw_metadata'
op.execute("UPDATE bw_metadata SET version = '1.5.9' WHERE id = 1")
def downgrade() -> None:
# Revert version in 'bw_metadata'
op.execute("UPDATE bw_metadata SET version = '1.5.8' WHERE id = 1")

View file

@ -0,0 +1,277 @@
"""Upgrade to version 1.6.0-rc1
Revision ID: 5b0ea031ccfc
Revises: 1e1fc017a424
Create Date: 2024-12-20 08:36:31.739835
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "5b0ea031ccfc"
down_revision: Union[str, None] = "1e1fc017a424"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Enable foreign key checks (SQLite specific)
op.execute("PRAGMA foreign_keys=OFF;")
# --- bw_services ---
# Old schema (1.6.0-beta):
# id: String(64), method: METHODS_ENUM, is_draft: Boolean, creation_date: DateTime, last_update: DateTime
# New schema (1.6.0-rc1):
# id: String(256), (other columns unchanged)
op.create_table(
"bw_services_new",
sa.Column("id", sa.String(256), primary_key=True),
sa.Column("method", sa.Enum("ui", "scheduler", "autoconf", "manual", "wizard", name="methods_enum"), nullable=False),
sa.Column("is_draft", sa.Boolean, default=False, nullable=False),
sa.Column("creation_date", sa.DateTime(timezone=True), nullable=False),
sa.Column("last_update", sa.DateTime(timezone=True), nullable=False),
)
# Copy data from old bw_services to bw_services_new
op.execute(
"""
INSERT INTO bw_services_new (id, method, is_draft, creation_date, last_update)
SELECT id, method, is_draft, creation_date, last_update FROM bw_services
"""
)
op.drop_table("bw_services")
op.rename_table("bw_services_new", "bw_services")
# --- bw_services_settings ---
# Old schema:
# service_id: String(64) FK -> bw_services.id
# New schema:
# service_id: String(256) FK -> bw_services.id
op.create_table(
"bw_services_settings_new",
sa.Column("service_id", sa.String(256), nullable=False),
sa.Column("setting_id", sa.String(256), nullable=False),
sa.Column("value", sa.TEXT, nullable=False),
sa.Column("suffix", sa.Integer, nullable=True, default=0),
sa.Column("method", sa.Enum("ui", "scheduler", "autoconf", "manual", "wizard", name="methods_enum"), nullable=False),
sa.PrimaryKeyConstraint("service_id", "setting_id", "suffix"),
sa.ForeignKeyConstraint(["service_id"], ["bw_services.id"], onupdate="CASCADE", ondelete="CASCADE"),
sa.ForeignKeyConstraint(["setting_id"], ["bw_settings.id"], onupdate="CASCADE", ondelete="CASCADE"),
)
op.execute(
"""
INSERT INTO bw_services_settings_new (service_id, setting_id, value, suffix, method)
SELECT service_id, setting_id, value, suffix, method
FROM bw_services_settings
"""
)
op.drop_table("bw_services_settings")
op.rename_table("bw_services_settings_new", "bw_services_settings")
# --- bw_custom_configs ---
# Old schema:
# service_id: String(64)
# New schema:
# service_id: String(256)
op.create_table(
"bw_custom_configs_new",
sa.Column("id", sa.Integer, sa.Identity(start=1, increment=1), primary_key=True),
sa.Column("service_id", sa.String(256), nullable=True),
sa.Column(
"type",
sa.Enum(
"http",
"stream",
"server_http",
"server_stream",
"default_server_http",
"default_server_stream",
"modsec",
"modsec_crs",
"crs_plugins_before",
"crs_plugins_after",
name="custom_configs_types_enum",
),
nullable=False,
),
sa.Column("name", sa.String(256), nullable=False),
sa.Column("data", sa.LargeBinary(length=(2**32) - 1), nullable=False),
sa.Column("checksum", sa.String(128), nullable=False),
sa.Column("method", sa.Enum("ui", "scheduler", "autoconf", "manual", "wizard", name="methods_enum"), nullable=False),
sa.UniqueConstraint("service_id", "type", "name"),
sa.ForeignKeyConstraint(["service_id"], ["bw_services.id"], onupdate="CASCADE", ondelete="CASCADE"),
)
op.execute(
"""
INSERT INTO bw_custom_configs_new (id, service_id, type, name, data, checksum, method)
SELECT id, service_id, type, name, data, checksum, method FROM bw_custom_configs
"""
)
op.drop_table("bw_custom_configs")
op.rename_table("bw_custom_configs_new", "bw_custom_configs")
# --- bw_jobs_cache ---
# Old schema:
# service_id: String(64)
# New schema:
# service_id: String(256)
op.create_table(
"bw_jobs_cache_new",
sa.Column("id", sa.Integer, sa.Identity(start=1, increment=1), primary_key=True),
sa.Column("job_name", sa.String(128), nullable=False),
sa.Column("service_id", sa.String(256), nullable=True),
sa.Column("file_name", sa.String(256), nullable=False),
sa.Column("data", sa.LargeBinary(length=(2**32) - 1), nullable=True),
sa.Column("last_update", sa.DateTime(timezone=True), nullable=True),
sa.Column("checksum", sa.String(128), nullable=True),
sa.ForeignKeyConstraint(["job_name"], ["bw_jobs.name"], onupdate="CASCADE", ondelete="CASCADE"),
sa.ForeignKeyConstraint(["service_id"], ["bw_services.id"], onupdate="CASCADE", ondelete="CASCADE"),
)
op.execute(
"""
INSERT INTO bw_jobs_cache_new (id, job_name, service_id, file_name, data, last_update, checksum)
SELECT id, job_name, service_id, file_name, data, last_update, checksum FROM bw_jobs_cache
"""
)
op.drop_table("bw_jobs_cache")
op.rename_table("bw_jobs_cache_new", "bw_jobs_cache")
# Re-enable foreign keys
op.execute("PRAGMA foreign_keys=ON;")
# Update the version in bw_metadata
op.execute("UPDATE bw_metadata SET version = '1.6.0-rc1' WHERE id = 1")
def downgrade() -> None:
# Disable foreign keys to allow dropping and recreating tables
op.execute("PRAGMA foreign_keys=OFF;")
# --- bw_services ---
# Downgrade: revert id from VARCHAR(256) back to VARCHAR(64)
op.create_table(
"bw_services_old",
sa.Column("id", sa.String(64), primary_key=True),
sa.Column("method", sa.Enum("ui", "scheduler", "autoconf", "manual", "wizard", name="methods_enum"), nullable=False),
sa.Column("is_draft", sa.Boolean, default=False, nullable=False),
sa.Column("creation_date", sa.DateTime(timezone=True), nullable=False),
sa.Column("last_update", sa.DateTime(timezone=True), nullable=False),
)
op.execute(
"""
INSERT INTO bw_services_old (id, method, is_draft, creation_date, last_update)
SELECT id, method, is_draft, creation_date, last_update FROM bw_services
"""
)
op.drop_table("bw_services")
op.rename_table("bw_services_old", "bw_services")
# --- bw_services_settings ---
# Downgrade: revert service_id from VARCHAR(256) back to VARCHAR(64)
op.create_table(
"bw_services_settings_old",
sa.Column("service_id", sa.String(64), nullable=False),
sa.Column("setting_id", sa.String(256), nullable=False),
sa.Column("value", sa.TEXT, nullable=False),
sa.Column("suffix", sa.Integer, nullable=True, default=0),
sa.Column("method", sa.Enum("ui", "scheduler", "autoconf", "manual", "wizard", name="methods_enum"), nullable=False),
sa.PrimaryKeyConstraint("service_id", "setting_id", "suffix"),
sa.ForeignKeyConstraint(["service_id"], ["bw_services.id"], onupdate="CASCADE", ondelete="CASCADE"),
sa.ForeignKeyConstraint(["setting_id"], ["bw_settings.id"], onupdate="CASCADE", ondelete="CASCADE"),
)
op.execute(
"""
INSERT INTO bw_services_settings_old (service_id, setting_id, value, suffix, method)
SELECT service_id, setting_id, value, suffix, method FROM bw_services_settings
"""
)
op.drop_table("bw_services_settings")
op.rename_table("bw_services_settings_old", "bw_services_settings")
# --- bw_custom_configs ---
# Downgrade: revert service_id from VARCHAR(256) back to VARCHAR(64)
op.create_table(
"bw_custom_configs_old",
sa.Column("id", sa.Integer, sa.Identity(start=1, increment=1), primary_key=True),
sa.Column("service_id", sa.String(64), nullable=True),
sa.Column(
"type",
sa.Enum(
"http",
"stream",
"server_http",
"server_stream",
"default_server_http",
"default_server_stream",
"modsec",
"modsec_crs",
"crs_plugins_before",
"crs_plugins_after",
name="custom_configs_types_enum",
),
nullable=False,
),
sa.Column("name", sa.String(256), nullable=False),
sa.Column("data", sa.LargeBinary(length=(2**32) - 1), nullable=False),
sa.Column("checksum", sa.String(128), nullable=False),
sa.Column("method", sa.Enum("ui", "scheduler", "autoconf", "manual", name="methods_enum"), nullable=False),
sa.UniqueConstraint("service_id", "type", "name"),
sa.ForeignKeyConstraint(["service_id"], ["bw_services.id"], onupdate="CASCADE", ondelete="CASCADE"),
)
op.execute(
"""
INSERT INTO bw_custom_configs_old (id, service_id, type, name, data, checksum, method)
SELECT id, service_id, type, name, data, checksum, method FROM bw_custom_configs
"""
)
op.drop_table("bw_custom_configs")
op.rename_table("bw_custom_configs_old", "bw_custom_configs")
# --- bw_jobs_cache ---
# Downgrade: revert service_id from VARCHAR(256) back to VARCHAR(64)
op.create_table(
"bw_jobs_cache_old",
sa.Column("id", sa.Integer, sa.Identity(start=1, increment=1), primary_key=True),
sa.Column("job_name", sa.String(128), nullable=False),
sa.Column("service_id", sa.String(64), nullable=True),
sa.Column("file_name", sa.String(256), nullable=False),
sa.Column("data", sa.LargeBinary(length=(2**32) - 1), nullable=True),
sa.Column("last_update", sa.DateTime(timezone=True), nullable=True),
sa.Column("checksum", sa.String(128), nullable=True),
sa.ForeignKeyConstraint(["job_name"], ["bw_jobs.name"], onupdate="CASCADE", ondelete="CASCADE"),
sa.ForeignKeyConstraint(["service_id"], ["bw_services.id"], onupdate="CASCADE", ondelete="CASCADE"),
)
op.execute(
"""
INSERT INTO bw_jobs_cache_old (id, job_name, service_id, file_name, data, last_update, checksum)
SELECT id, job_name, service_id, file_name, data, last_update, checksum FROM bw_jobs_cache
"""
)
op.drop_table("bw_jobs_cache")
op.rename_table("bw_jobs_cache_old", "bw_jobs_cache")
# Re-enable foreign keys
op.execute("PRAGMA foreign_keys=ON;")
# Revert the version in bw_metadata
op.execute("UPDATE bw_metadata SET version = '1.6.0-beta' WHERE id = 1")

View file

@ -0,0 +1,56 @@
"""Upgrade to version 1.5.1
Revision ID: 6599b34870d1
Revises: 8bb3be426524
Create Date: 2024-12-17 08:38:00.674169
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "6599b34870d1"
down_revision: Union[str, None] = "8bb3be426524"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Add new columns to bw_metadata."""
with op.batch_alter_table("bw_metadata") as batch_op:
batch_op.add_column(sa.Column("scheduler_first_start", sa.Boolean(), nullable=True))
batch_op.add_column(sa.Column("custom_configs_changed", sa.Boolean(), nullable=True))
batch_op.add_column(sa.Column("external_plugins_changed", sa.Boolean(), nullable=True))
batch_op.add_column(sa.Column("config_changed", sa.Boolean(), nullable=True))
batch_op.add_column(sa.Column("instances_changed", sa.Boolean(), nullable=True))
# Update all new columns and version in a single statement
op.execute(
"""
UPDATE bw_metadata
SET scheduler_first_start = false,
custom_configs_changed = false,
external_plugins_changed = false,
config_changed = false,
instances_changed = false,
version = '1.5.1'
WHERE id = 1
"""
)
def downgrade() -> None:
"""Remove new columns from bw_metadata."""
with op.batch_alter_table("bw_metadata") as batch_op:
batch_op.drop_column("instances_changed")
batch_op.drop_column("config_changed")
batch_op.drop_column("external_plugins_changed")
batch_op.drop_column("custom_configs_changed")
batch_op.drop_column("scheduler_first_start")
# Revert the version back to 1.5.0
op.execute("UPDATE bw_metadata SET version = '1.5.0' WHERE id = 1")

View file

@ -0,0 +1,29 @@
"""Upgrade to version 1.5.12
Revision ID: 75a5d34f9a7d
Revises: c272a8c3979c
Create Date: 2024-12-17 08:42:31.548453
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "75a5d34f9a7d"
down_revision: Union[str, None] = "c272a8c3979c"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Update version to 1.5.12
op.execute("UPDATE bw_metadata SET version = '1.5.12' WHERE id = 1")
def downgrade() -> None:
# Revert version to 1.5.11
op.execute("UPDATE bw_metadata SET version = '1.5.11' WHERE id = 1")

View file

@ -0,0 +1,29 @@
"""Upgrade to version 1.5.10
Revision ID: 760f95e8bee7
Revises: 4f7bdc32c662
Create Date: 2024-12-17 08:41:39.621642
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "760f95e8bee7"
down_revision: Union[str, None] = "4f7bdc32c662"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Update version in 'bw_metadata'
op.execute("UPDATE bw_metadata SET version = '1.5.10' WHERE id = 1")
def downgrade() -> None:
# Revert version in 'bw_metadata' back to 1.5.9
op.execute("UPDATE bw_metadata SET version = '1.5.9' WHERE id = 1")

View file

@ -0,0 +1,44 @@
"""Upgrade to version 1.5.0
Revision ID: 8bb3be426524
Revises:
Create Date: 2024-12-17 08:35:24.898488
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "8bb3be426524"
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Step 1: Drop 'order' column from 'bw_plugins'
with op.batch_alter_table("bw_plugins") as batch_op:
batch_op.drop_column("order")
# Data migration: Update the version to 1.5.0
op.execute("UPDATE bw_metadata SET version = '1.5.0' WHERE id = 1")
def downgrade() -> None:
# Step 1: Add 'order' column back to 'bw_plugins' (with a default value of 0 for NOT NULL constraint)
with op.batch_alter_table("bw_plugins") as batch_op:
batch_op.add_column(sa.Column("order", sa.Integer(), nullable=True, server_default="0"))
# Step 2: Set default value for existing rows
op.execute("UPDATE bw_plugins SET `order` = 0")
# Step 3: Alter 'order' column to NOT NULL
with op.batch_alter_table("bw_plugins") as batch_op:
batch_op.alter_column("order", nullable=False)
# Data migration: Update the version to 1.5.0-beta
op.execute("UPDATE bw_metadata SET version = '1.5.0-beta' WHERE id = 1")

View file

@ -0,0 +1,69 @@
"""Upgrade to version 1.5.7
Revision ID: 91859f8f75ad
Revises: 0a4144dd55d4
Create Date: 2024-12-17 08:40:26.598223
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "91859f8f75ad"
down_revision: Union[str, None] = "0a4144dd55d4"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Step 1: Add 'order' column as nullable with a default
with op.batch_alter_table("bw_settings") as batch_op:
batch_op.add_column(sa.Column("order", sa.Integer(), nullable=True, server_default="0"))
# Step 2: Set default value for existing rows
op.execute("UPDATE bw_settings SET `order` = 0")
# Step 3: Alter 'order' column to NOT NULL
with op.batch_alter_table("bw_settings") as batch_op:
batch_op.alter_column("order", nullable=False)
# Add new columns to 'bw_plugin_pages'
with op.batch_alter_table("bw_plugin_pages") as batch_op:
batch_op.add_column(sa.Column("obfuscation_file", sa.LargeBinary(length=4294967295), nullable=True))
batch_op.add_column(sa.Column("obfuscation_checksum", sa.String(length=128), nullable=True))
# Create the new 'bw_cli_commands' table
op.create_table(
"bw_cli_commands",
sa.Column("id", sa.Integer(), sa.Identity(start=1, increment=1), nullable=False),
sa.Column("name", sa.String(length=64), nullable=False),
sa.Column("plugin_id", sa.String(length=64), nullable=False),
sa.Column("file_name", sa.String(length=256), nullable=False),
sa.ForeignKeyConstraint(["plugin_id"], ["bw_plugins.id"], onupdate="cascade", ondelete="cascade"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("plugin_id", "name"),
)
# Update version in bw_metadata
op.execute("UPDATE bw_metadata SET version = '1.5.7' WHERE id = 1")
def downgrade() -> None:
# Drop the 'bw_cli_commands' table
op.drop_table("bw_cli_commands")
# Remove new columns from 'bw_plugin_pages'
with op.batch_alter_table("bw_plugin_pages") as batch_op:
batch_op.drop_column("obfuscation_checksum")
batch_op.drop_column("obfuscation_file")
# Drop 'order' column from 'bw_settings'
with op.batch_alter_table("bw_settings") as batch_op:
batch_op.drop_column("order")
# Revert version in bw_metadata
op.execute("UPDATE bw_metadata SET version = '1.5.6' WHERE id = 1")

View file

@ -0,0 +1,44 @@
"""Upgrade to version 1.5.11
Revision ID: c272a8c3979c
Revises: 760f95e8bee7
Create Date: 2024-12-17 08:41:42.203308
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "c272a8c3979c"
down_revision: Union[str, None] = "760f95e8bee7"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Add 'non_draft_services' column as nullable with server default
with op.batch_alter_table("bw_metadata") as batch_op:
batch_op.add_column(sa.Column("non_draft_services", sa.Integer(), nullable=True, server_default="0"))
# Update existing rows
op.execute("UPDATE bw_metadata SET non_draft_services = 0")
# Make the column NOT NULL and drop server_default
with op.batch_alter_table("bw_metadata") as batch_op:
batch_op.alter_column("non_draft_services", nullable=False, server_default=None)
# Update the version in metadata
op.execute("UPDATE bw_metadata SET version = '1.5.11' WHERE id = 1")
def downgrade() -> None:
# Drop the 'non_draft_services' column
with op.batch_alter_table("bw_metadata") as batch_op:
batch_op.drop_column("non_draft_services")
# Revert version to 1.5.10
op.execute("UPDATE bw_metadata SET version = '1.5.10' WHERE id = 1")

View file

@ -0,0 +1,66 @@
"""Upgrade to version 1.5.5
Revision ID: c9586782cd77
Revises: 17a6fddfddc2
Create Date: 2024-12-17 08:38:08.728703
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "c9586782cd77"
down_revision: Union[str, None] = "17a6fddfddc2"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
METHODS_ENUM = sa.Enum("ui", "scheduler", "autoconf", "manual", name="methods_enum")
def upgrade():
# Add new columns to bw_ui_users
with op.batch_alter_table("bw_ui_users") as batch_op:
batch_op.add_column(sa.Column("is_two_factor_enabled", sa.Boolean(), nullable=False, server_default="0"))
batch_op.add_column(sa.Column("secret_token", sa.String(length=32), nullable=True))
batch_op.add_column(sa.Column("method", METHODS_ENUM, nullable=False, server_default="manual"))
# Create unique constraint on bw_ui_users
with op.batch_alter_table("bw_ui_users") as batch_op:
batch_op.create_unique_constraint("uq_bw_ui_users_secret_token", ["secret_token"])
# Alter column lengths in bw_global_values and bw_services_settings
with op.batch_alter_table("bw_global_values") as batch_op:
batch_op.alter_column("value", type_=sa.String(8192), existing_type=sa.String(4096))
with op.batch_alter_table("bw_services_settings") as batch_op:
batch_op.alter_column("value", type_=sa.String(8192), existing_type=sa.String(4096))
# Update all new columns in a single statement
op.execute("UPDATE bw_ui_users SET is_two_factor_enabled = false, method = 'manual'")
# Update version in bw_metadata
op.execute("UPDATE bw_metadata SET version = '1.5.5' WHERE id = 1")
def downgrade():
# Revert bw_ui_users changes
with op.batch_alter_table("bw_ui_users") as batch_op:
batch_op.drop_constraint("uq_bw_ui_users_secret_token", type_="unique")
batch_op.drop_column("method")
batch_op.drop_column("secret_token")
batch_op.drop_column("is_two_factor_enabled")
# Revert column lengths in bw_global_values and bw_services_settings
with op.batch_alter_table("bw_global_values") as batch_op:
batch_op.alter_column("value", type_=sa.String(4096), existing_type=sa.String(8192))
with op.batch_alter_table("bw_services_settings") as batch_op:
batch_op.alter_column("value", type_=sa.String(4096), existing_type=sa.String(8192))
# Revert version in bw_metadata
op.execute("UPDATE bw_metadata SET version = '1.5.4' WHERE id = 1")

View file

@ -0,0 +1,28 @@
"""Upgrade to version 1.5.3
Revision ID: eb3ca0f3f20c
Revises: 259e352699f1
Create Date: 2024-12-17 08:38:04.878280
"""
from typing import Sequence, Union
from alembic import op
# revision identifiers, used by Alembic.
revision: str = "eb3ca0f3f20c"
down_revision: Union[str, None] = "259e352699f1"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Data migration: Update the version to 1.5.3
op.execute("UPDATE bw_metadata SET version = '1.5.3' WHERE id = 1")
def downgrade() -> None:
# Revert the version back to 1.5.2
op.execute("UPDATE bw_metadata SET version = '1.5.2' WHERE id = 1")

View file

@ -306,7 +306,7 @@ class Metadata(Base):
last_instances_change = Column(DateTime(timezone=True), nullable=True)
failover = Column(Boolean, default=None, nullable=True)
integration = Column(INTEGRATIONS_ENUM, default="Unknown", nullable=False)
version = Column(String(32), default="1.6.0-beta", nullable=False)
version = Column(String(32), default="1.6.0-rc1", nullable=False)
## UI Models

View file

@ -1,3 +1,4 @@
alembic==1.14.0
cryptography==44.0.0
psycopg[binary,pool]==3.2.3
PyMySQL==1.1.1

View file

@ -4,6 +4,10 @@
#
# pip-compile --allow-unsafe --generate-hashes --strip-extras requirements.in
#
alembic==1.14.0 \
--hash=sha256:99bd884ca390466db5e27ffccff1d179ec5c05c965cfefc0607e69f9e411cb25 \
--hash=sha256:b00892b53b3642d0b8dbedba234dbf1924b69be83a9a769d5a624b01094e304b
# via -r requirements.in
cffi==1.17.1 \
--hash=sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8 \
--hash=sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2 \
@ -179,6 +183,73 @@ greenlet==3.1.1 \
--hash=sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79 \
--hash=sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f
# via sqlalchemy
mako==1.3.8 \
--hash=sha256:42f48953c7eb91332040ff567eb7eea69b22e7a4affbc5ba8e845e8f730f6627 \
--hash=sha256:577b97e414580d3e088d47c2dbbe9594aa7a5146ed2875d4dfa9075af2dd3cc8
# via alembic
markupsafe==3.0.2 \
--hash=sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4 \
--hash=sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30 \
--hash=sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0 \
--hash=sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9 \
--hash=sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396 \
--hash=sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13 \
--hash=sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028 \
--hash=sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca \
--hash=sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557 \
--hash=sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832 \
--hash=sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0 \
--hash=sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b \
--hash=sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579 \
--hash=sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a \
--hash=sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c \
--hash=sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff \
--hash=sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c \
--hash=sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22 \
--hash=sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094 \
--hash=sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb \
--hash=sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e \
--hash=sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5 \
--hash=sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a \
--hash=sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d \
--hash=sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a \
--hash=sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b \
--hash=sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8 \
--hash=sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225 \
--hash=sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c \
--hash=sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144 \
--hash=sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f \
--hash=sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87 \
--hash=sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d \
--hash=sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93 \
--hash=sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf \
--hash=sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158 \
--hash=sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84 \
--hash=sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb \
--hash=sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48 \
--hash=sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171 \
--hash=sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c \
--hash=sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6 \
--hash=sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd \
--hash=sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d \
--hash=sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1 \
--hash=sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d \
--hash=sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca \
--hash=sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a \
--hash=sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29 \
--hash=sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe \
--hash=sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798 \
--hash=sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c \
--hash=sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8 \
--hash=sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f \
--hash=sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f \
--hash=sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a \
--hash=sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178 \
--hash=sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0 \
--hash=sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79 \
--hash=sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430 \
--hash=sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50
# via mako
psycopg==3.2.3 \
--hash=sha256:644d3973fe26908c73d4be746074f6e5224b03c1101d302d9a53bf565ad64907 \
--hash=sha256:a5764f67c27bec8bfac85764d23c534af2c27b893550377e37ce59c12aac47a2
@ -319,11 +390,14 @@ sqlalchemy==2.0.36 \
--hash=sha256:fd3a55deef00f689ce931d4d1b23fa9f04c880a48ee97af488fd215cf24e2a6c \
--hash=sha256:fddbe92b4760c6f5d48162aef14824add991aeda8ddadb3c31d56eb15ca69f8e \
--hash=sha256:fdf3386a801ea5aba17c6410dd1dc8d39cf454ca2565541b5ac42a84e1e28f53
# via -r requirements.in
# via
# -r requirements.in
# alembic
typing-extensions==4.12.2 \
--hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \
--hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8
# via
# alembic
# psycopg
# psycopg-pool
# sqlalchemy

View file

@ -120,15 +120,12 @@ if __name__ == "__main__":
db = Database(LOGGER, sqlalchemy_string=dotenv_env.get("DATABASE_URI", getenv("DATABASE_URI", None)))
bunkerweb_version = get_version()
db_metadata = db.get_metadata()
db_initialized = not isinstance(db_metadata, str) and db_metadata["is_initialized"]
if not db_initialized:
LOGGER.info("Database not initialized, initializing ...")
ret, err = db.init_tables(
[config.get_settings(), config.get_plugins("core"), config.get_plugins("external"), config.get_plugins("pro")], bunkerweb_version
)
ret, err = db.init_tables([config.get_settings(), config.get_plugins("core"), config.get_plugins("external"), config.get_plugins("pro")])
# Initialize database tables
if err:
@ -141,9 +138,7 @@ if __name__ == "__main__":
else:
LOGGER.info("Database is already initialized, checking for changes ...")
ret, err = db.init_tables(
[config.get_settings(), config.get_plugins("core"), config.get_plugins("external"), config.get_plugins("pro")], bunkerweb_version
)
ret, err = db.init_tables([config.get_settings(), config.get_plugins("core"), config.get_plugins("external"), config.get_plugins("pro")])
if not ret and err:
LOGGER.error(f"Exception while checking database tables : {err}")
@ -153,7 +148,7 @@ if __name__ == "__main__":
else:
LOGGER.info("Database tables successfully updated")
err = db.initialize_db(version=bunkerweb_version, integration=integration)
err = db.initialize_db(version=get_version(), integration=integration)
if err:
LOGGER.error(f"Can't {'initialize' if not db_initialized else 'update'} database metadata : {err}")

View file

@ -20,3 +20,11 @@ if [ -d /etc/nginx ]; then
echo " Copy /etc/nginx to $output_path"
do_and_check_cmd cp -R /etc/nginx "$output_path"
fi
# If version is older that 1.5.12 included then create another file
if [ -f /usr/share/bunkerweb/VERSION ]; then
version=$(cat /usr/share/bunkerweb/VERSION)
if dpkg --compare-versions "$version" le "1.5.12"; then
do_and_check_cmd touch /var/tmp/bunkerweb_enable_scheduler
fi
fi

View file

@ -5,7 +5,7 @@
source /usr/share/bunkerweb/helpers/utils.sh
# Set the PYTHONPATH
export PYTHONPATH=/usr/share/bunkerweb/deps/python
export PYTHONPATH=/usr/share/bunkerweb/deps/python:/usr/share/bunkerweb/db
# Create the scheduler.env file if it doesn't exist
if [ ! -f /etc/bunkerweb/scheduler.env ]; then
@ -48,6 +48,101 @@ function start() {
fi
export SCHEDULER_LOG_TO_FILE
# Database migration section
log "SYSTEMCTL" "" "Checking database configuration..."
cd /usr/share/bunkerweb/db/alembic || {
log "SYSTEMCTL" "❌" "Failed to access database migration directory"
exit 1
}
# Extract and validate database type
DATABASE_URI=${DATABASE_URI:-sqlite:////var/lib/bunkerweb/db.sqlite3}
DATABASE=$(echo "$DATABASE_URI" | awk -F: '{print $1}' | awk -F+ '{print $1}')
# Validate database type with case-insensitive comparison
db_type=$(echo "$DATABASE" | tr '[:upper:]' '[:lower:]')
case "$db_type" in
sqlite|mysql|mariadb|postgresql)
log "SYSTEMCTL" "" "Using database type: $DATABASE"
;;
*)
log "SYSTEMCTL" "❌" "Unsupported database type: $DATABASE"
exit 1
;;
esac
# Update configuration files
if ! sed -i "s|^sqlalchemy\\.url =.*$|sqlalchemy.url = $DATABASE_URI|" alembic.ini; then
log "SYSTEMCTL" "❌" "Failed to update database URL in configuration"
exit 1
fi
if ! sed -i "s|^version_locations =.*$|version_locations = ${DATABASE}_versions|" alembic.ini; then
log "SYSTEMCTL" "❌" "Failed to update version locations in configuration"
exit 1
fi
# Check current version and stamp
log "SYSTEMCTL" "" "Checking database version..."
installed_version=$(cat /usr/share/bunkerweb/VERSION)
# Create temporary Python script
cat > /tmp/version_check.py << EOL
import sqlalchemy as sa
from os import getenv
from Database import Database
from logger import setup_logger
LOGGER = setup_logger('Scheduler', getenv('CUSTOM_LOG_LEVEL', getenv('LOG_LEVEL', 'INFO')))
engine = Database(LOGGER, '${DATABASE_URI}').sql_engine
with engine.connect() as conn:
try:
result = conn.execute(sa.text('SELECT version FROM bw_metadata WHERE id = 1'))
print(next(result)[0])
except BaseException as e:
if 'doesn\\'t exist' not in str(e):
print('none')
print('${installed_version}')
EOL
current_version=$(sudo -E -u nginx -g nginx /bin/bash -c "PYTHONPATH=$PYTHONPATH python3 /tmp/version_check.py")
rm -f /tmp/version_check.py
if [ "$current_version" == "none" ]; then
log "SYSTEMCTL" "❌" "Failed to retrieve database version"
exit 1
fi
if [ "$current_version" != "$installed_version" ]; then
# Find the corresponding Alembic revision by scanning migration files
MIGRATION_DIR="/usr/share/bunkerweb/db/alembic/${DATABASE}_versions"
NORMALIZED_VERSION=$(echo "$current_version" | tr '.' '_' | tr '-' '_')
REVISION=$(find "$MIGRATION_DIR" -maxdepth 1 -type f -name "*_upgrade_to_version_${NORMALIZED_VERSION}.py" -exec basename {} \; | awk -F_ '{print $1}')
if [ -z "$REVISION" ]; then
log "SYSTEMCTL" "❌" "No migration file found for database version: $current_version"
exit 1
fi
# Stamp the database with the determined revision
if ! sudo -E -u nginx -g nginx /bin/bash -c "PYTHONPATH=$PYTHONPATH python3 -m alembic stamp \"$REVISION\""; then
log "SYSTEMCTL" "❌" "Failed to stamp database with revision: $REVISION"
exit 1
fi
# Run database migration
log "SYSTEMCTL" "" "Running database migration..."
if ! sudo -E -u nginx -g nginx /bin/bash -c "PYTHONPATH=$PYTHONPATH python3 -m alembic upgrade head"; then
log "SYSTEMCTL" "❌" "Database migration failed"
exit 1
fi
log "SYSTEMCTL" "✅" "Database migration completed successfully"
fi
cd - > /dev/null || exit 1
# Execute scheduler
log "SYSTEMCTL" " " "Executing scheduler ..."
sudo -E -u nginx -g nginx /bin/bash -c "PYTHONPATH=$PYTHONPATH /usr/share/bunkerweb/scheduler/main.py --variables /etc/bunkerweb/variables.env"

View file

@ -116,19 +116,27 @@ fi
# Create scheduler if necessary
if {
[ "${MANAGER_MODE:-yes}" != "no" ] || [ "${WORKER_MODE:-no}" = "no" ];
{
[ -z "$MANAGER_MODE" ] && [ -z "$WORKER_MODE" ];
} || {
[ "${MANAGER_MODE:-yes}" != "no" ] || [ "${WORKER_MODE:-no}" = "no" ];
};
} && [ "$SERVICE_SCHEDULER" != "no" ]; then
if [ -f /var/tmp/bunkerweb_upgrade ]; then
# Reload the bunkerweb-scheduler service if running
if systemctl is-active --quiet bunkerweb-scheduler; then
echo "Restarting the bunkerweb-scheduler service..."
do_and_check_cmd systemctl restart bunkerweb-scheduler
fi
else
if [[ -f /var/tmp/bunkerweb_enable_scheduler || ! -f /var/tmp/bunkerweb_upgrade ]]; then
# Auto start BW Scheduler service on boot and start it now
echo "Enabling and starting the bunkerweb-scheduler service..."
do_and_check_cmd systemctl enable bunkerweb-scheduler
do_and_check_cmd systemctl start bunkerweb-scheduler
if [ -f /var/tmp/bunkerweb_enable_scheduler ]; then
rm -f /var/tmp/bunkerweb_enable_scheduler
fi
else
# Reload the bunkerweb-scheduler service if running
if systemctl is-active --quiet bunkerweb-scheduler; then
echo "Restarting the bunkerweb-scheduler service..."
do_and_check_cmd systemctl restart bunkerweb-scheduler
fi
fi
elif systemctl is-active --quiet bunkerweb-scheduler; then
echo "Disabling the bunkerweb-scheduler service..."
@ -138,7 +146,11 @@ fi
# Create web UI if necessary
if {
[ "${MANAGER_MODE:-yes}" != "no" ] || [ "${WORKER_MODE:-no}" = "no" ];
{
[ -z "$MANAGER_MODE" ] && [ -z "$WORKER_MODE" ];
} || {
[ "${MANAGER_MODE:-yes}" != "no" ] || [ "${WORKER_MODE:-no}" = "no" ];
};
} && [ "$SERVICE_UI" != "no" ]; then
if [ -f /var/tmp/bunkerweb_upgrade ]; then
# Reload the bunkerweb-ui service if running

View file

@ -41,7 +41,7 @@ FROM python:3.13-alpine@sha256:fcbcbbecdeae71d3b77445d9144d1914df55110f825ab62b0
RUN umask 027
# Install runtime dependencies and add scheduler user
RUN apk add --no-cache bash unzip libgcc libstdc++ libpq openssl libmagic mariadb-connector-c mariadb-client postgresql-client sqlite tzdata && \
RUN apk add --no-cache bash unzip libgcc libstdc++ libpq openssl libmagic mariadb-connector-c mariadb-client postgresql-client sqlite tzdata sed grep && \
addgroup -g 101 scheduler && \
adduser -h /var/cache/nginx -g scheduler -s /bin/sh -G scheduler -D -H -u 101 scheduler
@ -69,6 +69,8 @@ RUN cp helpers/bwcli /usr/bin/ && \
find core/ -type f -name "*.sh" ! -path "core/modsecurity/files/*" -print0 | xargs -0 chmod 750 && \
find core/ -type f -name "*.py" ! -path "core/modsecurity/files/*" -print0 | xargs -0 chmod 750 && \
chmod 750 cli/main.py gen/*.py scheduler/main.py scheduler/entrypoint.sh helpers/*.sh deps/python/bin/* /usr/bin/bwcli && \
chmod 770 db/alembic/alembic.ini db/alembic/env.py && \
chmod u+w db/alembic && \
chmod 660 INTEGRATION
@ -76,7 +78,7 @@ COPY --chown=root:scheduler --chmod=770 src/bw/misc/asn.mmdb /var/tmp/bunkerweb/
COPY --chown=root:scheduler --chmod=770 src/bw/misc/country.mmdb /var/tmp/bunkerweb/country.mmdb
LABEL maintainer="Bunkerity <contact@bunkerity.com>"
LABEL version="1.6.0-beta"
LABEL version="1.6.0-rc1"
LABEL url="https://www.bunkerweb.io"
LABEL bunkerweb.type="scheduler"
@ -88,4 +90,6 @@ USER scheduler:scheduler
HEALTHCHECK --interval=10s --timeout=10s --start-period=30s --retries=6 CMD /usr/share/bunkerweb/helpers/healthcheck-scheduler.sh
ENV PYTHONPATH=/usr/share/bunkerweb/deps/python:/usr/share/bunkerweb/db
ENTRYPOINT [ "./entrypoint.sh" ]

View file

@ -34,6 +34,97 @@ elif [[ $(echo "$AUTOCONF_MODE" | awk '{print tolower($0)}') == "yes" ]] ; then
echo "Autoconf" > /usr/share/bunkerweb/INTEGRATION
fi
# Database migration section
log "ENTRYPOINT" "" "Checking database configuration..."
cd /usr/share/bunkerweb/db/alembic || {
log "ENTRYPOINT" "❌" "Failed to access database migration directory"
exit 1
}
# Extract and validate database type
DATABASE_URI=${DATABASE_URI:-sqlite:////var/lib/bunkerweb/db.sqlite3}
DATABASE=$(echo "$DATABASE_URI" | awk -F: '{print $1}' | awk -F+ '{print $1}')
# Validate database type with case-insensitive comparison
db_type=$(echo "$DATABASE" | tr '[:upper:]' '[:lower:]')
case "$db_type" in
sqlite|mysql|mariadb|postgresql)
log "ENTRYPOINT" "" "Using database type: $DATABASE"
;;
*)
log "ENTRYPOINT" "❌" "Unsupported database type: $DATABASE"
exit 1
;;
esac
# Update configuration files
if ! sed -i "s|^sqlalchemy\\.url =.*$|sqlalchemy.url = $DATABASE_URI|" alembic.ini; then
log "ENTRYPOINT" "❌" "Failed to update database URL in configuration"
exit 1
fi
if ! sed -i "s|^version_locations =.*$|version_locations = ${DATABASE}_versions|" alembic.ini; then
log "ENTRYPOINT" "❌" "Failed to update version locations in configuration"
exit 1
fi
# Check current version and stamp
log "ENTRYPOINT" "" "Checking database version..."
installed_version=$(cat /usr/share/bunkerweb/VERSION)
current_version=$(python3 -c "
import sqlalchemy as sa
from os import getenv
from Database import Database
from logger import setup_logger
LOGGER = setup_logger('Scheduler', getenv('CUSTOM_LOG_LEVEL', getenv('LOG_LEVEL', 'INFO')))
engine = Database(LOGGER, '${DATABASE_URI}').sql_engine
with engine.connect() as conn:
try:
result = conn.execute(sa.text('SELECT version FROM bw_metadata WHERE id = 1'))
print(next(result)[0])
except BaseException as e:
if 'doesn\'t exist' not in str(e):
print('none')
print('${installed_version}')
")
if [ "$current_version" == "none" ]; then
log "ENTRYPOINT" "❌" "Failed to retrieve database version"
exit 1
fi
if [ "$current_version" != "$installed_version" ]; then
# Find the corresponding Alembic revision by scanning migration files
MIGRATION_DIR="/usr/share/bunkerweb/db/alembic/${DATABASE}_versions"
NORMALIZED_VERSION=$(echo "$current_version" | tr '.' '_' | tr '-' '_')
REVISION=$(find "$MIGRATION_DIR" -maxdepth 1 -type f -name "*_upgrade_to_version_${NORMALIZED_VERSION}.py" -exec basename {} \; | awk -F_ '{print $1}')
if [ -z "$REVISION" ]; then
log "ENTRYPOINT" "❌" "No migration file found for database version: $current_version"
exit 1
fi
# Stamp the database with the determined revision
if ! python3 -m alembic stamp "$REVISION"; then
log "ENTRYPOINT" "❌" "Failed to stamp database with revision: $REVISION"
exit 1
fi
# Run database migration
log "ENTRYPOINT" "" "Running database migration..."
if ! python3 -m alembic upgrade head; then
log "ENTRYPOINT" "❌" "Database migration failed"
exit 1
fi
log "ENTRYPOINT" "✅" "Database migration completed successfully"
fi
cd - > /dev/null || exit 1
# execute jobs
log "ENTRYPOINT" " " "Executing scheduler ..."
/usr/share/bunkerweb/scheduler/main.py &

View file

@ -26,7 +26,7 @@ for deps_path in [join(sep, "usr", "share", "bunkerweb", *paths) for paths in ((
from dotenv import dotenv_values
from schedule import every as schedule_every, run_pending
from common_utils import bytes_hash, dict_to_frozenset, get_version # type: ignore
from common_utils import bytes_hash, dict_to_frozenset # type: ignore
from logger import setup_logger # type: ignore
from Database import Database # type: ignore
from JobScheduler import JobScheduler
@ -396,7 +396,13 @@ def healthcheck_job():
for db_instance in SCHEDULER.db.get_instances():
bw_instance = API(f"http://{db_instance['hostname']}:{db_instance['port']}", db_instance["server_name"])
try:
sent, err, status, resp = bw_instance.request("GET", "health")
try:
sent, err, status, resp = bw_instance.request("GET", "health")
except BaseException as e:
err = str(e)
sent = False
status = 500
resp = {"status": "down", "msg": err}
success = True
if not sent:
@ -493,27 +499,6 @@ if __name__ == "__main__":
APPLYING_CHANGES.set()
db_version = SCHEDULER.db.get_version()
if not db_version.startswith("Error") and db_version != get_version():
LOGGER.warning("BunkerWeb version changed, creating a backup of the database and proceeding with the upgrade ...")
SCHEDULER.env = {
"DATABASE_URI": SCHEDULER.db.database_uri,
"USE_BACKUP": "yes",
"FORCE_BACKUP": "yes",
"BACKUP_SCHEDULE": "daily",
"BACKUP_ROTATION": "7",
"BACKUP_DIRECTORY": "/var/lib/bunkerweb/upgrade_backups",
"LOG_LEVEL": getenv("CUSTOM_LOG_LEVEL", getenv("LOG_LEVEL", "notice")),
"RELOAD_MIN_TIMEOUT": str(RELOAD_MIN_TIMEOUT),
}
if not SCHEDULER.run_single("backup-data"):
LOGGER.error("backup-data job failed, stopping ...")
stop(1)
LOGGER.info("Backup completed successfully, if you want to restore the backup, you can find it in /var/lib/bunkerweb/upgrade_backups")
rmtree(join(sep, "etc", "bunkerweb", "pro", "plugins", "letsencrypt_dns"), ignore_errors=True)
if SCHEDULER.db.readonly:
LOGGER.warning("The database is read-only, no need to save the changes in the configuration as they will not be saved")
else:
@ -623,6 +608,9 @@ if __name__ == "__main__":
with file.open("r", encoding="utf-8") as f:
plugin_data = json_load(f)
if plugin_data["id"] == "letsencrypt_dns":
continue
checksum = bytes_hash(plugin_content, algorithm="sha256")
common_data = plugin_data | {
"type": _type,