From: Pere Diaz Bou Date: Mon, 9 Jan 2023 17:02:12 +0000 (+0100) Subject: cephadm/box: add container engine class X-Git-Tag: v18.1.0~410^2~2 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=e70de13cf87c99a066867438d6b18dac93563bf2;p=ceph.git cephadm/box: add container engine class ContainerEngine is the baseclass of PodmanEngine and DockerEngine. Furthermore, a HostContainer class was added as struct of info related to a container. Signed-off-by: Pere Diaz Bou --- diff --git a/src/cephadm/box/box.py b/src/cephadm/box/box.py index ee6ad168d97e..fca55403c6ec 100755 --- a/src/cephadm/box/box.py +++ b/src/cephadm/box/box.py @@ -16,8 +16,12 @@ from util import ( 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, @@ -55,7 +59,8 @@ def image_exists(image_name: str): # extract_tag assert image_name.find(':') image_name, tag = image_name.split(':') - images = run_shell_command(f'{engine()} image ls').split('\n') + engine = get_container_engine() + images = engine.run('image ls').split('\n') IMAGE_NAME = 0 TAG = 1 for image in images: @@ -69,25 +74,24 @@ def image_exists(image_name: str): def get_ceph_image(): print('Getting ceph image') - run_shell_command(f'{engine()} pull {CEPH_IMAGE}') + engine = get_container_engine() + engine.run('pull {CEPH_IMAGE}') # update - run_shell_command(f'{engine()} build -t {CEPH_IMAGE} docker/ceph') + engine.run('build -t {CEPH_IMAGE} docker/ceph') if not os.path.exists('docker/ceph/image'): os.mkdir('docker/ceph/image') remove_ceph_image_tar() - run_shell_command(f'{engine()} save {CEPH_IMAGE} -o {CEPH_IMAGE_TAR}') + engine.run('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') - if engine() == 'docker': - run_shell_command(f'{engine()} build -t cephadm-box -f DockerfileDocker .') - else: - run_shell_command(f'{engine()} build -t cephadm-box -f DockerfilePodman .') + engine = get_container_engine() + engine.run(f'build -t cephadm-box -f {engine.dockerfile} .') print('Box image added') def check_dashboard(): @@ -106,65 +110,9 @@ def check_selinux(): print(colored('selinux should be disabled, please disable it if you ' 'don\'t want unexpected behaviour.', Colors.WARNING)) -def setup_podman_env(hosts: int = 1, osd_devs={}): - network_name = 'box_network' - networks = run_shell_command('podman network ls') - if network_name not in networks: - run_shell_command(f'podman network create -d bridge {network_name}') - - run_default_options = """--group-add keep-groups --device /dev/fuse -it -d \\ - --cap-add SYS_ADMIN \\ - --cap-add NET_ADMIN \\ - --cap-add SYS_TIME \\ - --cap-add SYS_RAWIO \\ - --cap-add MKNOD \\ - --cap-add NET_RAW \\ - --cap-add SETUID \\ - --cap-add SETGID \\ - --cap-add CHOWN \\ - --cap-add SYS_PTRACE \\ - --cap-add SYS_TTY_CONFIG \\ - --cap-add CAP_AUDIT_WRITE \\ - --cap-add CAP_AUDIT_CONTROL \\ - -e CEPH_BRANCH=main \\ - -v ../../../:/ceph:z \\ - -v ../:/cephadm:z \\ - -v /run/udev:/run/udev \\ - --tmpfs /run \\ - --tmpfs /tmp \\ - -v /sys/dev/block:/sys/dev/block \\ - -v /sys/fs/cgroup:/sys/fs/cgroup:ro \\ - -v /dev/fuse:/dev/fuse \\ - -v /dev/disk:/dev/disk \\ - -v /sys/devices/virtual/block:/sys/devices/virtual/block \\ - -v /sys/block:/dev/block \\ - -v /dev/mapper:/dev/mapper \\ - -v /dev/mapper/control:/dev/mapper/control \\ - --stop-signal RTMIN+3 -m 20g cephadm-box \\ - """ - def add_option(dest, src): - dest = f'{src} {dest}' - return dest - for osd_dev in osd_devs.values(): - device = osd_dev["device"] - run_default_options = add_option(run_default_options, f'--device {device}:{device}') - - - for host in range(hosts+1): # 0 will be the seed - options = run_default_options - options = add_option(options, f'--name box_hosts_{host}') - options = add_option(options, f'--network {network_name}') - if host == 0: - options = add_option(options, f'-p 8443:8443') # dashboard - options = add_option(options, f'-p 3000:3000') # grafana - options = add_option(options, f'-p 9093:9093') # alertmanager - options = add_option(options, f'-p 9095:9095') # prometheus - - run_shell_command(f'podman run {options}') - class Cluster(Target): _help = 'Manage docker cephadm boxes' - actions = ['bootstrap', 'start', 'down', 'list', 'sh', 'setup', 'cleanup', 'doctor'] + actions = ['bootstrap', 'start', 'down', 'list', 'sh', 'setup', 'cleanup'] def set_args(self): self.parser.add_argument( @@ -196,16 +144,13 @@ class Cluster(Target): print('Running bootstrap on seed') cephadm_path = str(os.environ.get('CEPHADM_PATH')) - if engine() == 'docker': - # restart to ensure docker is using daemon.json - run_shell_command( - 'systemctl restart docker' - ) - + 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) - run_shell_command(f'{engine()} load < /cephadm/box/docker/ceph/image/quay.ceph.image.tar') + 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: # instead of main branch's tag run_shell_command('export CEPH_SOURCE_FOLDER=/ceph') @@ -254,7 +199,7 @@ class Cluster(Target): ) print('Running cephadm bootstrap...') - run_shell_command(cephadm_bootstrap_command) + run_shell_command(cephadm_bootstrap_command, expect_exit_code=120) print('Cephadm bootstrap complete') run_shell_command('sudo vgchange --refresh') @@ -271,12 +216,13 @@ class Cluster(Target): 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 engine() == 'podman': + if isinstance(engine, PodmanEngine): I_am = run_shell_command('whoami') if 'root' in I_am: print(root_error_msg) @@ -296,18 +242,14 @@ class Cluster(Target): print('Starting containers') - if engine() == 'docker': - dcflags = f'-f {Config.get("docker_yaml")}' - if not os.path.exists('/sys/fs/cgroup/cgroup.controllers'): - dcflags += f' -f {Config.get("docker_v1_yaml")}' - run_shell_command(f'{engine_compose()} {dcflags} up --scale hosts={hosts} -d') - else: - setup_podman_env(hosts=hosts, osd_devs=osd.load_osd_devices()) + 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', 1, BoxType.SEED) + 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') @@ -319,15 +261,15 @@ class Cluster(Target): systemctl start chronyd systemctl status --no-pager chronyd """ - for h in range(hosts): - run_dc_shell_commands(h + 1, BoxType.HOST, chronyd_setup) - run_dc_shell_commands(1, BoxType.SEED, chronyd_setup) + 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 h in range(hosts): - host._setup_ssh(BoxType.HOST, h + 1) - - host._setup_ssh(BoxType.SEED, 1) + 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 '' @@ -336,15 +278,15 @@ class Cluster(Target): ) skip_dashboard = '--skip-dashboard' if Config.get('skip-dashboard') else '' box_bootstrap_command = ( - f'/cephadm/box/box.py {verbose} --engine {engine()} cluster bootstrap ' + 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} ' ) - run_dc_shell_command(box_bootstrap_command, 1, BoxType.SEED) - + print(box_bootstrap_command) + run_dc_shell_command(box_bootstrap_command, seed) expanded = Config.get('expanded') if expanded: @@ -363,7 +305,7 @@ class Cluster(Target): dashboard_ip = 'localhost' info = get_boxes_container_info(with_seed=True) - if engine() == 'docker': + if isinstance(engine, DockerEngine): for i in range(info['size']): if get_seed_name() in info['container_names'][i]: dashboard_ip = info["ips"][i] @@ -371,25 +313,22 @@ class Cluster(Target): print('Bootstrap finished successfully') - @ensure_outside_container - def doctor(self): - pass - @ensure_outside_container def down(self): - if engine() == 'podman': - containers = json.loads(run_shell_command('podman container ls --format json')) + 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_'): - run_shell_command(f'podman container kill {name}') - run_shell_command(f'podman container rm {name}') - pods = json.loads(run_shell_command('podman pod ls --format json')) + 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'] - run_shell_command(f'podman pod kill {name}') - run_shell_command(f'podman pod rm {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') diff --git a/src/cephadm/box/host.py b/src/cephadm/box/host.py index cb663a61d951..aae16d07f453 100644 --- a/src/cephadm/box/host.py +++ b/src/cephadm/box/host.py @@ -3,8 +3,10 @@ from typing import List, Union from util import ( Config, + HostContainer, Target, get_boxes_container_info, + get_container_engine, inside_container, run_cephadm_shell_command, run_dc_shell_command, @@ -14,10 +16,11 @@ from util import ( ) -def _setup_ssh(container_type: BoxType, container_index): +def _setup_ssh(container: HostContainer): if inside_container(): if not os.path.exists('/root/.ssh/known_hosts'): - run_shell_command('ssh-keygen -b 2048 -t rsa -f /root/.ssh/id_rsa -q -N ""') + run_shell_command('echo "y" | ssh-keygen -b 2048 -t rsa -f /root/.ssh/id_rsa -q -N ""', + expect_error=True) run_shell_command('echo "root:root" | chpasswd') with open('/etc/ssh/sshd_config', 'a+') as f: @@ -29,9 +32,8 @@ def _setup_ssh(container_type: BoxType, container_index): print('Redirecting to _setup_ssh to container') verbose = '-v' if Config.get('verbose') else '' run_dc_shell_command( - f'/cephadm/box/box.py {verbose} --engine {engine()} host setup_ssh {BoxType.to_string(container_type)} {container_index}', - container_index, - container_type, + f'/cephadm/box/box.py {verbose} --engine {engine()} host setup_ssh {container.name}', + container ) @@ -48,11 +50,11 @@ def _add_hosts(ips: Union[List[str], str], hostnames: Union[List[str], str]): ips = f'{ips}' hostnames = ' '.join(hostnames) hostnames = f'{hostnames}' + seed = get_container_engine().get_seed() run_dc_shell_command( - f'/cephadm/box/box.py {verbose} --engine {engine()} host add_hosts seed {BoxType.to_string(BoxType.SEED)} --ips {ips} --hostnames {hostnames}', - 1, - BoxType.SEED, - ) + f'/cephadm/box/box.py {verbose} --engine {engine()} host add_hosts {seed.name} --ips {ips} --hostnames {hostnames}', + seed + ) def _copy_cluster_ssh_key(ips: Union[List[str], str]): @@ -74,10 +76,10 @@ def _copy_cluster_ssh_key(ips: Union[List[str], str]): ips = ' '.join(ips) ips = f'{ips}' # assume we only have one seed + seed = get_container_engine().get_seed() run_dc_shell_command( - f'/cephadm/box/box.py {verbose} --engine {engine()} host copy_cluster_ssh_key {BoxType.to_string(BoxType.SEED)} 1 --ips {ips}', - 1, - BoxType.SEED, + f'/cephadm/box/box.py {verbose} --engine {engine()} host copy_cluster_ssh_key {seed.name} --ips {ips}', + seed ) @@ -88,10 +90,9 @@ class Host(Target): def set_args(self): self.parser.add_argument('action', choices=Host.actions) self.parser.add_argument( - 'container_type', type=str, help='box_{type}_{index}' - ) - self.parser.add_argument( - 'container_index', type=str, help='box_{type}_{index}' + 'container_name', + type=str, + help='box_{type}_{index}. In docker, type can be seed or hosts. In podman only hosts.' ) self.parser.add_argument('--ips', nargs='*', help='List of host ips') self.parser.add_argument( @@ -99,9 +100,9 @@ class Host(Target): ) def setup_ssh(self): - type_ = Config.get('container_type') - index = Config.get('container_index') - _setup_ssh(type_, index) + container_name = Config.get('container_name') + engine = get_container_engine() + _setup_ssh(engine.get_container(container_name)) def add_hosts(self): ips = Config.get('ips') diff --git a/src/cephadm/box/osd.py b/src/cephadm/box/osd.py index 6ce3a1d6fea5..b57af42434ac 100644 --- a/src/cephadm/box/osd.py +++ b/src/cephadm/box/osd.py @@ -12,6 +12,7 @@ from util import ( get_orch_hosts, run_cephadm_shell_command, run_dc_shell_command, + get_container_engine, run_shell_command, ) @@ -100,6 +101,7 @@ def deploy_osds(count: int): osd_devs = load_osd_devices() hosts = get_orch_hosts() host_index = 0 + seed = get_container_engine().get_seed() v = '-v' if Config.get('verbose') else '' for osd in osd_devs.values(): deployed = False @@ -108,8 +110,7 @@ def deploy_osds(count: int): hostname = hosts[host_index]['hostname'] deployed = run_dc_shell_command( f'/cephadm/box/box.py {v} osd deploy --data {osd["device"]} --hostname {hostname}', - 1, - BoxType.SEED + seed ) deployed = 'created osd' in deployed.lower() or 'already created?' in deployed.lower() print('Waiting 5 seconds to re-run deploy osd...') diff --git a/src/cephadm/box/util.py b/src/cephadm/box/util.py index e2d802b21ed3..7dcf883f8a37 100644 --- a/src/cephadm/box/util.py +++ b/src/cephadm/box/util.py @@ -2,8 +2,10 @@ import json import os import subprocess import sys -import enum -from typing import Any, Callable, Dict +import copy +from abc import ABCMeta, abstractmethod +from enum import Enum +from typing import Any, Callable, Dict, List class Colors: HEADER = '\033[95m' @@ -92,7 +94,26 @@ def ensure_inside_container(func) -> bool: def colored(msg, color: Colors): return color + msg + Colors.ENDC -def run_shell_command(command: str, expect_error=False, verbose=True) -> str: +class BoxType(str, Enum): + SEED = 'seed' + HOST = 'host' + +class HostContainer: + def __init__(self, _name, _type) -> None: + self._name: str = _name + self._type: BoxType = _type + + @property + def name(self) -> str: + return self._name + + @property + def type(self) -> BoxType: + return self._type + def __str__(self) -> str: + return f'{self.name} {self.type}' + +def run_shell_command(command: str, expect_error=False, verbose=True, expect_exit_code=0) -> str: if Config.get('verbose'): print(f'{colored("Running command", Colors.HEADER)}: {colored(command, Colors.OKBLUE)}') @@ -119,43 +140,20 @@ def run_shell_command(command: str, expect_error=False, verbose=True) -> str: err += process.stderr.read().decode('latin1').strip() out = out.strip() - if process.returncode != 0 and not expect_error: + if process.returncode != 0 and not expect_error and process.returncode != expect_exit_code: err = colored(err, Colors.FAIL); - raise RuntimeError(f'Failed command: {command}\n{err}') + + raise RuntimeError(f'Failed command: {command}\n{err}\nexit code: {process.returncode}') sys.exit(1) return out -class BoxType(enum.IntEnum): - SEED = 0 # where we bootstrap cephadm - HOST = 1 - @staticmethod - def to_enum(value: str): - if value == 'seed': - return BoxType.SEED - elif value == 'host': - return BoxType.HOST - else: - print(f'Wrong container type {value}') - sys.exit(1) - - @staticmethod - def to_string(box_type): - if box_type == BoxType.SEED: - return 'seed' - elif box_type == BoxType.HOST: - return 'host' - else: - print(f'Wrong container type {type_}') - sys.exit(1) - - -def run_dc_shell_commands(index, box_type: BoxType, commands: str, expect_error=False) -> str: +def run_dc_shell_commands(commands: str, container: HostContainer, expect_error=False) -> str: for command in commands.split('\n'): command = command.strip() if not command: continue - run_dc_shell_command(command.strip(), index, box_type, expect_error=expect_error) + run_dc_shell_command(command.strip(), container, expect_error=expect_error) def run_shell_commands(commands: str, expect_error=False) -> str: for command in commands.split('\n'): @@ -179,20 +177,9 @@ def run_cephadm_shell_command(command: str, expect_error=False) -> str: def run_dc_shell_command( - command: str, index: int, box_type: BoxType, expect_error=False + command: str, container: HostContainer, expect_error=False ) -> str: - box_type_str = 'box_hosts' - if box_type == BoxType.SEED: - index = 0 - if engine() == 'docker': - box_type_str = 'seed' - index = 1 - - container_id = get_container_id(f'{box_type_str}_{index}') - print(container_id) - out = run_shell_command( - f'{engine()} exec -it {container_id} {command}', expect_error - ) + out = get_container_engine().run_exec(container, command, expect_error=expect_error) return out def inside_container() -> bool: @@ -246,8 +233,189 @@ def get_orch_hosts(): if inside_container(): orch_host_ls_out = run_cephadm_shell_command('ceph orch host ls --format json') else: - orch_host_ls_out = run_dc_shell_command(f'cephadm shell --keyring /etc/ceph/ceph.keyring --config /etc/ceph/ceph.conf -- ceph orch host ls --format json', 1, BoxType.SEED) + orch_host_ls_out = run_dc_shell_command(f'cephadm shell --keyring /etc/ceph/ceph.keyring --config /etc/ceph/ceph.conf -- ceph orch host ls --format json', + get_container_engine().get_seed()) sp = orch_host_ls_out.split('\n') orch_host_ls_out = sp[len(sp) - 1] hosts = json.loads(orch_host_ls_out) return hosts + + +class ContainerEngine(metaclass=ABCMeta): + @property + @abstractmethod + def command(self) -> str: pass + + @property + @abstractmethod + def seed_name(self) -> str: pass + + @property + @abstractmethod + def dockerfile(self) -> str: pass + + @property + def host_name_prefix(self) -> str: + return 'box_hosts_' + + @abstractmethod + def up(self, hosts: int): pass + + def run_exec(self, container: HostContainer, command: str, expect_error: bool = False): + return run_shell_command(' '.join([self.command, 'exec', container.name, command]), + expect_error=expect_error) + + def run(self, engine_command: str, expect_error: bool = False): + return run_shell_command(' '.join([self.command, engine_command]), expect_error=expect_error) + + def get_containers(self) -> List[HostContainer]: + ps_out = json.loads(run_shell_command('podman ps --format json')) + containers = [] + for container in ps_out: + if not container['Names']: + raise RuntimeError(f'Container {container} missing name') + name = container['Names'][0] + if name == self.seed_name: + containers.append(HostContainer(name, BoxType.SEED)) + elif name.startswith(self.host_name_prefix): + containers.append(HostContainer(name, BoxType.HOST)) + return containers + + def get_seed(self) -> HostContainer: + for container in self.get_containers(): + if container.type == BoxType.SEED: + return container + raise RuntimeError('Missing seed container') + + def get_container(self, container_name: str): + containers = self.get_containers() + for container in containers: + if container.name == container_name: + return container + return None + + + def restart(self): + pass + + +class DockerEngine(ContainerEngine): + command = 'docker' + seed_name = 'seed' + dockerfile = 'DockerfileDocker' + + def restart(self): + run_shell_command('systemctl restart docker') + + def up(self, hosts: int): + dcflags = f'-f {Config.get("docker_yaml")}' + if not os.path.exists('/sys/fs/cgroup/cgroup.controllers'): + dcflags += f' -f {Config.get("docker_v1_yaml")}' + run_shell_command(f'{engine_compose()} {dcflags} up --scale hosts={hosts} -d') + +class PodmanEngine(ContainerEngine): + command = 'podman' + seed_name = 'box_hosts_0' + dockerfile = 'DockerfilePodman' + + CAPS = [ + "SYS_ADMIN", + "NET_ADMIN", + "SYS_TIME", + "SYS_RAWIO", + "MKNOD", + "NET_RAW", + "SETUID", + "SETGID", + "CHOWN", + "SYS_PTRACE", + "SYS_TTY_CONFIG", + "CAP_AUDIT_WRITE", + "CAP_AUDIT_CONTROL", + ] + + VOLUMES = [ + '../../../:/ceph:z', + '../:/cephadm:z', + '/run/udev:/run/udev', + '/sys/dev/block:/sys/dev/block', + '/sys/fs/cgroup:/sys/fs/cgroup:ro', + '/dev/fuse:/dev/fuse', + '/dev/disk:/dev/disk', + '/sys/devices/virtual/block:/sys/devices/virtual/block', + '/sys/block:/dev/block', + '/dev/mapper:/dev/mapper', + '/dev/mapper/control:/dev/mapper/control', + ] + + TMPFS = ['/run', '/tmp'] + + # FIXME: right now we are assuming every service will be exposed through the seed, but this is far + # from the truth. Services can be deployed on different hosts so we need a system to manage this. + SEED_PORTS = [ + 8443, # dashboard + 3000, # grafana + 9093, # alertmanager + 9095 # prometheus + ] + + + def setup_podman_env(self, hosts: int = 1, osd_devs={}): + network_name = 'box_network' + networks = run_shell_command('podman network ls') + if network_name not in networks: + run_shell_command(f'podman network create -d bridge {network_name}') + + args = [ + '--group-add', 'keep-groups', + '--device', '/dev/fuse' , + '-it' , + '-d', + '-e', 'CEPH_BRANCH=main', + '--stop-signal', 'RTMIN+3' + ] + + for cap in self.CAPS: + args.append('--cap-add') + args.append(cap) + + for volume in self.VOLUMES: + args.append('-v') + args.append(volume) + + for tmp in self.TMPFS: + args.append('--tmpfs') + args.append(tmp) + + + for osd_dev in osd_devs.values(): + device = osd_dev["device"] + args.append('--device') + args.append(f'{device}:{device}') + + + for host in range(hosts+1): # 0 will be the seed + options = copy.copy(args) + options.append('--name') + options.append(f'box_hosts_{host}') + options.append('--network') + options.append(f'{network_name}') + if host == 0: + for port in self.SEED_PORTS: + options.append('-p') + options.append(f'{port}:{port}') + + options.append('cephadm-box') + options = ' '.join(options) + + run_shell_command(f'podman run {options}') + + def up(self, hosts: int): + import osd + self.setup_podman_env(hosts=hosts, osd_devs=osd.load_osd_devices()) + +def get_container_engine() -> ContainerEngine: + if engine() == 'docker': + return DockerEngine() + else: + return PodmanEngine()