#!/bin/python3 import click import logging import subprocess import json import os import tempfile import tarfile from time import sleep from icecream import ic from shutil import copyfile, rmtree from pathlib import Path @click.command() @click.option('-l', '--log', 'loglevel') @click.argument('source_app') @click.option('dst_domain', '--dst_domain', '-d') @click.option('target_server', '--target_server', '-s') @click.option('move_volumes', '--move-volumes', '-m', is_flag=True) @click.option('undeploy', '--undeploy', '-u', is_flag=True) @click.option('run_backup', '--run-backup', '-b', is_flag=True) def main(loglevel, source_app, dst_domain, target_server, move_volumes, undeploy, run_backup): if loglevel: numeric_level = getattr(logging, loglevel.upper(), None) if not isinstance(numeric_level, int): raise ValueError('Invalid log level: %s' % loglevel) logging.basicConfig(level=numeric_level) #move_app(source_app, target_server, dst_domain, move_volumes, undeploy, run_backup) #copy_files_between_servers('source.example.com', '/var/lib/docker/', 'target.example.com', '/var/lib/docker') def move_app(source_app, target_server=False, target_domain=False, move_vols=False, undeploy=False, run_backup=False): if run_backup: backup(source_app) if undeploy: print(abra('app', 'undeploy', '-n', source_app)) sleep(10) copy_volumes(source_app, target_server, target_domain, move_vols) copy_configs(source_app, target_server, target_domain) copy_secrets(source_app, target_domain) def backup(app): logging.info(f"Start Backup of {app}...") #output = abra('app', 'backup', 'create', app) backupbot = get_backupbot(app) print(f'Run backupbot on {backupbot}') subprocess.run(['abra', 'app', 'run', backupbot, 'app', '--', 'backup','-m','create']) def copy_files_between_servers(source_server, source_dir, destination_server, destination_dir): # Generate temporary SSH key pair ssh_key_pair_dir = tempfile.mkdtemp() ssh_key_id = 'migrabrator_key' private_key_path = os.path.join(ssh_key_pair_dir, 'id_ed25519') public_key_path = os.path.join(ssh_key_pair_dir, 'id_ed25519.pub') subprocess.run(['ssh-keygen', '-t', 'ed25519', '-N', '', '-f', private_key_path, '-C', ssh_key_id], check=True, capture_output=True) try: # Copy the private key to the source server source_key_dir = run_ssh(source_server, 'mktemp -d', True) subprocess.run( ['scp', private_key_path, f'{source_server}:{source_key_dir}'], check=True, capture_output=True) source_key_file = f'{source_key_dir}/id_ed25519' run_ssh(source_server, f'chmod 600 {source_key_file}') # Add the public key to the authorized hosts of the destination server subprocess.run(['ssh-copy-id', '-f' ,'-i', public_key_path, destination_server], check=True, capture_output=True) # Run rsync over SSH on the source server to copy files to the destination server source_rsync_cmd = f'rsync -az -S --delete --info=progress2 -e "ssh -i {source_key_file} -o StrictHostKeyChecking=accept-new" {source_dir} {destination_server}:{destination_dir}' print(source_rsync_cmd) run_ssh(source_server, source_rsync_cmd) # Remove the SSH key pair from the source server run_ssh(source_server, f'rm -r {source_key_dir}') # Remove the public key from the authorized hosts of the destination server run_ssh(destination_server, f'sed -i.bak "/{ssh_key_id}/d" ~/.ssh/authorized_keys') finally: # Remove the temporary SSH key pair directory rmtree(ssh_key_pair_dir) def copy_volumes(source_app, target_server, target_domain, move=False): if not any([target_domain, target_server]): logging.error( 'At leat one of target_domain or target_app need to be speicified') exit(1) source_server = get_server(source_app) source_service = source_app.replace(".", "_") volume_dir = f'/var/lib/docker/volumes/{source_service}_*' target_dir = f'/var/lib/docker/volumes' if target_server and not target_domain: copy_files_between_servers( source_server, volume_dir, target_server, target_dir) if move: cmd = 'mv' else: cmd = 'rsync -a --delete' if target_server: server = target_server else: server = source_server if target_domain: source_paths = run_ssh(source_server, f'echo {volume_dir}', True) if not source_paths: logging.error("No path for {volume_dir} found") exit(1) source_paths = source_paths.split() if source_paths[0] == volume_dir: logging.error(f"Path {volume_dir} does not exists") return target_service = target_domain.replace(".", "_") for old_path in source_paths: new_dir = Path(old_path).name.replace(source_service, target_service) new_path = f'{target_dir}/{new_dir}' print(f'{cmd} {old_path}/ {new_path}') if target_server: copy_files_between_servers(source_server, f'{old_path}/', target_server, new_path) else: run_ssh(server, f'{cmd} {old_path}/ {new_path}') def copy_configs(source_app, target_server=False, target_domain=False): source_server = get_server(source_app) target_path= source_path = Path(f"~/.abra/servers/{source_server}").expanduser() if target_server: target_path = Path(f"~/.abra/servers/{target_server}").expanduser() source_env = source_path.joinpath(f'{source_app}.env') if target_domain: target_env = target_path.joinpath(f'{target_domain}.env') else: target_env = target_path.joinpath(f'{source_app}.env') print(f"copy env {source_env} to {target_env}") copy_env(source_env, target_env) if target_domain: print(f"\t replace {source_app} with {target_domain}") replace_domain(source_app, target_domain, target_env) def copy_env(source_env, target_env): if not source_env.exists(): logging.error(f"file {source_env} not found") exit(1) copyfile(source_env, target_env) def replace_domain(source_app, target_domain, target_env): source_domain = get_domain(source_app) replace_string_in_file(target_env, source_domain, target_domain) def replace_string_in_file(file_path, search_string, replace_string): try: with open(file_path, 'r') as file: content = file.read() modified_content = content.replace(search_string, replace_string) with open(file_path, 'w') as file: file.write(modified_content) except FileNotFoundError: print(f"File '{file_path}' not found.") except Exception as e: print(f"An error occurred: {str(e)}") def get_server(app_name): server_list = json.loads(abra('app', 'ls', '-m')) for server in server_list.values(): for app in server['apps']: if app['appName'] == app_name: return app['server'] def get_domain(app_name): server_list = json.loads(abra('app', 'ls', '-m')) for server in server_list.values(): for app in server['apps']: if app['appName'] == app_name: return app['domain'] def copy_secrets(source_app, target_domain): secrets = get_secrets(source_app) insert_secrets(target_domain, secrets) def get_secrets(source_app): # TODO: replace with abra app backup command backupbot = get_backupbot(source_app) subprocess.run(['abra', 'app', 'run', backupbot, 'app', '--', 'backup', '-h', source_app, 'download', '--secrets']) output = subprocess.run(['abra', 'app', 'cp', backupbot, f"app:/tmp/backup.tar.gz", "/tmp"]) # Todo: https://git.coopcloud.tech/coop-cloud/organising/issues/525 # if output.returncode: # logging.error(f"Could not dump secrets for {source_app}") # exit() with tarfile.open('/tmp/backup.tar.gz') as tar: source_service = source_app.replace(".", "_") file = tar.extractfile(f"{source_service}.json") secrets = json.load(file) return secrets def get_backupbot(app_name): server = get_server(app_name) server_list = json.loads(abra('app', 'ls', '-m')) for app in server_list[server]['apps']: if app['recipe'] == 'backup-bot-two': return app['appName'] def insert_secrets(target_domain, secrets): for sec in secrets: secret = secrets[sec] secret_name = "_".join(sec.split('_')[:-1]) secret_version = sec.split('_')[-1] cmd = ['abra', 'app', 'secret', 'insert', target_domain, secret_name, secret_version, secret] print(" ".join(cmd)) output = subprocess.run(cmd, capture_output=True, text=True) logging.debug(output.stdout) if output.returncode: logging.error(output.stderr) def run_ssh(server, cmd, get_output=False): output = subprocess.run(["ssh", server] + cmd.split(), capture_output=get_output, check=True, text=True) if get_output: return output.stdout.strip() def abra(*args, machine_output=False, ignore_error=False): command = ["abra", *args] if machine_output: command.append("-m") logging.debug(f"run command: {' '.join(command)}") process = subprocess.run(command, capture_output=True, text=True) if process.stderr: logging.debug(process.stderr) if process.stdout: logging.debug(process.stdout) if process.returncode and not ignore_error: raise RuntimeError( f'{" ".join(command)} \n STDOUT: \n {process.stdout} \n STDERR: {process.stderr}') if machine_output: return json.loads(process.stdout) return process.stdout if __name__ == '__main__': main()