|
|
|
@@ -1,92 +1,39 @@
|
|
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
IFS=$'\n\t'
|
|
|
|
|
# Script to manage multitenant deployment
|
|
|
|
|
# Allows adding, removing, and restarting tenants without downtime for others
|
|
|
|
|
|
|
|
|
|
# Skript zur Verwaltung von Multitenant-Deployments
|
|
|
|
|
|
|
|
|
|
if [ ! -f "docker-compose-multitenant.yml" ]; then
|
|
|
|
|
echo "Error: docker-compose-multitenant.yml not found."
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Resolve script directory so config paths are deterministic even when called via sudo
|
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
|
CONFIG_FILE="$SCRIPT_DIR/config.json"
|
|
|
|
|
|
|
|
|
|
ensure_runtime_config_json() {
|
|
|
|
|
local config_path backup_path
|
|
|
|
|
config_path="$CONFIG_FILE"
|
|
|
|
|
# === HILFSFUNKTIONEN ===
|
|
|
|
|
|
|
|
|
|
ensure_runtime_config_json() {
|
|
|
|
|
local config_path="$CONFIG_FILE"
|
|
|
|
|
local backup_path
|
|
|
|
|
if [ -d "$config_path" ]; then
|
|
|
|
|
backup_path="${config_path}.dir.$(date +%Y%m%d-%H%M%S).bak"
|
|
|
|
|
mv "$config_path" "$backup_path"
|
|
|
|
|
echo "Warning: moved unexpected directory $config_path to $backup_path"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
FORCE_REMOVE=false
|
|
|
|
|
if [ "${2:-}" = "--yes" ] || [ "${2:-}" = "-y" ]; then
|
|
|
|
|
FORCE_REMOVE=true
|
|
|
|
|
TENANT_ID="${3:-}"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if [ -z "$TENANT_ID" ]; then
|
|
|
|
|
if [ ! -f "$config_path" ]; then
|
|
|
|
|
cat > "$config_path" <<'EOF'
|
|
|
|
|
{
|
|
|
|
|
"ver": "2.6.5",
|
|
|
|
|
"tenants": {}
|
|
|
|
|
}
|
|
|
|
|
EOF
|
|
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if [ "$FORCE_REMOVE" != true ]; then
|
|
|
|
|
echo -n "WARNING: Are you sure you want to permanently delete all data for tenant '$TENANT_ID'? (y/N) "
|
|
|
|
|
read confirm
|
|
|
|
|
if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then
|
|
|
|
|
echo "Removal canceled."
|
|
|
|
|
exit 0
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
echo "Removing tenant '$TENANT_ID'..."
|
|
|
|
|
APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1)
|
|
|
|
|
port_to_remove=""
|
|
|
|
|
if [ -n "$APP_CONTAINER" ]; then
|
|
|
|
|
port_to_remove="$({ docker exec "$APP_CONTAINER" python3 - "$TENANT_ID" <<'PY'
|
|
|
|
|
import sys
|
|
|
|
|
sys.path.insert(0, '/app')
|
|
|
|
|
sys.path.insert(0, '/app/Web')
|
|
|
|
|
from tenant import delete_tenant, get_tenant_config
|
|
|
|
|
|
|
|
|
|
tenant_id = sys.argv[1]
|
|
|
|
|
tenant_cfg = get_tenant_config(tenant_id)
|
|
|
|
|
port = tenant_cfg.get('port')
|
|
|
|
|
|
|
|
|
|
if not delete_tenant(tenant_id):
|
|
|
|
|
print(f'Error: failed to delete tenant {tenant_id}', file=sys.stderr)
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
if port is not None:
|
|
|
|
|
print(port)
|
|
|
|
|
PY
|
|
|
|
|
} 2>/dev/null)"
|
|
|
|
|
echo "Tenant '$TENANT_ID' database and config removed."
|
|
|
|
|
else
|
|
|
|
|
echo "Warning: Application container not running. Tenant database may still exist in MongoDB."
|
|
|
|
|
if port_to_remove="$(remove_tenant_port "$TENANT_ID" 2>/dev/null)"; then
|
|
|
|
|
:
|
|
|
|
|
else
|
|
|
|
|
echo "Warning: tenant '$TENANT_ID' was not configured in config.json or could not be removed."
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if [ -n "$port_to_remove" ]; then
|
|
|
|
|
remove_runtime_port "$port_to_remove"
|
|
|
|
|
fi
|
|
|
|
|
sync_tenant_port_map
|
|
|
|
|
if [ -n "$(docker ps -qf 'name=app' | head -n 1)" ]; then
|
|
|
|
|
restart_app_container
|
|
|
|
|
fi
|
|
|
|
|
if [ -n "$port_to_remove" ]; then
|
|
|
|
|
echo "Removed tenant '$TENANT_ID' and cleaned runtime port $port_to_remove."
|
|
|
|
|
else
|
|
|
|
|
echo "Removed tenant '$TENANT_ID'. No port mapping was present."
|
|
|
|
|
fi
|
|
|
|
|
get_tenant_aliases() {
|
|
|
|
|
local tenant_id="$1"
|
|
|
|
|
local normalized alias
|
|
|
|
|
normalized="$(printf '%s' "$tenant_id" | tr '[:upper:]' '[:lower:]')"
|
|
|
|
|
printf '%s\n' "$tenant_id"
|
|
|
|
@@ -212,7 +159,7 @@ existing['modules'] = {
|
|
|
|
|
'inventory': {'enabled': True},
|
|
|
|
|
'library': {'enabled': True},
|
|
|
|
|
'student_cards': {'enabled': True, 'default_borrow_days': 14, 'max_borrow_days': 365},
|
|
|
|
|
'terminplan': {'enabled': True},
|
|
|
|
|
'terminplan': {'enabled': True},
|
|
|
|
|
'mail': {'enabled': True},
|
|
|
|
|
}
|
|
|
|
|
existing['trial'] = trial_config
|
|
|
|
@@ -258,7 +205,7 @@ sanitized = "".join(c for c in tenant_id if c.isalnum() or c == "_")
|
|
|
|
|
db_name = f"inventar_{sanitized}"
|
|
|
|
|
client = MongoClient(settings.MONGODB_HOST, int(settings.MONGODB_PORT))
|
|
|
|
|
db = client[db_name]
|
|
|
|
|
hashed_pw = hashlib.sha512("admin123".encode()).hexdigest()
|
|
|
|
|
hashed_pw = hashlib.scrypt("admin123", salt=b"some_salt", n=16384, r=8, p=1).hex()
|
|
|
|
|
|
|
|
|
|
action_permissions = {
|
|
|
|
|
"can_borrow": True,
|
|
|
|
@@ -435,7 +382,6 @@ restart_app_container() {
|
|
|
|
|
|
|
|
|
|
ensure_runtime_config_json
|
|
|
|
|
|
|
|
|
|
# If HOST_WORKDIR is set (called from container), use absolute paths so docker daemon resolves them correctly
|
|
|
|
|
if [ -n "${HOST_WORKDIR:-}" ]; then
|
|
|
|
|
workdir="$HOST_WORKDIR"
|
|
|
|
|
compose_args+=( -f "$(readlink -f "$HOST_WORKDIR/docker-compose-multitenant.yml")" )
|
|
|
|
@@ -446,7 +392,6 @@ restart_app_container() {
|
|
|
|
|
compose_args+=( --env-file "$(readlink -f "$HOST_WORKDIR/.docker-build.env")" )
|
|
|
|
|
fi
|
|
|
|
|
else
|
|
|
|
|
# Normal case: called directly from host
|
|
|
|
|
compose_args+=( -f "$workdir/docker-compose-multitenant.yml" )
|
|
|
|
|
if [ -f "$workdir/.docker-compose.runtime.override.yml" ]; then
|
|
|
|
|
compose_args+=( -f "$workdir/.docker-compose.runtime.override.yml" )
|
|
|
|
@@ -456,7 +401,6 @@ restart_app_container() {
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Pass along COMPOSE_PROJECT_NAME if set so the internal docker-compose sees it
|
|
|
|
|
if [ -n "${COMPOSE_PROJECT_NAME:-}" ]; then
|
|
|
|
|
compose_args=( -p "$COMPOSE_PROJECT_NAME" "${compose_args[@]}" )
|
|
|
|
|
fi
|
|
|
|
@@ -566,24 +510,83 @@ remove_runtime_port() {
|
|
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
show_help() {
|
|
|
|
|
local HEADER='\033[95m'
|
|
|
|
|
local BLUE='\033[94m'
|
|
|
|
|
local GREEN='\033[92m'
|
|
|
|
|
local YELLOW='\033[93m'
|
|
|
|
|
local RED='\033[91m'
|
|
|
|
|
local BOLD='\033[1m'
|
|
|
|
|
local RESET='\033[0m'
|
|
|
|
|
|
|
|
|
|
cat << EOF
|
|
|
|
|
|
|
|
|
|
${HEADER}${BOLD}=== MULTI-TENANT INVENTAR MANAGER ===${RESET}
|
|
|
|
|
${YELLOW}Nutzung:${RESET} $0 <befehl> [tenant_id] [optionen]
|
|
|
|
|
|
|
|
|
|
${BLUE}${BOLD}VERFÜGBARE BEFEHLE:${RESET}
|
|
|
|
|
${GREEN}add${RESET} <tenant_id> [port]
|
|
|
|
|
Legt einen neuen Tenant an, registriert den Port und initialisiert
|
|
|
|
|
die MongoDB-Datenbank mit einem Standard-Admin (${YELLOW}admin / admin123${RESET}).
|
|
|
|
|
|
|
|
|
|
${GREEN}trial${RESET} <tenant_id> [port] [tage]
|
|
|
|
|
Erstellt einen temporären Test-Tenant. Standardlaufzeit: 7 Tage.
|
|
|
|
|
Läuft automatisch ab und löscht sich selbst.
|
|
|
|
|
|
|
|
|
|
${GREEN}remove${RESET} [-y|--yes] <tenant_id>
|
|
|
|
|
Löscht einen Tenant, seine Konfiguration, Ports und die Datenbank.
|
|
|
|
|
Nutze ${RED}-y${RESET}, um die Bestätigungsabfrage zu überspringen.
|
|
|
|
|
|
|
|
|
|
${GREEN}restart-tenant${RESET} <tenant_id>
|
|
|
|
|
Startet einen spezifischen Tenant neu, indem alle aktiven Sessions
|
|
|
|
|
und der Cache in der MongoDB geleert werden (Sitzungs-Reset).
|
|
|
|
|
|
|
|
|
|
${GREEN}restart-all${RESET}
|
|
|
|
|
Führt einen Zero-Downtime Rolling-Restart für alle App-Container durch.
|
|
|
|
|
|
|
|
|
|
${GREEN}list${RESET}
|
|
|
|
|
Listet alle registrierten Tenants aus der 'config.json' sowie alle
|
|
|
|
|
aktiven Tenant-Datenbanken aus der MongoDB auf.
|
|
|
|
|
|
|
|
|
|
${GREEN}module${RESET} <tenant_id> <modulname>=<on|off> ...
|
|
|
|
|
Aktiviert oder deaktiviert bestimmte Features/Module für einen Tenant.
|
|
|
|
|
Es können mehrere Module gleichzeitig übergeben werden.
|
|
|
|
|
|
|
|
|
|
${BLUE}${BOLD}GLOBALE OPTIONEN:${RESET}
|
|
|
|
|
${GREEN}-h, --help${RESET}
|
|
|
|
|
Zeigt diese Hilfe an.
|
|
|
|
|
|
|
|
|
|
${YELLOW}Beispiele:${RESET}
|
|
|
|
|
$0 add schule_muenchen 8081
|
|
|
|
|
$0 trial test_user 8082 14
|
|
|
|
|
$0 module schule_muenchen barre_code=on leih_historie=off
|
|
|
|
|
$0 remove -y test_user
|
|
|
|
|
|
|
|
|
|
-----------------------------------------------------------------
|
|
|
|
|
EOF
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if [ -z "${1:-}" ]; then
|
|
|
|
|
show_help
|
|
|
|
|
exit 0
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
COMMAND="$1"
|
|
|
|
|
TENANT_ID="${2:-}"
|
|
|
|
|
|
|
|
|
|
# === MAIN ROUTING BLOCK ===
|
|
|
|
|
|
|
|
|
|
case "$COMMAND" in
|
|
|
|
|
-h|--help)
|
|
|
|
|
show_help
|
|
|
|
|
;;
|
|
|
|
|
add)
|
|
|
|
|
TENANT_ID="${2:-}"
|
|
|
|
|
if [ -z "$TENANT_ID" ]; then
|
|
|
|
|
echo "Error: Please provide a tenant_id."
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
PORT_ARG="$3"
|
|
|
|
|
PORT_ARG="${3:-}"
|
|
|
|
|
if [ -n "$PORT_ARG" ]; then
|
|
|
|
|
if ! printf '%s\n' "$PORT_ARG" | grep -qE '^[0-9]+$'; then
|
|
|
|
|
echo "Error: Port must be a numeric value."
|
|
|
|
@@ -598,68 +601,13 @@ case "$COMMAND" in
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
echo "Adding new tenant '$TENANT_ID'..."
|
|
|
|
|
# Initialize tenant database via Python inside container
|
|
|
|
|
echo "Initializing database for $TENANT_ID..."
|
|
|
|
|
APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1)
|
|
|
|
|
if [ -n "$APP_CONTAINER" ]; then
|
|
|
|
|
docker exec $APP_CONTAINER python3 -c "
|
|
|
|
|
import sys, re; sys.path.insert(0, '/app'); sys.path.insert(0, '/app/Web'); from Web.modules.database import settings; from pymongo import MongoClient; import hashlib
|
|
|
|
|
tenant_id = sys.argv[1].lower()
|
|
|
|
|
sanitized = ''.join(c for c in tenant_id if c.isalnum() or c == '_')
|
|
|
|
|
db_name = f'inventar_{sanitized}'
|
|
|
|
|
client = MongoClient(settings.MONGODB_HOST, int(settings.MONGODB_PORT))
|
|
|
|
|
db = client[db_name]
|
|
|
|
|
hashed_pw = hashlib.sha512('admin123'.encode()).hexdigest()
|
|
|
|
|
if db.users.count_documents({'Username': 'admin'}) == 0:
|
|
|
|
|
db.users.insert_one({
|
|
|
|
|
'Username': 'admin',
|
|
|
|
|
'Password': hashed_pw,
|
|
|
|
|
'Admin': True,
|
|
|
|
|
'active_ausleihung': None,
|
|
|
|
|
'name': 'Admin',
|
|
|
|
|
'last_name': 'User',
|
|
|
|
|
'IsStudent': False,
|
|
|
|
|
'PermissionPreset': 'full_access',
|
|
|
|
|
'ActionPermissions': {
|
|
|
|
|
'can_borrow': True,
|
|
|
|
|
'can_insert': True,
|
|
|
|
|
'can_edit': True,
|
|
|
|
|
'can_delete': True,
|
|
|
|
|
'can_manage_users': True,
|
|
|
|
|
'can_manage_settings': True,
|
|
|
|
|
'can_view_logs': True,
|
|
|
|
|
},
|
|
|
|
|
'PagePermissions': {
|
|
|
|
|
'home': True,
|
|
|
|
|
'tutorial_page': True,
|
|
|
|
|
'my_borrowed_items': True,
|
|
|
|
|
'notifications_view': True,
|
|
|
|
|
'impressum': True,
|
|
|
|
|
'license': True,
|
|
|
|
|
'library_view': True,
|
|
|
|
|
'terminplan': True,
|
|
|
|
|
'home_admin': True,
|
|
|
|
|
'upload_admin': True,
|
|
|
|
|
'library_admin': True,
|
|
|
|
|
'admin_borrowings': True,
|
|
|
|
|
'library_loans_admin': True,
|
|
|
|
|
'admin_damaged_items': True,
|
|
|
|
|
'admin_audit_dashboard': True,
|
|
|
|
|
'logs': True,
|
|
|
|
|
'manage_filters': True,
|
|
|
|
|
'manage_locations': True,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
print(f'Tenant {sys.argv[1]} database initialized. Default admin: admin / admin123')
|
|
|
|
|
" "$TENANT_ID"
|
|
|
|
|
echo "Tenant '$TENANT_ID' successfully added. Ready to use."
|
|
|
|
|
else
|
|
|
|
|
echo "Warning: Application container is not running. Please start the multi-tenant system first."
|
|
|
|
|
echo "Data will be initialized upon first access by the tenant."
|
|
|
|
|
fi
|
|
|
|
|
initialize_tenant_database "$TENANT_ID" "standard"
|
|
|
|
|
echo "Tenant '$TENANT_ID' successfully added. Ready to use."
|
|
|
|
|
;;
|
|
|
|
|
|
|
|
|
|
trial)
|
|
|
|
|
TENANT_ID="${2:-}"
|
|
|
|
|
if [ -z "$TENANT_ID" ]; then
|
|
|
|
|
echo "Error: Please provide a tenant_id."
|
|
|
|
|
exit 1
|
|
|
|
@@ -691,9 +639,13 @@ print(f'Tenant {sys.argv[1]} database initialized. Default admin: admin / admin1
|
|
|
|
|
|
|
|
|
|
remove)
|
|
|
|
|
FORCE_REMOVE=false
|
|
|
|
|
if [ "${2:-}" = "--yes" ] || [ "${2:-}" = "-y" ]; then
|
|
|
|
|
TENANT_ARG="${2:-}"
|
|
|
|
|
|
|
|
|
|
if [ "$TENANT_ARG" = "--yes" ] || [ "$TENANT_ARG" = "-y" ]; then
|
|
|
|
|
FORCE_REMOVE=true
|
|
|
|
|
TENANT_ID="${3:-}"
|
|
|
|
|
else
|
|
|
|
|
TENANT_ID="$TENANT_ARG"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if [ -z "$TENANT_ID" ]; then
|
|
|
|
@@ -713,17 +665,32 @@ print(f'Tenant {sys.argv[1]} database initialized. Default admin: admin / admin1
|
|
|
|
|
echo "Removing tenant '$TENANT_ID'..."
|
|
|
|
|
APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1)
|
|
|
|
|
port_to_remove=""
|
|
|
|
|
|
|
|
|
|
if [ -n "$APP_CONTAINER" ]; then
|
|
|
|
|
# Zuerst die Datenbank via PyMongo hart droppen (Erzwungenes Löschen)
|
|
|
|
|
port_to_remove="$(docker exec "$APP_CONTAINER" python3 - "$TENANT_ID" <<'PY'
|
|
|
|
|
import sys
|
|
|
|
|
import sys, re
|
|
|
|
|
sys.path.insert(0, '/app')
|
|
|
|
|
sys.path.insert(0, '/app/Web')
|
|
|
|
|
from tenant import delete_tenant, get_tenant_config
|
|
|
|
|
from Web.modules.database import settings
|
|
|
|
|
from pymongo import MongoClient
|
|
|
|
|
|
|
|
|
|
tenant_id = sys.argv[1]
|
|
|
|
|
tenant_cfg = get_tenant_config(tenant_id)
|
|
|
|
|
port = tenant_cfg.get('port')
|
|
|
|
|
|
|
|
|
|
# Datenbanknamen exakt rekonstruieren
|
|
|
|
|
sanitized = "".join(c for c in tenant_id if c.isalnum() or c == "_")
|
|
|
|
|
db_name = f"inventar_{sanitized}"
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
client = MongoClient(settings.MONGODB_HOST, int(settings.MONGODB_PORT))
|
|
|
|
|
client.drop_database(db_name)
|
|
|
|
|
print(f"MongoDB database '{db_name}' dropped successfully.", file=sys.stderr)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"Warning: Could not drop database '{db_name}': {e}", file=sys.stderr)
|
|
|
|
|
|
|
|
|
|
if not delete_tenant(tenant_id):
|
|
|
|
|
print(f'Error: failed to delete tenant {tenant_id}', file=sys.stderr)
|
|
|
|
|
sys.exit(1)
|
|
|
|
@@ -757,20 +724,19 @@ PY
|
|
|
|
|
;;
|
|
|
|
|
|
|
|
|
|
restart-tenant)
|
|
|
|
|
TENANT_ID="${2:-}"
|
|
|
|
|
if [ -z "$TENANT_ID" ]; then
|
|
|
|
|
echo "Error: Please provide a tenant_id."
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
echo "Restarting tenant '$TENANT_ID' (clearing session/cache)..."
|
|
|
|
|
# To restart a single tenant without restarting the global python processes,
|
|
|
|
|
# we can invalidate their cache or drop their sessions collection to sign everyone out
|
|
|
|
|
APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1)
|
|
|
|
|
if [ -n "$APP_CONTAINER" ]; then
|
|
|
|
|
docker exec $APP_CONTAINER python3 -c "
|
|
|
|
|
import sys; sys.path.insert(0, '/app'); sys.path.insert(0, '/app/Web'); from Web.modules.database import settings; from pymongo import MongoClient
|
|
|
|
|
client = MongoClient(settings.MONGODB_HOST, int(settings.MONGODB_PORT))
|
|
|
|
|
db = client[f'{settings.MONGODB_DB}_{sys.argv[1]}']
|
|
|
|
|
db.sessions.drop() # Force sign-out / session clear
|
|
|
|
|
db.sessions.drop()
|
|
|
|
|
print(f'Tenant {sys.argv[1]} session cache cleared. Tenant restarted.')
|
|
|
|
|
" "$TENANT_ID"
|
|
|
|
|
echo "Tenant '$TENANT_ID' has been refreshed without impacting others."
|
|
|
|
@@ -833,12 +799,12 @@ PY
|
|
|
|
|
;;
|
|
|
|
|
|
|
|
|
|
module)
|
|
|
|
|
TENANT_ID="${2:-}"
|
|
|
|
|
if [ -z "$TENANT_ID" ] || [ -z "${3:-}" ]; then
|
|
|
|
|
echo "Error: Usage: manage-tenant.sh module <tenant_id> <module_name>=<on|off> [...]"
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Pass all remaining arguments to python script
|
|
|
|
|
shift 2
|
|
|
|
|
|
|
|
|
|
if python3 - "$CONFIG_FILE" "$TENANT_ID" "$@" <<'PY'
|
|
|
|
@@ -914,4 +880,4 @@ PY
|
|
|
|
|
;;
|
|
|
|
|
esac
|
|
|
|
|
|
|
|
|
|
exit 0
|
|
|
|
|
exit 0
|