diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-21 11:54:28 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-21 11:54:28 +0000 |
commit | e6918187568dbd01842d8d1d2c808ce16a894239 (patch) | |
tree | 64f88b554b444a49f656b6c656111a145cbbaa28 /src/cephadm/box/box.py | |
parent | Initial commit. (diff) | |
download | ceph-e6918187568dbd01842d8d1d2c808ce16a894239.tar.xz ceph-e6918187568dbd01842d8d1d2c808ce16a894239.zip |
Adding upstream version 18.2.2.upstream/18.2.2
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/cephadm/box/box.py')
-rwxr-xr-x | src/cephadm/box/box.py | 414 |
1 files changed, 414 insertions, 0 deletions
diff --git a/src/cephadm/box/box.py b/src/cephadm/box/box.py new file mode 100755 index 000000000..db2f24233 --- /dev/null +++ b/src/cephadm/box/box.py @@ -0,0 +1,414 @@ +#!/bin/python3 +import argparse +import os +import stat +import json +import sys +import host +import osd +from multiprocessing import Process, Pool +from util import ( + BoxType, + Config, + Target, + ensure_inside_container, + ensure_outside_container, + get_boxes_container_info, + run_cephadm_shell_command, + run_dc_shell_command, + run_dc_shell_commands, + get_container_engine, + run_shell_command, + run_shell_commands, + ContainerEngine, + DockerEngine, + PodmanEngine, + colored, + engine, + engine_compose, + Colors, + get_seed_name +) + +CEPH_IMAGE = 'quay.ceph.io/ceph-ci/ceph:main' +BOX_IMAGE = 'cephadm-box:latest' + +# NOTE: this image tar is a trickeroo so cephadm won't pull the image everytime +# we deploy a cluster. Keep in mind that you'll be responsible for pulling the +# image yourself with `./box.py -v cluster setup` +CEPH_IMAGE_TAR = 'docker/ceph/image/quay.ceph.image.tar' +CEPH_ROOT = '../../../' +DASHBOARD_PATH = '../../../src/pybind/mgr/dashboard/frontend/' + +root_error_msg = """ +WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING +sudo with this script can kill your computer, try again without sudo +if you value your time. +""" + +def remove_ceph_image_tar(): + if os.path.exists(CEPH_IMAGE_TAR): + os.remove(CEPH_IMAGE_TAR) + + +def cleanup_box() -> None: + osd.cleanup_osds() + remove_ceph_image_tar() + + +def image_exists(image_name: str): + # extract_tag + assert image_name.find(':') + image_name, tag = image_name.split(':') + engine = get_container_engine() + images = engine.run('image ls').split('\n') + IMAGE_NAME = 0 + TAG = 1 + for image in images: + image = image.split() + print(image) + print(image_name, tag) + if image[IMAGE_NAME] == image_name and image[TAG] == tag: + return True + return False + + +def get_ceph_image(): + print('Getting ceph image') + engine = get_container_engine() + engine.run(f'pull {CEPH_IMAGE}') + # update + engine.run(f'build -t {CEPH_IMAGE} docker/ceph') + if not os.path.exists('docker/ceph/image'): + os.mkdir('docker/ceph/image') + + remove_ceph_image_tar() + + engine.run(f'save {CEPH_IMAGE} -o {CEPH_IMAGE_TAR}') + run_shell_command(f'chmod 777 {CEPH_IMAGE_TAR}') + print('Ceph image added') + + +def get_box_image(): + print('Getting box image') + engine = get_container_engine() + engine.run(f'build -t cephadm-box -f {engine.dockerfile} .') + print('Box image added') + +def check_dashboard(): + if not os.path.exists(os.path.join(CEPH_ROOT, 'dist')): + print(colored('Missing build in dashboard', Colors.WARNING)) + +def check_cgroups(): + if not os.path.exists('/sys/fs/cgroup/cgroup.controllers'): + print(colored('cgroups v1 is not supported', Colors.FAIL)) + print('Enable cgroups v2 please') + sys.exit(666) + +def check_selinux(): + selinux = run_shell_command('getenforce') + if 'Disabled' not in selinux: + print(colored('selinux should be disabled, please disable it if you ' + 'don\'t want unexpected behaviour.', Colors.WARNING)) +def dashboard_setup(): + command = f'cd {DASHBOARD_PATH} && npm install' + run_shell_command(command) + command = f'cd {DASHBOARD_PATH} && npm run build' + run_shell_command(command) + +class Cluster(Target): + _help = 'Manage docker cephadm boxes' + actions = ['bootstrap', 'start', 'down', 'list', 'bash', 'setup', 'cleanup'] + + def set_args(self): + self.parser.add_argument( + 'action', choices=Cluster.actions, help='Action to perform on the box' + ) + self.parser.add_argument('--osds', type=int, default=3, help='Number of osds') + + self.parser.add_argument('--hosts', type=int, default=1, help='Number of hosts') + self.parser.add_argument('--skip-deploy-osds', action='store_true', help='skip deploy osd') + self.parser.add_argument('--skip-create-loop', action='store_true', help='skip create loopback device') + self.parser.add_argument('--skip-monitoring-stack', action='store_true', help='skip monitoring stack') + self.parser.add_argument('--skip-dashboard', action='store_true', help='skip dashboard') + self.parser.add_argument('--expanded', action='store_true', help='deploy 3 hosts and 3 osds') + self.parser.add_argument('--jobs', type=int, help='Number of jobs scheduled in parallel') + + @ensure_outside_container + def setup(self): + check_cgroups() + check_selinux() + + targets = [ + get_ceph_image, + get_box_image, + dashboard_setup + ] + results = [] + jobs = Config.get('jobs') + if jobs: + jobs = int(jobs) + else: + jobs = None + pool = Pool(jobs) + for target in targets: + results.append(pool.apply_async(target)) + + for result in results: + result.wait() + + + @ensure_outside_container + def cleanup(self): + cleanup_box() + + @ensure_inside_container + def bootstrap(self): + print('Running bootstrap on seed') + cephadm_path = str(os.environ.get('CEPHADM_PATH')) + + engine = get_container_engine() + if isinstance(engine, DockerEngine): + engine.restart() + st = os.stat(cephadm_path) + os.chmod(cephadm_path, st.st_mode | stat.S_IEXEC) + + engine.run('load < /cephadm/box/docker/ceph/image/quay.ceph.image.tar') + # cephadm guid error because it sometimes tries to use quay.ceph.io/ceph-ci/ceph:<none> + # instead of main branch's tag + run_shell_command('export CEPH_SOURCE_FOLDER=/ceph') + run_shell_command('export CEPHADM_IMAGE=quay.ceph.io/ceph-ci/ceph:main') + run_shell_command( + 'echo "export CEPHADM_IMAGE=quay.ceph.io/ceph-ci/ceph:main" >> ~/.bashrc' + ) + + extra_args = [] + + extra_args.append('--skip-pull') + + # cephadm prints in warning, let's redirect it to the output so shell_command doesn't + # complain + extra_args.append('2>&0') + + extra_args = ' '.join(extra_args) + skip_monitoring_stack = ( + '--skip-monitoring-stack' if Config.get('skip-monitoring-stack') else '' + ) + skip_dashboard = '--skip-dashboard' if Config.get('skip-dashboard') else '' + + fsid = Config.get('fsid') + config_folder = str(Config.get('config_folder')) + config = str(Config.get('config')) + keyring = str(Config.get('keyring')) + if not os.path.exists(config_folder): + os.mkdir(config_folder) + + cephadm_bootstrap_command = ( + '$CEPHADM_PATH --verbose bootstrap ' + '--mon-ip "$(hostname -i)" ' + '--allow-fqdn-hostname ' + '--initial-dashboard-password admin ' + '--dashboard-password-noupdate ' + '--shared_ceph_folder /ceph ' + '--allow-overwrite ' + f'--output-config {config} ' + f'--output-keyring {keyring} ' + f'--output-config {config} ' + f'--fsid "{fsid}" ' + '--log-to-file ' + f'{skip_dashboard} ' + f'{skip_monitoring_stack} ' + f'{extra_args} ' + ) + + print('Running cephadm bootstrap...') + run_shell_command(cephadm_bootstrap_command, expect_exit_code=120) + print('Cephadm bootstrap complete') + + run_shell_command('sudo vgchange --refresh') + run_shell_command('cephadm ls') + run_shell_command('ln -s /ceph/src/cephadm/box/box.py /usr/bin/box') + + run_cephadm_shell_command('ceph -s') + + print('Bootstrap completed!') + + @ensure_outside_container + def start(self): + check_cgroups() + check_selinux() + osds = int(Config.get('osds')) + hosts = int(Config.get('hosts')) + engine = get_container_engine() + + # ensure boxes don't exist + self.down() + + # podman is ran without sudo + if isinstance(engine, PodmanEngine): + I_am = run_shell_command('whoami') + if 'root' in I_am: + print(root_error_msg) + sys.exit(1) + + print('Checking docker images') + if not image_exists(CEPH_IMAGE): + get_ceph_image() + if not image_exists(BOX_IMAGE): + get_box_image() + + used_loop = "" + if not Config.get('skip_create_loop'): + print('Creating OSD devices...') + used_loop = osd.create_loopback_devices(osds) + print(f'Added {osds} logical volumes in a loopback device') + + print('Starting containers') + + engine.up(hosts) + + containers = engine.get_containers() + seed = engine.get_seed() + # Umounting somehow brings back the contents of the host /sys/dev/block. + # On startup /sys/dev/block is empty. After umount, we can see symlinks again + # so that lsblk is able to run as expected + run_dc_shell_command('umount /sys/dev/block', seed) + + run_shell_command('sudo sysctl net.ipv4.conf.all.forwarding=1') + run_shell_command('sudo iptables -P FORWARD ACCEPT') + + # don't update clock with chronyd / setup chronyd on all boxes + chronyd_setup = """ + sed 's/$OPTIONS/-x/g' /usr/lib/systemd/system/chronyd.service -i + systemctl daemon-reload + systemctl start chronyd + systemctl status --no-pager chronyd + """ + for container in containers: + print(colored('Got container:', Colors.OKCYAN), str(container)) + for container in containers: + run_dc_shell_commands(chronyd_setup, container) + + print('Seting up host ssh servers') + for container in containers: + print(colored('Setting up ssh server for:', Colors.OKCYAN), str(container)) + host._setup_ssh(container) + + verbose = '-v' if Config.get('verbose') else '' + skip_deploy = '--skip-deploy-osds' if Config.get('skip-deploy-osds') else '' + skip_monitoring_stack = ( + '--skip-monitoring-stack' if Config.get('skip-monitoring-stack') else '' + ) + skip_dashboard = '--skip-dashboard' if Config.get('skip-dashboard') else '' + box_bootstrap_command = ( + f'/cephadm/box/box.py {verbose} --engine {engine.command} cluster bootstrap ' + f'--osds {osds} ' + f'--hosts {hosts} ' + f'{skip_deploy} ' + f'{skip_dashboard} ' + f'{skip_monitoring_stack} ' + ) + print(box_bootstrap_command) + run_dc_shell_command(box_bootstrap_command, seed) + + expanded = Config.get('expanded') + if expanded: + info = get_boxes_container_info() + ips = info['ips'] + hostnames = info['hostnames'] + print(ips) + if hosts > 0: + host._copy_cluster_ssh_key(ips) + host._add_hosts(ips, hostnames) + if not Config.get('skip-deploy-osds'): + print('Deploying osds... This could take up to minutes') + osd.deploy_osds(osds) + print('Osds deployed') + + + dashboard_ip = 'localhost' + info = get_boxes_container_info(with_seed=True) + if isinstance(engine, DockerEngine): + for i in range(info['size']): + if get_seed_name() in info['container_names'][i]: + dashboard_ip = info["ips"][i] + print(colored(f'dashboard available at https://{dashboard_ip}:8443', Colors.OKGREEN)) + + print('Bootstrap finished successfully') + + @ensure_outside_container + def down(self): + engine = get_container_engine() + if isinstance(engine, PodmanEngine): + containers = json.loads(engine.run('container ls --format json')) + for container in containers: + for name in container['Names']: + if name.startswith('box_hosts_'): + engine.run(f'container kill {name}') + engine.run(f'container rm {name}') + pods = json.loads(engine.run('pod ls --format json')) + for pod in pods: + if 'Name' in pod and pod['Name'].startswith('box_pod_host'): + name = pod['Name'] + engine.run(f'pod kill {name}') + engine.run(f'pod rm {name}') + else: + run_shell_command(f'{engine_compose()} -f {Config.get("docker_yaml")} down') + print('Successfully killed all boxes') + + @ensure_outside_container + def list(self): + info = get_boxes_container_info(with_seed=True) + for i in range(info['size']): + ip = info['ips'][i] + name = info['container_names'][i] + hostname = info['hostnames'][i] + print(f'{name} \t{ip} \t{hostname}') + + @ensure_outside_container + def bash(self): + # we need verbose to see the prompt after running shell command + Config.set('verbose', True) + print('Seed bash') + engine = get_container_engine() + engine.run(f'exec -it {engine.seed_name} bash') + + +targets = { + 'cluster': Cluster, + 'osd': osd.Osd, + 'host': host.Host, +} + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + '-v', action='store_true', dest='verbose', help='be more verbose' + ) + parser.add_argument( + '--engine', type=str, default='podman', + dest='engine', help='choose engine between "docker" and "podman"' + ) + + subparsers = parser.add_subparsers() + target_instances = {} + for name, target in targets.items(): + target_instances[name] = target(None, subparsers) + + for count, arg in enumerate(sys.argv, 1): + if arg in targets: + instance = target_instances[arg] + if hasattr(instance, 'main'): + instance.argv = sys.argv[count:] + instance.set_args() + args = parser.parse_args() + Config.add_args(vars(args)) + instance.main() + sys.exit(0) + + parser.print_help() + + +if __name__ == '__main__': + main() |