"""PyTest Ansible Plugin.""" import pytest import ansible import ansible.constants import ansible.utils import ansible.errors try: from ansible.plugins.loader import become_loader except ImportError: become_loader = None from pytest_ansible.logger import get_logger from pytest_ansible.fixtures import (ansible_adhoc, ansible_module, ansible_facts, localhost) from pytest_ansible.host_manager import get_host_manager log = get_logger(__name__) # Silence linters for imported fixtures (ansible_adhoc, ansible_module, ansible_facts, localhost) def become_methods(): """Return string list of become methods available to ansible.""" if become_loader: return [method.name for method in become_loader.all()] else: return ansible.constants.BECOME_METHODS def pytest_addoption(parser): """Add options to control ansible.""" log.debug("pytest_addoption() called") group = parser.getgroup('pytest-ansible') group.addoption('--inventory', '--ansible-inventory', action='store', dest='ansible_inventory', default=ansible.constants.DEFAULT_HOST_LIST, metavar='ANSIBLE_INVENTORY', help='ansible inventory file URI (default: %(default)s)') group.addoption('--host-pattern', '--ansible-host-pattern', action='store', dest='ansible_host_pattern', default=None, metavar='ANSIBLE_HOST_PATTERN', help='ansible host pattern (default: %(default)s)') group.addoption('--limit', '--ansible-limit', action='store', dest='ansible_subset', default=ansible.constants.DEFAULT_SUBSET, metavar='ANSIBLE_SUBSET', help='further limit selected hosts to an additional pattern') group.addoption('--connection', '--ansible-connection', action='store', dest='ansible_connection', default=ansible.constants.DEFAULT_TRANSPORT, help="connection type to use (default: %(default)s)") group.addoption('--user', '--ansible-user', action='store', dest='ansible_user', default=ansible.constants.DEFAULT_REMOTE_USER, help='connect as this user (default: %(default)s)') group.addoption('--check', '--ansible-check', action='store_true', dest='ansible_check', default=False, help='don\'t make any changes; instead, try to predict some of the changes that may occur') group.addoption('--module-path', '--ansible-module-path', action='store', dest='ansible_module_path', default=ansible.constants.DEFAULT_MODULE_PATH, help='specify path(s) to module library (default: %(default)s)') # become privilege escalation group.addoption('--become', '--ansible-become', action='store_true', dest='ansible_become', default=ansible.constants.DEFAULT_BECOME, help='run operations with become, nopasswd implied (default: %(default)s)') group.addoption('--become-method', '--ansible-become-method', action='store', dest='ansible_become_method', default=ansible.constants.DEFAULT_BECOME_METHOD, help="privilege escalation method to use (default: %%(default)s), valid choices: [ %s ]" % (' | '.join(become_methods()))) group.addoption('--become-user', '--ansible-become-user', action='store', dest='ansible_become_user', default=ansible.constants.DEFAULT_BECOME_USER, help='run operations as this user (default: %(default)s)') group.addoption('--ask-become-pass', '--ansible-ask-become-pass', action='store', dest='ansible_ask_become_pass', default=ansible.constants.DEFAULT_BECOME_ASK_PASS, help='ask for privilege escalation password (default: %(default)s)') # Add github marker to --help parser.addini("ansible", "Ansible integration", "args") def pytest_configure(config): """Validate --ansible-* parameters.""" log.debug("pytest_configure() called") config.addinivalue_line("markers", "ansible(**kwargs): Ansible integration") # Enable connection debugging if config.option.verbose > 0: if hasattr(ansible.utils, 'VERBOSITY'): ansible.utils.VERBOSITY = int(config.option.verbose) else: from ansible.utils.display import Display display = Display() display.verbosity = int(config.option.verbose) assert config.pluginmanager.register(PyTestAnsiblePlugin(config), "ansible") def pytest_generate_tests(metafunc): """Generate tests when specific `ansible_*` fixtures are used by tests.""" log.debug("pytest_generate_tests() called") if 'ansible_host' in metafunc.fixturenames: # assert required --ansible-* parameters were used PyTestAnsiblePlugin.assert_required_ansible_parameters(metafunc.config) try: plugin = metafunc.config.pluginmanager.getplugin("ansible") hosts = plugin.initialize(config=plugin.config, pattern=metafunc.config.getoption('ansible_host_pattern')) except ansible.errors.AnsibleError as e: raise pytest.UsageError(e) # Return the host name as a string # metafunc.parametrize("ansible_host", hosts.keys()) # Return a HostManager instance where pattern=host (e.g. ansible_host.all.shell('date')) # metafunc.parametrize("ansible_host", iter(plugin.initialize(config=plugin.config, pattern=h) for h in # hosts.keys())) # Return a ModuleDispatcher instance representing `host` (e.g. ansible_host.shell('date')) metafunc.parametrize("ansible_host", iter(hosts[h] for h in hosts.keys())) if 'ansible_group' in metafunc.fixturenames: # assert required --ansible-* parameters were used PyTestAnsiblePlugin.assert_required_ansible_parameters(metafunc.config) try: plugin = metafunc.config.pluginmanager.getplugin("ansible") hosts = plugin.initialize(config=plugin.config, pattern=metafunc.config.getoption('ansible_host_pattern')) except ansible.errors.AnsibleError as e: raise pytest.UsageError(e) # FIXME: Eeew, this shouldn't be interfacing with `hosts.options` groups = hosts.options['inventory_manager'].list_groups() # Return the group name as a string # metafunc.parametrize("ansible_group", groups) # Return a ModuleDispatcher instance representing the group (e.g. ansible_group.shell('date')) metafunc.parametrize("ansible_group", iter(hosts[g] for g in groups)) class PyTestAnsiblePlugin: """Ansible PyTest Plugin Class.""" def __init__(self, config): """Initialize plugin.""" log.debug("PyTestAnsiblePlugin initialized") self.config = config def pytest_report_header(self, config, startdir): """Return the version of ansible.""" log.debug("pytest_report_header() called") return 'ansible: %s' % ansible.__version__ def pytest_collection_modifyitems(self, session, config, items): """Validate --ansible-* parameters.""" log.debug("pytest_collection_modifyitems() called") log.debug("items: %s" % items) uses_ansible_fixtures = False for item in items: if not hasattr(item, 'fixturenames'): continue if any([fixture.startswith('ansible_') for fixture in item.fixturenames]): # TODO - ignore if they are using a marker # marker = item.get_marker('ansible') # if marker and 'inventory' in marker.kwargs: uses_ansible_fixtures = True break if uses_ansible_fixtures: # assert required --ansible-* parameters were used self.assert_required_ansible_parameters(config) def _load_ansible_config(self, config): """Load ansible configuration from command-line.""" option_names = ['ansible_inventory', 'ansible_host_pattern', 'ansible_connection', 'ansible_user', 'ansible_module_path', 'ansible_become', 'ansible_become_method', 'ansible_become_user', 'ansible_ask_become_pass', 'ansible_subset'] kwargs = dict() # Load command-line supplied values for key in option_names: short_key = key[8:] kwargs[short_key] = config.getoption(key) # normalize ansible.ansible_become options kwargs['become'] = kwargs['become'] or ansible.constants.DEFAULT_BECOME kwargs['become_user'] = kwargs['become_user'] or ansible.constants.DEFAULT_BECOME_USER kwargs['ask_become_pass'] = kwargs['ask_become_pass'] or ansible.constants.DEFAULT_BECOME_ASK_PASS log.debug("config: %s" % kwargs) return kwargs def _load_request_config(self, request): """Load ansible configuration from decorator kwargs.""" kwargs = dict() # Override options from @pytest.mark.ansible marker = request.node.get_closest_marker('ansible') if marker: kwargs = marker.kwargs log.debug("request: %s" % kwargs) return kwargs def initialize(self, config=None, request=None, **kwargs): """Return an initialized Ansible Host Manager instance.""" ansible_cfg = dict() # merge command-line configuration options if config is not None: ansible_cfg.update(self._load_ansible_config(config)) # merge pytest request configuration options if request is not None: ansible_cfg.update(self._load_request_config(request)) # merge in provided kwargs ansible_cfg.update(kwargs) return get_host_manager(**ansible_cfg) @staticmethod def assert_required_ansible_parameters(config): """Assert whether the required --ansible-* parameters were provided.""" errors = [] # Verify --ansible-host-pattern was provided ansible_hostname = config.getoption('ansible_host_pattern') if ansible_hostname is None or ansible_hostname == '': errors.append("Missing required parameter --ansible-host-pattern/--host-pattern") # NOTE: I don't think this will ever catch issues since ansible_inventory # defaults to '/etc/ansible/hosts' # Verify --ansible-inventory was provided ansible_inventory = config.getoption('ansible_inventory') if ansible_inventory is None or ansible_inventory == "": errors.append("Unable to find an inventory file, specify one with the --ansible-inventory/--inventory " "parameter.") if errors: raise pytest.UsageError(*errors)