migrabrator/migrabrator.py

249 lines
9.8 KiB
Python
Executable File

#!/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()