Use /mnt/borg-backups in place of /mnt/object_storage for Borg. Remove JuiceFS and Redis artifacts (tasks, templates, service configs) and delete borg-local-sync tooling. Update borg-backup service ReadWritePaths, remove Plex slow tvshows mount, add system sysctl performance tunings, and apply minor code and flake.lock updates.
459 lines
14 KiB
Python
Executable File
459 lines
14 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
"""Manage Docker services."""
|
|
|
|
import argparse
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
|
|
# Import helper functions
|
|
sys.path.append(os.path.join(os.path.dirname(__file__), ".."))
|
|
from helpers.functions import printfe, println
|
|
|
|
# Base directory for Docker services $HOME/services
|
|
SERVICES_DIR = os.path.join(os.path.expanduser("~"), ".services")
|
|
# Protected services that should never be stopped
|
|
PROTECTED_SERVICES = []
|
|
|
|
|
|
def get_service_path(service_name):
|
|
"""Return the path to a service's docker-compose file"""
|
|
service_dir = os.path.join(SERVICES_DIR, service_name)
|
|
compose_file = os.path.join(service_dir, "docker-compose.yml")
|
|
|
|
if not os.path.exists(compose_file):
|
|
printfe("red", f"Error: Service '{service_name}' not found at {compose_file}")
|
|
return None
|
|
|
|
return compose_file
|
|
|
|
|
|
def run_docker_compose(args, service_name=None, compose_file=None):
|
|
"""Run docker compose command with provided args"""
|
|
if service_name and not compose_file:
|
|
compose_file = get_service_path(service_name)
|
|
if not compose_file:
|
|
return 1
|
|
|
|
cmd = ["docker", "compose"]
|
|
|
|
if compose_file:
|
|
cmd.extend(["-f", compose_file])
|
|
|
|
cmd.extend(args)
|
|
|
|
printfe("blue", f"Running: {' '.join(cmd)}")
|
|
result = subprocess.run(cmd, check=False)
|
|
return result.returncode
|
|
|
|
|
|
def get_all_services():
|
|
"""Return a list of all available services"""
|
|
if not os.path.exists(SERVICES_DIR):
|
|
return []
|
|
|
|
services = [
|
|
d
|
|
for d in os.listdir(SERVICES_DIR)
|
|
if os.path.isdir(os.path.join(SERVICES_DIR, d))
|
|
and os.path.exists(os.path.join(SERVICES_DIR, d, "docker-compose.yml"))
|
|
]
|
|
|
|
return sorted(services)
|
|
|
|
|
|
def cmd_start(args):
|
|
"""Start a Docker service"""
|
|
if args.all:
|
|
services = get_all_services()
|
|
if not services:
|
|
printfe("yellow", "No services found to start")
|
|
return 0
|
|
|
|
printfe("blue", f"Starting all services: {', '.join(services)}")
|
|
|
|
failed_services = []
|
|
for service in services:
|
|
printfe("blue", f"\n=== Starting {service} ===")
|
|
result = run_docker_compose(["up", "-d"], service_name=service)
|
|
if result != 0:
|
|
failed_services.append(service)
|
|
|
|
if failed_services:
|
|
printfe(
|
|
"red",
|
|
f"\nFailed to start the following services: {', '.join(failed_services)}",
|
|
)
|
|
return 1
|
|
else:
|
|
printfe("green", "\nAll services started successfully")
|
|
return 0
|
|
else:
|
|
return run_docker_compose(["up", "-d"], service_name=args.service)
|
|
|
|
|
|
def cmd_stop(args):
|
|
"""Stop a Docker service"""
|
|
if args.all:
|
|
running_services = get_all_running_services()
|
|
if not running_services:
|
|
printfe("yellow", "No running services found to stop")
|
|
return 0
|
|
|
|
# Filter out the protected services
|
|
safe_services = [s for s in running_services if s not in PROTECTED_SERVICES]
|
|
|
|
# Check if protected services were filtered out
|
|
protected_running = [s for s in running_services if s in PROTECTED_SERVICES]
|
|
if protected_running:
|
|
printfe(
|
|
"yellow",
|
|
f"Note: {', '.join(protected_running)} will not be stopped "
|
|
"as they are protected services",
|
|
)
|
|
|
|
if not safe_services:
|
|
printfe(
|
|
"yellow", "No services to stop (all running services are protected)"
|
|
)
|
|
return 0
|
|
|
|
printfe("blue", f"Stopping all running services: {', '.join(safe_services)}")
|
|
|
|
failed_services = []
|
|
for service in safe_services:
|
|
printfe("blue", f"\n=== Stopping {service} ===")
|
|
result = run_docker_compose(["down"], service_name=service)
|
|
if result != 0:
|
|
failed_services.append(service)
|
|
|
|
if failed_services:
|
|
printfe(
|
|
"red",
|
|
f"\nFailed to stop the following services: {', '.join(failed_services)}",
|
|
)
|
|
return 1
|
|
else:
|
|
printfe("green", "\nAll running services stopped successfully")
|
|
return 0
|
|
# Check if trying to stop a protected service
|
|
if args.service in PROTECTED_SERVICES:
|
|
printfe(
|
|
"red",
|
|
f"Error: {args.service} is a protected service and cannot be stopped",
|
|
)
|
|
printfe(
|
|
"yellow",
|
|
f"The {args.service} service is required for other services to work properly",
|
|
)
|
|
return 1
|
|
return run_docker_compose(["down"], service_name=args.service)
|
|
|
|
|
|
def cmd_restart(args):
|
|
"""Restart a Docker service"""
|
|
return run_docker_compose(["restart"], service_name=args.service)
|
|
|
|
|
|
def get_all_running_services():
|
|
"""Return a list of all running services"""
|
|
if not os.path.exists(SERVICES_DIR):
|
|
return []
|
|
|
|
running_services = []
|
|
services = [
|
|
d
|
|
for d in os.listdir(SERVICES_DIR)
|
|
if os.path.isdir(os.path.join(SERVICES_DIR, d))
|
|
and os.path.exists(os.path.join(SERVICES_DIR, d, "docker-compose.yml"))
|
|
]
|
|
|
|
for service in services:
|
|
if check_service_running(service) > 0:
|
|
running_services.append(service)
|
|
|
|
return running_services
|
|
|
|
|
|
def cmd_update(args):
|
|
"""Update a Docker service by pulling new images and recreating containers if needed"""
|
|
if args.all:
|
|
running_services = get_all_running_services()
|
|
if not running_services:
|
|
printfe("yellow", "No running services found to update")
|
|
return 0
|
|
|
|
printfe("blue", f"Updating all running services: {', '.join(running_services)}")
|
|
|
|
failed_services = []
|
|
for service in running_services:
|
|
printfe("blue", f"\n=== Updating {service} ===")
|
|
|
|
# Pull the latest images
|
|
pull_result = run_docker_compose(["pull"], service_name=service)
|
|
|
|
# Bring the service up with the latest images
|
|
up_result = run_docker_compose(["up", "-d"], service_name=service)
|
|
|
|
if pull_result != 0 or up_result != 0:
|
|
failed_services.append(service)
|
|
|
|
if failed_services:
|
|
printfe(
|
|
"red",
|
|
f"\nFailed to update the following services: {', '.join(failed_services)}",
|
|
)
|
|
return 1
|
|
else:
|
|
printfe("green", "\nAll running services updated successfully")
|
|
return 0
|
|
|
|
# The original single-service update logic
|
|
# First pull the latest images
|
|
pull_result = run_docker_compose(["pull"], service_name=args.service)
|
|
if pull_result != 0:
|
|
return pull_result
|
|
|
|
# Then bring the service up with the latest images
|
|
return run_docker_compose(["up", "-d"], service_name=args.service)
|
|
|
|
|
|
def cmd_ps(args):
|
|
"""Show Docker service status"""
|
|
if args.service:
|
|
return run_docker_compose(["ps"], service_name=args.service)
|
|
else:
|
|
return run_docker_compose(["ps"])
|
|
|
|
|
|
def cmd_logs(args):
|
|
"""Show Docker service logs"""
|
|
cmd = ["logs"]
|
|
|
|
if args.follow:
|
|
cmd.append("-f")
|
|
|
|
if args.tail:
|
|
cmd.extend(["--tail", args.tail])
|
|
|
|
return run_docker_compose(cmd, service_name=args.service)
|
|
|
|
|
|
def check_service_running(service_name):
|
|
"""Check if service has running containers and return the count"""
|
|
compose_file = get_service_path(service_name)
|
|
if not compose_file:
|
|
return 0
|
|
|
|
result = subprocess.run(
|
|
["docker", "compose", "-f", compose_file, "ps", "--quiet"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
|
|
# Count non-empty lines to get container count
|
|
containers = [line for line in result.stdout.strip().split("\n") if line]
|
|
return len(containers)
|
|
|
|
|
|
def get_systemd_timer_status(timer_name):
|
|
"""Check if a systemd timer is active and enabled, and get next run time"""
|
|
# Check if timer is active (running/waiting)
|
|
active_result = subprocess.run(
|
|
["sudo", "systemctl", "is-active", timer_name],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
|
|
# Check if timer is enabled (will start on boot)
|
|
enabled_result = subprocess.run(
|
|
["sudo", "systemctl", "is-enabled", timer_name],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
|
|
# Check corresponding service status
|
|
service_name = timer_name.replace(".timer", ".service")
|
|
service_result = subprocess.run(
|
|
["sudo", "systemctl", "is-active", service_name],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
|
|
# Get next run time
|
|
list_result = subprocess.run(
|
|
["sudo", "systemctl", "list-timers", timer_name, "--no-legend"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
|
|
is_active = active_result.returncode == 0
|
|
is_enabled = enabled_result.returncode == 0
|
|
service_status = service_result.stdout.strip() if service_result else "unknown"
|
|
|
|
next_run = "unknown"
|
|
if list_result.returncode == 0 and list_result.stdout.strip():
|
|
parts = list_result.stdout.strip().split()
|
|
if len(parts) >= 4:
|
|
next_run = f"{parts[0]} {parts[1]} {parts[2]}"
|
|
|
|
return is_active, is_enabled, next_run, service_status
|
|
|
|
|
|
def cmd_list(args): # pylint: disable=unused-argument
|
|
"""List available Docker services and systemd services"""
|
|
# Docker services section
|
|
if not os.path.exists(SERVICES_DIR):
|
|
printfe("red", f"Error: Services directory not found at {SERVICES_DIR}")
|
|
return 1
|
|
|
|
services = [
|
|
d
|
|
for d in os.listdir(SERVICES_DIR)
|
|
if os.path.isdir(os.path.join(SERVICES_DIR, d))
|
|
and os.path.exists(os.path.join(SERVICES_DIR, d, "docker-compose.yml"))
|
|
]
|
|
|
|
if not services:
|
|
printfe("yellow", "No Docker services found")
|
|
else:
|
|
println("Available Docker services:", "blue")
|
|
for service in sorted(services):
|
|
container_count = check_service_running(service)
|
|
is_running = container_count > 0
|
|
|
|
if is_running:
|
|
status = (
|
|
f"[RUNNING - {container_count} container"
|
|
f"{'s' if container_count > 1 else ''}]"
|
|
)
|
|
color = "green"
|
|
else:
|
|
status = "[STOPPED]"
|
|
color = "red"
|
|
|
|
printfe(color, f" - {service:<20} {status}")
|
|
|
|
# Systemd services section
|
|
print()
|
|
println("System services:", "blue")
|
|
|
|
systemd_timers = ["borg-backup.timer", "borg-local-sync.timer", "dynamic-dns.timer"]
|
|
|
|
for timer in systemd_timers:
|
|
is_active, is_enabled, next_run, service_status = get_systemd_timer_status(
|
|
timer
|
|
)
|
|
service_name = timer.replace(".timer", "")
|
|
|
|
if service_status in ["activating", "active"]:
|
|
# Service is currently running
|
|
status = f"[🔄 RUNNING - next: {next_run}]"
|
|
color = "yellow"
|
|
elif is_active and is_enabled:
|
|
status = f"[TIMER ACTIVE - next: {next_run}]"
|
|
color = "green"
|
|
elif is_enabled:
|
|
status = "[TIMER ENABLED - INACTIVE]"
|
|
color = "yellow"
|
|
else:
|
|
status = "[TIMER DISABLED]"
|
|
color = "red"
|
|
|
|
printfe(color, f" - {service_name:<20} {status}")
|
|
|
|
return 0
|
|
|
|
|
|
def main():
|
|
"""Main entry point for managing Docker services."""
|
|
parser = argparse.ArgumentParser(description="Manage Docker services")
|
|
subparsers = parser.add_subparsers(dest="command", help="Command to run")
|
|
|
|
# Start command
|
|
start_parser = subparsers.add_parser("start", help="Start a Docker service")
|
|
start_group = start_parser.add_mutually_exclusive_group(required=True)
|
|
start_group.add_argument("--all", action="store_true", help="Start all services")
|
|
start_group.add_argument("service", nargs="?", help="Service to start")
|
|
start_group.add_argument(
|
|
"--service", dest="service", help="Service to start (deprecated)"
|
|
)
|
|
|
|
# Stop command
|
|
stop_parser = subparsers.add_parser("stop", help="Stop a Docker service")
|
|
stop_group = stop_parser.add_mutually_exclusive_group(required=True)
|
|
stop_group.add_argument(
|
|
"--all", action="store_true", help="Stop all running services"
|
|
)
|
|
stop_group.add_argument("service", nargs="?", help="Service to stop")
|
|
stop_group.add_argument(
|
|
"--service", dest="service", help="Service to stop (deprecated)"
|
|
)
|
|
|
|
# Restart command
|
|
restart_parser = subparsers.add_parser("restart", help="Restart a Docker service")
|
|
restart_parser.add_argument("service", help="Service to restart")
|
|
|
|
# Update command
|
|
update_parser = subparsers.add_parser(
|
|
"update",
|
|
help="Update a Docker service (pull new images and recreate if needed)",
|
|
)
|
|
update_group = update_parser.add_mutually_exclusive_group(required=True)
|
|
update_group.add_argument(
|
|
"--all", action="store_true", help="Update all running services"
|
|
)
|
|
update_group.add_argument("service", nargs="?", help="Service to update")
|
|
update_group.add_argument(
|
|
"--service", dest="service", help="Service to update (deprecated)"
|
|
)
|
|
|
|
# PS command
|
|
ps_parser = subparsers.add_parser("ps", help="Show Docker service status")
|
|
ps_parser.add_argument("service", nargs="?", help="Service to check")
|
|
|
|
# Logs command
|
|
logs_parser = subparsers.add_parser("logs", help="Show Docker service logs")
|
|
logs_parser.add_argument("service", help="Service to show logs for")
|
|
logs_parser.add_argument(
|
|
"-f", "--follow", action="store_true", help="Follow log output"
|
|
)
|
|
logs_parser.add_argument(
|
|
"--tail", help="Number of lines to show from the end of logs"
|
|
)
|
|
|
|
# List command and its alias
|
|
subparsers.add_parser("list", help="List available Docker services")
|
|
subparsers.add_parser("ls", help="List available Docker services (alias for list)")
|
|
|
|
# Parse arguments
|
|
args = parser.parse_args()
|
|
|
|
if not args.command:
|
|
parser.print_help()
|
|
return 1
|
|
|
|
# Execute the appropriate command
|
|
commands = {
|
|
"start": cmd_start,
|
|
"stop": cmd_stop,
|
|
"restart": cmd_restart,
|
|
"update": cmd_update,
|
|
"ps": cmd_ps,
|
|
"logs": cmd_logs,
|
|
"list": cmd_list,
|
|
"ls": cmd_list, # Alias 'ls' to the same function as 'list'
|
|
}
|
|
|
|
return commands[args.command](args)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|