From f901c8a22c8c5b4b88ec2e6ad161fd7bdc568a31 Mon Sep 17 00:00:00 2001 From: jaydee Date: Sun, 30 Nov 2025 19:35:08 +0100 Subject: [PATCH] build --- backup_volumes copy.py | 185 +++++++++++++++++++++++++++++++++++ backup_volumes.py | 90 +++++++++++++++++ bitwarden/docker-compose.yml | 5 +- 3 files changed, 279 insertions(+), 1 deletion(-) create mode 100644 backup_volumes copy.py create mode 100644 backup_volumes.py diff --git a/backup_volumes copy.py b/backup_volumes copy.py new file mode 100644 index 0000000..8eee0eb --- /dev/null +++ b/backup_volumes copy.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 +import docker +import os +import datetime +import argparse +import subprocess +import sys + +BACKUP_DIR = "/backup/docker_volumes" + +import requests + +PORTAINER_URL = "https://port.sectorq.eu/api" +API_KEY = "ptr_/5RkMCT/j3BTaL32vMSDtXFi76yOXRKVFOrUtzMsl5Y=" +HEADERS = {"X-API-Key": f"{API_KEY}"} +ENDPOINT_ID = 4 +def get_stack_by_name(stack_name): + url = f"{PORTAINER_URL}/stacks" + r = requests.get(url, headers=HEADERS) + r.raise_for_status() + stacks = r.json() + for s in stacks: + if s['Name'] == stack_name: + return s + return None + +def stop_stack(stack_name): + stack = get_stack_by_name(stack_name) + if not stack: + print(f"[INFO] Stack {stack_name} not found.") + return + + print(f"Stopping stack {stack_name} via Portainer API...") + url = f"{PORTAINER_URL}/stacks/{stack['Id']}/stop?endpointId={ENDPOINT_ID}" + print("URL:", url) + r = requests.post(url, headers=HEADERS) + if r.status_code == 200: + print(f"[OK] Stack {stack_name} stopped.") + else: + print(f"[ERROR] Failed to stop stack: {r.status_code} {r.text}") + +def start_stack(stack_name): + stack = get_stack_by_name(stack_name) + if not stack: + print(f"[INFO] Stack {stack_name} not found.") + return + + print(f"Starting stack {stack_name} via Portainer API...") + url = f"{PORTAINER_URL}/stacks/{stack['Id']}/start?endpointId={ENDPOINT_ID}" + r = requests.post(url, headers=HEADERS) + if r.status_code == 200: + print(f"[OK] Stack {stack_name} started.") + else: + print(f"[ERROR] Failed to start stack: {r.status_code} {r.text}") + +def to_timestamp(): + return datetime.datetime.now().strftime("%Y%m%d-%H%M%S") + +def get_containers_using_volume(client, vol_name): + containers = [] + for c in client.containers.list(): + for m in c.attrs['Mounts']: + if m.get('Name') == vol_name: + containers.append(c) + return containers + +def backup_volumes(client): + os.makedirs(BACKUP_DIR, exist_ok=True) + timestamp = to_timestamp() + volumes = client.volumes.list() + if not volumes: + print("No Docker volumes found.") + return + + for vol in volumes: + vol_name = vol.name + backup_file = os.path.join(BACKUP_DIR, f"{vol_name}-{timestamp}.tar.gz") + containers = get_containers_using_volume(client, vol_name) + + # Track stacks that need to be stopped + stacks_to_restart = {} + + for c in containers: + stack = c.labels.get('com.docker.stack.namespace') + if stack: + # Stop stack instead of individual container + if stack not in stacks_to_restart: + stacks_to_restart[stack] = True + stop_stack(stack) + else: + # Stop individual container + print(f"Stopping container {c.name} for backup...") + c.stop() + + # Backup using busybox + try: + print(f"Backing up volume {vol_name} → {backup_file}") + client.containers.run( + image="busybox", + command=f"tar czf /backup/{vol_name}-{timestamp}.tar.gz -C /volume .", + remove=True, + volumes={ + vol_name: {'bind': '/volume', 'mode': 'ro'}, + BACKUP_DIR: {'bind': '/backup', 'mode': 'rw'} + }, + ) + print(f"[OK] Backup completed: {backup_file}") + except Exception as e: + print(f"[ERROR] Failed to backup {vol_name}: {e}") + + # Restart individual containers + for c in containers: + stack = c.labels.get('com.docker.stack.namespace') + if not stack: + print(f"Starting container {c.name} after backup...") + c.start() + + # Restart stacks + for stack in stacks_to_restart.keys(): + start_stack(stack) + + +def restore_volume(client, vol_name, backup_file): + containers = get_containers_using_volume(client, vol_name) + stacks_to_restart = {} + + for c in containers: + stack = c.labels.get('com.docker.stack.namespace') + if stack: + if stack not in stacks_to_restart: + stacks_to_restart[stack] = True + stop_stack(stack) + else: + print(f"Stopping container {c.name} for restore...") + c.stop() + + try: + print(f"Restoring volume {vol_name} from {backup_file}") + client.containers.run( + image="busybox", + command=f"tar xzf /backup/{os.path.basename(backup_file)} -C /volume", + remove=True, + volumes={ + vol_name: {'bind': '/volume', 'mode': 'rw'}, + os.path.dirname(backup_file): {'bind': '/backup', 'mode': 'rw'} + }, + ) + print(f"[OK] Restore completed: {vol_name}") + except Exception as e: + print(f"[ERROR] Failed to restore {vol_name}: {e}") + + # Restart containers + for c in containers: + stack = c.labels.get('com.docker.stack.namespace') + if not stack: + print(f"Starting container {c.name} after restore...") + c.start() + + # Restart stacks + for stack in stacks_to_restart.keys(): + stack_file = f"/deployments/{stack}.yml" + if os.path.exists(stack_file): + start_stack(stack_file) + else: + print(f"[WARNING] Stack file {stack_file} not found. Start manually.") + +def main(): + parser = argparse.ArgumentParser(description="Backup or restore Docker volumes.") + parser.add_argument("action", choices=["backup", "restore"], help="Action to perform") + parser.add_argument("--volume", help="Volume name (required for restore)") + parser.add_argument("--file", help="Backup file (required for restore)") + args = parser.parse_args() + + client = docker.from_env() + + if args.action == "backup": + backup_volumes(client) + elif args.action == "restore": + if not args.volume or not args.file: + print("[ERROR] --volume and --file are required for restore") + sys.exit(1) + restore_volume(client, args.volume, args.file) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/backup_volumes.py b/backup_volumes.py new file mode 100644 index 0000000..b3e3990 --- /dev/null +++ b/backup_volumes.py @@ -0,0 +1,90 @@ +import docker +import requests +import os + +PORTAINER_URL = "https://port.sectorq.eu/api" +API_KEY = "ptr_/5RkMCT/j3BTaL32vMSDtXFi76yOXRKVFOrUtzMsl5Y=" +HEADERS = {"X-API-Key": f"{API_KEY}"} +ENDPOINT_ID = 4 +client = docker.from_env() + +# Keep track of which stacks we already stopped +stopped_stacks = {} + +def get_stack_by_name(stack_name): + url = f"{PORTAINER_URL}/stacks" + r = requests.get(url, headers=HEADERS) + r.raise_for_status() + for s in r.json(): + if s['Name'] == stack_name: + return s + return None + +def stop_stack(stack_name): + if stack_name in stopped_stacks: + return + stack = get_stack_by_name(stack_name) + if not stack: + print(f"[INFO] Stack {stack_name} not found, skipping stop") + return + url = f"{PORTAINER_URL}/stacks/{stack['Id']}/stop?endpointId={ENDPOINT_ID}" + print(f"url: {url}") + r = requests.post(url, headers=HEADERS) + if r.status_code == 200: + print(f"[OK] Stack {stack_name} stopped") + else: + print(f"[ERROR] Failed to stop stack {stack_name}: {r.text}") + stopped_stacks[stack_name] = True + +def start_stack(stack_name): + stack = get_stack_by_name(stack_name) + if not stack: + print(f"[INFO] Stack {stack_name} not found, skipping start") + return + url = f"{PORTAINER_URL}/stacks/{stack['Id']}/start?endpointId={ENDPOINT_ID}" + r = requests.post(url, headers=HEADERS) + if r.status_code == 200: + print(f"[OK] Stack {stack_name} started") + else: + print(f"[ERROR] Failed to start stack {stack_name}: {r.text}") + +def backup_volume(vol): + vol_name = vol.name + backup_file = f"/backup/{vol_name}.tar" + print(f"[INFO] Backing up volume {vol_name} → {backup_file}") + # Use a temporary container to archive the volume + client.containers.run( + image="alpine", + command=f"tar czf /backup/{vol_name}.tar -C /data .", + volumes={vol_name: {'bind': '/data', 'mode': 'ro'}, + "/backup": {'bind': '/backup', 'mode': 'rw'}}, + remove=True + ) + +def main(): + volumes = client.volumes.list() + stack_volumes = {} + normal_volumes = [] + + # Categorize volumes by stack + for vol in volumes: + labels = vol.attrs.get('Labels', {}) + stack_name = labels.get('com.docker.stack.namespace') + if stack_name: + stack_volumes.setdefault(stack_name, []).append(vol) + else: + normal_volumes.append(vol) + + # Backup stacks + for stack_name, vols in stack_volumes.items(): + stop_stack(stack_name) + for v in vols: + backup_volume(v) + start_stack(stack_name) + + # Backup normal volumes + for v in normal_volumes: + backup_volume(v) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/bitwarden/docker-compose.yml b/bitwarden/docker-compose.yml index 2bc157c..514854e 100755 --- a/bitwarden/docker-compose.yml +++ b/bitwarden/docker-compose.yml @@ -1,3 +1,6 @@ +volumes: + bitwarden_data: + driver: local services: bitwarden: container_name: vaultwarden @@ -29,4 +32,4 @@ services: - 8181:80 restart: ${RESTART:-unless-stopped} volumes: - - /share/docker_data/bitwarden/bw-data:/data + - bitwarden_data:/data