#!/bin/python3 import click import logging import subprocess import json import os import tempfile import tarfile 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) def main(loglevel, source_app, dst_domain, target_server, move_volumes): 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) def move_app(source_app, target_server=False, target_domain=False, move_vols=False): #backup(source_app) print(abra('app', 'undeploy', '-n', source_app)) 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) print(output) 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 = ssh_key_pair_dir.split('/')[2] 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) 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) 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', '-i', public_key_path, destination_server], check=True) # Run rsync over SSH on the source server to copy files to the destination server source_rsync_cmd = f'rsync -avz -e "ssh -i {source_key_file} -o StrictHostKeyChecking=accept-new" {source_dir} {destination_server}:{destination_dir}' 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=False, target_domain=False, 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: copy_files_between_servers( source_server, volume_dir, target_server, target_dir) server = None if target_server and target_domain: server = target_server cmd = 'mv' elif target_domain: server = source_server cmd = 'cp -r' if move: cmd = 'mv' if target_domain: paths = run_ssh(server, f'echo {volume_dir}', True).split() if paths[0] == volume_dir: logging.error(f"Path {volume_dir} does not exists") return target_service = target_domain.replace(".", "_") for old_path in paths: container = old_path.split('_')[-1] new_path = f'{target_dir}/{target_service}_{container}' print(f'{cmd} {old_path} {new_path}') 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') copy_env(source_env, target_env) if 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"]) 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()