migrabrator/migrabrator.py

251 lines
9.8 KiB
Python
Raw Permalink Normal View History

2023-10-10 22:14:44 +00:00
#!/bin/python3
import click
import logging
import subprocess
import json
import os
import tempfile
2023-10-19 23:59:28 +00:00
import tarfile
from time import sleep
from icecream import ic
2023-10-10 22:14:44 +00:00
from shutil import copyfile, rmtree
from pathlib import Path
@click.command()
@click.option('-l', '--log', 'loglevel')
2023-10-19 23:59:28 +00:00
@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)
2024-03-20 11:40:04 +00:00
@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):
2023-10-10 22:14:44 +00:00
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)
2024-10-25 20:42:59 +00:00
move_app(source_app, target_server, dst_domain, move_volumes, undeploy, run_backup)
2024-04-22 14:37:26 +00:00
#copy_files_between_servers('source.example.com', '/var/lib/docker/', 'target.example.com', '/var/lib/docker')
2023-10-10 22:14:44 +00:00
2024-03-20 11:40:04 +00:00
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:
2024-10-29 16:06:08 +00:00
print(f"undeploy {source_app}")
2024-03-20 11:40:04 +00:00
print(abra('app', 'undeploy', '-n', source_app))
sleep(10)
2023-10-19 23:59:28 +00:00
copy_volumes(source_app, target_server, target_domain, move_vols)
2023-10-10 22:14:44 +00:00
copy_configs(source_app, target_server, target_domain)
copy_secrets(source_app, target_domain)
def backup(app):
2023-11-09 11:38:34 +00:00
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'])
2023-10-10 22:14:44 +00:00
def copy_files_between_servers(source_server, source_dir, destination_server, destination_dir):
# Generate temporary SSH key pair
ssh_key_pair_dir = tempfile.mkdtemp()
2023-11-09 11:42:32 +00:00
ssh_key_id = 'migrabrator_key'
2023-10-10 22:14:44 +00:00
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)
2023-10-10 22:14:44 +00:00
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)
2023-10-10 22:14:44 +00:00
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
2024-04-22 14:37:26 +00:00
subprocess.run(['ssh-copy-id', '-f' ,'-i', public_key_path,
destination_server], check=True, capture_output=True)
2023-10-10 22:14:44 +00:00
# Run rsync over SSH on the source server to copy files to the destination server
2024-04-22 14:47:01 +00:00
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)
2023-10-10 22:14:44 +00:00
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):
2023-10-10 22:14:44 +00:00
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)
2023-10-19 23:59:28 +00:00
source_service = source_app.replace(".", "_")
2023-10-10 22:14:44 +00:00
volume_dir = f'/var/lib/docker/volumes/{source_service}_*'
target_dir = f'/var/lib/docker/volumes'
if target_server and not target_domain:
2023-10-10 22:14:44 +00:00
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
2023-10-10 22:14:44 +00:00
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:
2023-10-19 23:59:28 +00:00
logging.error(f"Path {volume_dir} does not exists")
return
2023-10-10 22:14:44 +00:00
target_service = target_domain.replace(".", "_")
for old_path in source_paths:
2023-11-09 11:42:32 +00:00
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}')
2023-10-10 22:14:44 +00:00
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}")
2023-10-10 22:14:44 +00:00
copy_env(source_env, target_env)
if target_domain:
print(f"\t replace {source_app} with {target_domain}")
2023-10-10 22:14:44 +00:00
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):
2024-10-29 16:06:08 +00:00
print(f"Get Secretes from {source_app}...")
2023-10-10 22:14:44 +00:00
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)
2023-10-19 23:59:28 +00:00
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"])
2023-11-09 11:42:32 +00:00
# Todo: https://git.coopcloud.tech/coop-cloud/organising/issues/525
2024-10-25 20:42:59 +00:00
if output.returncode:
logging.error(f"Could not dump secrets for {source_app}")
exit()
2023-10-19 23:59:28 +00:00
with tarfile.open('/tmp/backup.tar.gz') as tar:
source_service = source_app.replace(".", "_")
file = tar.extractfile(f"{source_service}.json")
2023-10-10 22:14:44 +00:00
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()