Multihost Roles

Objects that inherits from MultihostRole are directly accessible in the test. These objects are short-lived, new instance is created for each test, therefore it is possible to store test related data. The main purpose of this class is to provide role setup and teardown as well as a place to implement high-level API for testing your project.

See also

See Multihost Utilities to see how it is possible to share code between multiple role classes (and host classes).

As a first example, we implement a basic code for a client role. This role includes several built-in utilities to automatically get access to functionality we want to use in our tests.

Example: Trivial client role
 1from pytest_mh import MultihostRole
 2from pytest_mh.utils.firewall import Firewalld
 3from pytest_mh.utils.fs import LinuxFileSystem
 4from pytest_mh.utils.journald import JournaldUtils
 5from pytest_mh.utils.services import SystemdServices
 6from pytest_mh.utils.tc import LinuxTrafficControl
 7
 8class ClientRole(MultihostRole[ClientHost]):
 9    def __init__(self, *args, **kwargs) -> None:
10        super().__init__(*args, **kwargs)
11
12        self.fs: LinuxFileSystem = LinuxFileSystem(self.host)
13        """
14        File system manipulation.
15        """
16
17        self.svc: SystemdServices = SystemdServices(self.host)
18        """
19        Systemd service management.
20        """
21
22        self.firewall: Firewalld = Firewalld(self.host).postpone_setup()
23        """
24        Configure firewall using firewalld.
25        """
26
27        self.tc: LinuxTrafficControl = LinuxTrafficControl(self.host).postpone_setup()
28        """
29        Traffic control manipulation.
30        """
31
32        self.journald: JournaldUtils = JournaldUtils(self.host)
33        """
34        Journald utilities.
35        """
36
37    def setup(self) -> None:
38        """
39        Called before execution of each test.
40
41        * stop the client
42        * remove client's database and logs
43        """
44        super().setup()
45
46        self.svc.stop("my-project-client")
47        self.fs.rm("/var/lib/my-project-client")
48        self.fs.rm("/var/log/my-project-client")
49
50    def teardown(self) -> None:
51        """
52        Called after execution of each test.
53        """
54        # It is not required to restore removed files or restart
55        # the service. This is done automatically by the utilities.
56        super().teardown()

The following snippet add a high-level API to add a local user. It uses a built-in CLI builder, that can help you to prepare a command line for execution. Notice, that all local users that are created during a test are later removed during teardown.

Example: Method to add a local user
  1from typing import Self
  2
  3from pytest_mh import MultihostRole
  4from pytest_mh.cli import CLIBuilder, CLIBuilderArgs
  5from pytest_mh.conn import ProcessLogLevel
  6from pytest_mh.utils.firewall import Firewalld
  7from pytest_mh.utils.fs import LinuxFileSystem
  8from pytest_mh.utils.journald import JournaldUtils
  9from pytest_mh.utils.services import SystemdServices
 10from pytest_mh.utils.tc import LinuxTrafficControl
 11
 12
 13class ClientRole(MultihostRole[ClientHost]):
 14    def __init__(self, *args, **kwargs) -> None:
 15        super().__init__(*args, **kwargs)
 16
 17        self.fs: LinuxFileSystem = LinuxFileSystem(self.host)
 18        """
 19        File system manipulation.
 20        """
 21
 22        self.svc: SystemdServices = SystemdServices(self.host)
 23        """
 24        Systemd service management.
 25        """
 26
 27        self.firewall: Firewalld = Firewalld(self.host).postpone_setup()
 28        """
 29        Configure firewall using firewalld.
 30        """
 31
 32        self.tc: LinuxTrafficControl = LinuxTrafficControl(self.host).postpone_setup()
 33        """
 34        Traffic control manipulation.
 35        """
 36
 37        self.journald: JournaldUtils = JournaldUtils(self.host)
 38        """
 39        Journald utilities.
 40        """
 41
 42        self.cli: CLIBuilder = CLIBuilder(self.host.conn)
 43        """
 44        CLI builder helper.
 45        """
 46
 47        self._added_users: list[str] = []
 48        """
 49        List of local users that were created during the test.
 50        """
 51
 52    def setup(self) -> None:
 53        """
 54        Called before execution of each test.
 55
 56        * stop the client
 57        * remove client's database and logs
 58        """
 59        super().setup()
 60
 61        self.svc.stop("my-project-client")
 62        self.fs.rm("/var/lib/my-project-client")
 63        self.fs.rm("/var/log/my-project-client")
 64
 65    def teardown(self) -> None:
 66        """
 67        Called after execution of each test.
 68        """
 69        # It is not required to restore removed files or restart
 70        # the service. This is done automatically by the utilities.
 71
 72        # Delete users that we added
 73        if self._users:
 74            cmd = "\n".join([f"userdel '{x}' --force --remove" for x in self._users]) + "\n"
 75            self.host.conn.run("set -e\n\n" + cmd)
 76
 77        super().teardown()
 78
 79    def add_local_user(
 80        self,
 81        *,
 82        name: str,
 83        uid: int | None = None,
 84        gid: int | None = None,
 85        password: str | None = "Secret123",
 86        home: str | None = None,
 87        gecos: str | None = None,
 88        shell: str | None = None,
 89    ) -> Self:
 90        """
 91        Create new local user.
 92
 93        :param uid: User id, defaults to None
 94        :type uid: int | None, optional
 95        :param gid: Primary group id, defaults to None
 96        :type gid: int | None, optional
 97        :param password: Password, defaults to 'Secret123'
 98        :type password: str, optional
 99        :param home: Home directory, defaults to None
100        :type home: str | None, optional
101        :param gecos: GECOS, defaults to None
102        :type gecos: str | None, optional
103        :param shell: Login shell, defaults to None
104        :type shell: str | None, optional
105        :return: Self.
106        :rtype: Self
107        """
108        if home is not None:
109            self.fs.backup(home)
110
111        args: CLIBuilderArgs = {
112            "name": (self.cli.option.POSITIONAL, name),
113            "uid": (self.cli.option.VALUE, uid),
114            "gid": (self.cli.option.VALUE, gid),
115            "home": (self.cli.option.VALUE, home),
116            "gecos": (self.cli.option.VALUE, gecos),
117            "shell": (self.cli.option.VALUE, shell),
118        }
119
120        passwd = f" && passwd --stdin '{name}'" if password else ""
121        self.logger.info(f'Creating local user "{name}" on {self.host.hostname}')
122        self.host.conn.run(self.cli.command("useradd", args) + passwd, input=password, log_level=ProcessLogLevel.Error)
123
124        self._users.append(name)
125
126        return self

See also

The examples above are very trivial in order to show the idea. To see a feature-rich roles that are actively used to test a real life project, checkout the sssd-test-framework roles. These roles provide extensive, high-level API to manage users, group and other objects in LDAP, IPA, SambaDC and Active Directory as well as tools to manage and test SSSD.