A pytest plugin for multi-host testing.


Release tarballs will be made available for download from Pagure Releases:

The goal is to include this project in Fedora repositories. Until that happens, you can use testing builds from COPR – see "Developer links" below.

You can also install using pip:


This plugin takes a description of your infrastructure, and provides, via a fixture, Host objects that commands can be called on.

It is intended as a general base for a framework; any project using it will need to extend it for its own needs.

The object provided to tests is a Config object, which has (among others) these attributes:

test_dir – directory to store test-specific data in,
           defaults to /root/multihost_tests
ipv6 – true if connecting via IPv6

domains – the list of domains

Hosts to run on are arranged in domains, which have:

name – the DNS name of the domain
type – a string specifying the type of the domain ('default' by default)

config – the Config this domain is part of
hosts – list of hosts in this domain

And the hosts have:

role – type of this host; should encode the OS and installed packages
hostname – fully qualified hostname, usually reachable from other hosts
shortname – first component of hostname
external_hostname – hostname used to connect to this host
ip – IP address

domain – the Domain this host is part of

transport – allows operations like uploading and downloading files
run_command() – runs the given command on the host

For each object – Config, Domain, Host – one can provide subclasses to modify the behavior (for example, FreeIPA would add Host methods to run a LDAP query or to install an IPA server). Each object has from_dict and to_dict methods, which can add additional attributes – for example, Config.ntp_server.

To use the multihost plugin in tests, create a fixture listing the domains and what number of which host role is needed:

import pytest
from pytest_multihost import make_multihost_fixture

def multihost(request):
    mh = make_multihost_fixture(
                'type': 'ipa',
                'hosts': {
                    'master': 1,
                    'replica': 2,
    return mh

If not enough hosts are available, all tests that use the fixture are skipped.

The object returned from make_multihost_fixture only has the "config" attribute. Users are expected to add convenience attributes. For example, FreeIPA, which typically uses a single domain with one master, several replicas and some clients, would do:

from pytest_multihost import make_multihost_fixture

def multihost(request):
    mh = make_multihost_fixture(request, descriptions=[
                'type': 'ipa',
                'hosts': {
                    'master': 1,
                    'replica': 1,
                    'client': 1,

    # Set convenience attributes
    mh.domain =[0]
    [mh.master] = mh.domain.hosts_by_role('master')
    mh.replicas = mh.domain.hosts_by_role('replica')
    mh.clients = mh.domain.hosts_by_role('client')

    # IPA-specific initialization/teardown of the hosts
    request.addfinalizer(lambda: request.cls().uninstall(mh))

    # Return the fixture
    return mh

As with any pytest fixture, this can be used by getting it as a function argument. For a simplified example, FreeIPA usage could look something like this:

class TestMultihost(object):
    def install(self, multihost):

    def uninstall(self, multihost):
        multihost.master.run_command(['ipa-server-install', '--uninstall'])

    def test_installed(self, multihost):
        multihost.master.run_command(['ipa', 'ping'])

The description of infrastructure is provided in a JSON or YAML file, which is named on the py.test command line. For example:

ssh_key_filename: ~/.ssh/id_rsa
  - name: adomain.test
    type: test-a
      - name: master
        role: master
      - name: replica1
        role: replica
      - name: replica2
        role: replica
        external_hostname: r2.adomain.test
      - name: client1
        role: client
      - name: extra
        role: extrarole
  - name: bdomain.test
    type: test-b
      - name: master.bdomain.test
        role: master

$ py.test --multihost-config=/path/to/configfile.yaml

To use YAML files, the PyYAML package is required. Without it only JSON files can be used.

Encoding and bytes/text

When writing files or issuing commands, bytestrings are passed through unchanged, and text strings (unicode in Python 2) are encoded using a configurable encoding (utf-8 by default).

When reading files, bytestrings are returned by default, but an encoding can be given to get a test string.

For command output, separate stdout_bytes and stdout_text attributes are provided. The latter uses a configurable encoding (utf-8 by default).

Note about shell logout behavior

When using Paramiko for the SSH Transport, a shell logout script (e.g. .bash_logout) will be executed at the end of a run_command. If this script fails in any way, it can cause the run_command to raise an error. This can occur even if the executed command succeeds.

The OpenSSH Transport exits without executing a shell logout script. As such, if there are issues with errors due to Paramiko running the logout script, you can use OpenSSH as a workaround.

In code:

import os
os.environ['PYTESTMULTIHOST_SSH_TRANSPORT'] = 'openssh'

In shell before running pytest:



The project is happy to accept patches! Please file any patches as Pull Requests on the project's Pagure repo. Any development discussion should be in Pagure Pull Requests and Issues.