diff --git a/tests/common_setup.py b/tests/common_setup.py index fff4e2159..d30b514f0 100644 --- a/tests/common_setup.py +++ b/tests/common_setup.py @@ -44,6 +44,56 @@ def standard_setup_one_client(request): return env +@pytest.fixture(scope="function") +def standard_setup_one_docker_client(request): + env = container_factory.get_docker_client_setup() + request.addfinalizer(env.teardown) + + env.setup() + + env.device = MenderDevice(env.get_mender_clients()[0]) + env.device.ssh_is_opened() + + reset_mender_api(env) + + env.auth = auth + return env + + +@pytest.fixture(scope="function") +def standard_setup_two_docker_clients_bootstrapped(request): + env = container_factory.get_docker_client_setup(num_clients=2) + request.addfinalizer(env.teardown) + + env.setup() + + env.device_group = MenderDeviceGroup(env.get_mender_clients()) + env.device_group.ssh_is_opened() + + reset_mender_api(env) + devauth.accept_devices(2) + + env.auth = auth + return env + + +@pytest.fixture(scope="function") +def qemu_setup_one_client(request): + """Explicit QEMU client setup for tests that require A/B rootfs updates.""" + env = container_factory.get_qemu_client_setup(num_clients=1) + env.setup() + + env.device = MenderDevice(env.get_mender_clients()[0]) + env.device.ssh_is_opened() + + reset_mender_api(env) + + request.addfinalizer(env.teardown) + + env.auth = auth + return env + + @pytest.fixture(scope="function") def standard_setup_extended(request): env = container_factory.get_extended_setup(num_clients=1) @@ -376,6 +426,42 @@ def enterprise_one_docker_client_bootstrapped(request): return env +@pytest.fixture(scope="function") +def enterprise_one_docker_client(request): + env = container_factory.get_enterprise_docker_client_setup(num_clients=0) + request.addfinalizer(env.teardown) + + env.setup() + reset_mender_api(env) + + tenant = create_tenant(env) + new_tenant_client(env, "mender-client", tenant["tenant_token"], docker=True) + env.device_group.ssh_is_opened() + + return env + + +@pytest.fixture(scope="function") +def enterprise_two_docker_clients_bootstrapped(request): + env = container_factory.get_enterprise_docker_client_setup(num_clients=0) + request.addfinalizer(env.teardown) + + env.setup() + reset_mender_api(env) + + tenant = create_tenant(env) + new_tenant_client(env, "mender-client-1", tenant["tenant_token"], docker=True) + new_tenant_client(env, "mender-client-2", tenant["tenant_token"], docker=True) + env.device_group.ssh_is_opened() + + devauth_tenant = DeviceAuthV2(env.auth) + devauth_tenant.accept_devices(2) + devices = devauth_tenant.get_devices_status("accepted", expected_devices=2) + assert 2 == len(devices) + + return env + + @pytest.fixture(scope="function") def enterprise_one_rofs_client_bootstrapped(request): env = container_factory.get_enterprise_rofs_client_setup(num_clients=0) diff --git a/tests/helpers.py b/tests/helpers.py index 6646da360..57be47458 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -61,6 +61,33 @@ def ip_to_device_id_map(device_group, devauth=devauth): @staticmethod def _check_log_for_message(device, message, since=None): + # Docker client has no systemd — check auth state via D-Bus instead. + has_systemd = ( + device.run("which systemctl 2>/dev/null", warn=True, hide=True) != "" + ) + if not has_systemd: + if "Successfully received new authorization data" in message: + sleepsec = 0 + timeout = 600 + while sleepsec < timeout: + out = device.run( + "dbus-send --system --print-reply " + "--dest=io.mender.AuthenticationManager " + "/io/mender/AuthenticationManager " + "io.mender.Authentication1.GetJwtToken 2>/dev/null", + warn=True, + hide=True, + ) + if out and "string" in out: + return + time.sleep(10) + sleepsec += 10 + logger.info( + f"waiting for device to authenticate via D-Bus, waited {sleepsec}s" + ) + assert False, "timeout waiting for device to authenticate via D-Bus" + return + if since: cmd = f"journalctl --unit mender-authd --full --since '{since}'" else: diff --git a/tests/tests/test_bootstrapping.py b/tests/tests/test_bootstrapping.py index c47527195..b60243bd4 100644 --- a/tests/tests/test_bootstrapping.py +++ b/tests/tests/test_bootstrapping.py @@ -19,8 +19,10 @@ from ..common_setup import ( standard_setup_one_client, standard_setup_one_client_bootstrapped, + standard_setup_one_docker_client, enterprise_one_client, enterprise_one_client_bootstrapped, + enterprise_one_docker_client, ) from .common_update import common_update_procedure from ..MenderAPI import DeviceAuthV2, Deployments, logger @@ -102,8 +104,8 @@ def do_test_reject_bootstrap(self, env, valid_image): class TestBootstrappingOpenSource(BaseTestBootstrapping): @MenderTesting.fast - def test_bootstrap(self, standard_setup_one_client): - self.do_test_bootstrap(standard_setup_one_client) + def test_bootstrap(self, standard_setup_one_docker_client): + self.do_test_bootstrap(standard_setup_one_docker_client) @MenderTesting.slow def test_reject_bootstrap( @@ -116,8 +118,8 @@ def test_reject_bootstrap( class TestBootstrappingEnterprise(BaseTestBootstrapping): @MenderTesting.fast - def test_bootstrap(self, enterprise_one_client): - self.do_test_bootstrap(enterprise_one_client) + def test_bootstrap(self, enterprise_one_docker_client): + self.do_test_bootstrap(enterprise_one_docker_client) @MenderTesting.slow def test_reject_bootstrap(self, enterprise_one_client_bootstrapped, valid_image): diff --git a/tests/tests/test_grouping.py b/tests/tests/test_grouping.py index 5ddc80df2..8b1e307dc 100644 --- a/tests/tests/test_grouping.py +++ b/tests/tests/test_grouping.py @@ -14,7 +14,9 @@ from ..common_setup import ( standard_setup_two_clients_bootstrapped, + standard_setup_two_docker_clients_bootstrapped, enterprise_two_clients_bootstrapped, + enterprise_two_docker_clients_bootstrapped, ) from .common_update import common_update_procedure from ..MenderAPI import DeviceAuthV2, Deployments, Inventory, image, logger @@ -171,8 +173,8 @@ def do_test_update_device_group(self, env, valid_image_with_mender_conf): @MenderTesting.fast class TestGroupingOpenSource(BaseTestGrouping): - def test_basic_groups(self, standard_setup_two_clients_bootstrapped): - self.do_test_basic_groups(standard_setup_two_clients_bootstrapped) + def test_basic_groups(self, standard_setup_two_docker_clients_bootstrapped): + self.do_test_basic_groups(standard_setup_two_docker_clients_bootstrapped) def test_update_device_group( self, standard_setup_two_clients_bootstrapped, valid_image_with_mender_conf @@ -185,8 +187,8 @@ def test_update_device_group( @MenderTesting.fast class TestGroupingEnterprise(BaseTestGrouping): - def test_basic_groups(self, enterprise_two_clients_bootstrapped): - self.do_test_basic_groups(enterprise_two_clients_bootstrapped) + def test_basic_groups(self, enterprise_two_docker_clients_bootstrapped): + self.do_test_basic_groups(enterprise_two_docker_clients_bootstrapped) def test_update_device_group( self, enterprise_two_clients_bootstrapped, valid_image_with_mender_conf diff --git a/tests/tests/test_preauth.py b/tests/tests/test_preauth.py index 5c2813116..33f9b43f4 100644 --- a/tests/tests/test_preauth.py +++ b/tests/tests/test_preauth.py @@ -18,7 +18,7 @@ from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization -from ..common_setup import standard_setup_one_client, enterprise_no_client +from ..common_setup import standard_setup_one_docker_client, enterprise_no_client from .mendertesting import MenderTesting from ..MenderAPI import auth, devauth, inv, logger from ..helpers import Helpers @@ -154,13 +154,13 @@ def do_test_fail_preauth_existing(self): class TestPreauth(TestPreauthBase): - def test_ok_preauth_and_bootstrap(self, standard_setup_one_client): - self.do_test_ok_preauth_and_bootstrap(standard_setup_one_client) + def test_ok_preauth_and_bootstrap(self, standard_setup_one_docker_client): + self.do_test_ok_preauth_and_bootstrap(standard_setup_one_docker_client) - def test_ok_preauth_and_remove(self, standard_setup_one_client): + def test_ok_preauth_and_remove(self, standard_setup_one_docker_client): self.do_test_ok_preauth_and_remove() - def test_fail_preauth_existing(self, standard_setup_one_client): + def test_fail_preauth_existing(self, standard_setup_one_docker_client): self.do_test_fail_preauth_existing() @@ -196,7 +196,11 @@ class Client: """Wraps various actions on the client, performed via SSH (inside fabric.execute()).""" ID_HELPER = "/usr/share/mender/identity/mender-device-identity" - PRIV_KEY = "/data/mender/mender-agent.pem" + # QEMU (Yocto) stores the key under /data/mender; Docker client uses /var/lib/mender. + _PRIV_KEY_CANDIDATES = [ + "/data/mender/mender-agent.pem", + "/var/lib/mender/mender-agent.pem", + ] KEYGEN_TIMEOUT = 300 @@ -209,8 +213,8 @@ def get_logs(device): def get_pub_key(device): """Extract the device's public key from its private key.""" - Client.__wait_for_keygen(device) - keystr = device.run("cat {}".format(Client.PRIV_KEY)) + priv_key_path = Client.__wait_for_keygen(device) + keystr = device.run("cat {}".format(priv_key_path)) private_key = serialization.load_pem_private_key( data=keystr.encode() if isinstance(keystr, str) else keystr, password=None, @@ -234,16 +238,19 @@ def substitute_id_data(device, id_data_dict): @staticmethod def __wait_for_keygen(device): + """Wait for the mender private key to appear and return its path.""" sleepsec = 0 while sleepsec < Client.KEYGEN_TIMEOUT: - try: - device.run("stat {}".format(Client.PRIV_KEY)) - except: - time.sleep(10) - sleepsec += 10 - logger.info("waiting for key gen, sleepsec: {}".format(sleepsec)) - else: - time.sleep(5) - break + for path in Client._PRIV_KEY_CANDIDATES: + try: + device.run(f"stat {path}") + time.sleep(5) + return path + except Exception: + pass + time.sleep(10) + sleepsec += 10 + logger.info("waiting for key gen, sleepsec: {}".format(sleepsec)) assert sleepsec <= Client.KEYGEN_TIMEOUT, "timeout for key generation exceeded" + return Client._PRIV_KEY_CANDIDATES[0] diff --git a/testutils/infra/container_manager/docker_compose_manager.py b/testutils/infra/container_manager/docker_compose_manager.py index 7830c4d65..b613d5bdc 100644 --- a/testutils/infra/container_manager/docker_compose_manager.py +++ b/testutils/infra/container_manager/docker_compose_manager.py @@ -132,6 +132,15 @@ def setup(self): self._docker_compose_up(f"--scale mender-client={self.num_clients}") +class DockerComposeQemuClientSetup(DockerComposeNamespace): + def __init__(self, name, num_clients=1): + self.num_clients = num_clients + super().__init__(name, self.QEMU_CLIENT_FILES) + + def setup(self): + self._docker_compose_up(f"--scale mender-client={self.num_clients}") + + class DockerComposeExtendedSetup(DockerComposeNamespace): def __init__(self, name, num_clients=1): self.num_clients = num_clients @@ -184,12 +193,13 @@ def new_tenant_docker_client(self, name, tenant): class DockerComposeDockerClientSetup(DockerComposeNamespace): - def __init__( - self, - name, - ): + def __init__(self, name, num_clients=1): + self.num_clients = num_clients DockerComposeNamespace.__init__(self, name, self.DOCKER_CLIENT_FILES) + def setup(self): + self._docker_compose_up(f"--scale mender-client={self.num_clients}") + class DockerComposeRofsClientSetup(DockerComposeNamespace): def __init__( @@ -393,6 +403,7 @@ def __init__(self, name, num_clients=0): class DockerComposeEnterpriseDockerClientSetup(DockerComposeEnterpriseSetup): def __init__(self, name, num_clients=0): self.num_clients = num_clients + self._docker_client_count = 0 if self.num_clients > 0: raise NotImplementedError( "Clients not implemented on setup time, use new_tenant_client" @@ -406,9 +417,10 @@ def setup(self): self._docker_compose_up("--scale mender-client=0") def new_tenant_docker_client(self, name, tenant): + self._docker_client_count += 1 logger.info("creating docker client connected to tenant: " + tenant) self._docker_compose_up( - "--scale mender-client=1", + f"--scale mender-client={self._docker_client_count}", {"TENANT_TOKEN": "%s" % tenant}, ) diff --git a/testutils/infra/container_manager/factory.py b/testutils/infra/container_manager/factory.py index b4a793cb8..b10521706 100644 --- a/testutils/infra/container_manager/factory.py +++ b/testutils/infra/container_manager/factory.py @@ -15,6 +15,7 @@ from .docker_compose_manager import ( DockerComposeStandardSetup, + DockerComposeQemuClientSetup, DockerComposeExtendedSetup, DockerComposeMonitorCommercialSetup, DockerComposeDockerClientSetup, @@ -41,7 +42,7 @@ class ContainerManagerFactory: def get_standard_setup(self, name=None, num_clients=1): """Standard setup consisting on all core backend services and optionally clients - The num_clients define how many QEMU Mender clients will be spawn. + The num_clients define how many Mender clients will be spawned. """ pass @@ -52,12 +53,12 @@ def get_monitor_commercial_setup(self, name=None, num_clients=1): """ pass - def get_docker_client_setup(self, name=None): - """Standard setup with one Docker client instead of QEMU one""" + def get_docker_client_setup(self, name=None, num_clients=1): + """Standard setup with Docker client(s) instead of QEMU""" pass def get_rofs_client_setup(self, name=None): - """Standard setup with one QEMU Read-Only FS client instead of standard R/W""" + """Standard setup with one QEMU Read-Only FS client (QEMU required for ROFS behavior)""" pass def get_legacy_v1_client_setup(self, name=None): @@ -109,13 +110,17 @@ def get_enterprise_docker_client_setup(self, name=None, num_clients=0): pass def get_enterprise_rofs_client_setup(self, name=None, num_clients=0): - """Enterprise setup with one Mender QEMU Read-Only FS client""" + """Enterprise setup with one QEMU Read-Only FS client (QEMU required for ROFS behavior)""" pass def get_enterprise_rofs_commercial_client_setup(self, name=None, num_clients=0): """Enterprise setup with one Mender QEMU Read-Only FS commercial client""" pass + def get_qemu_client_setup(self, name=None, num_clients=1): + """Explicit QEMU setup for tests requiring A/B rootfs partition updates (QEMU required)""" + pass + def get_enterprise_smtp_setup(self, name=None): """Enterprise setup with SMTP enabled""" pass @@ -133,14 +138,17 @@ class DockerComposeManagerFactory(ContainerManagerFactory): def get_standard_setup(self, name=None, num_clients=1): return DockerComposeStandardSetup(name, num_clients) + def get_qemu_client_setup(self, name=None, num_clients=1): + return DockerComposeQemuClientSetup(name, num_clients) + def get_extended_setup(self, name=None, num_clients=1): return DockerComposeExtendedSetup(name, num_clients) def get_monitor_commercial_setup(self, name=None, num_clients=0): return DockerComposeMonitorCommercialSetup(name, num_clients) - def get_docker_client_setup(self, name=None): - return DockerComposeDockerClientSetup(name) + def get_docker_client_setup(self, name=None, num_clients=1): + return DockerComposeDockerClientSetup(name, num_clients) def get_rofs_client_setup(self, name=None): return DockerComposeRofsClientSetup(name)