Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support ansible v28+ #30

Merged
merged 8 commits into from
Feb 14, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ python:
- "2.7"
- "3.6"
env:
- TOXENV=ansible_v24
- TOXENV=ansible_v25
- TOXENV=ansible_v26
- TOXENV=ansible_v27
- TOXENV=ansible_devel
install:
- pip install -r test-requirements.txt
Expand Down
8 changes: 8 additions & 0 deletions pytest_ansible/has_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import ansible
from pkg_resources import parse_version

has_ansible_v1 = parse_version(ansible.__version__) < parse_version('2.0.0')
has_ansible_v2 = parse_version(ansible.__version__) >= parse_version('2.0.0')
has_ansible_v24 = parse_version(ansible.__version__) >= parse_version('2.4.0')
has_ansible_v28 = parse_version(ansible.__version__) >= parse_version('2.8.0.dev0') \
or parse_version(ansible.__version__) >= parse_version('2.8.0')
13 changes: 8 additions & 5 deletions pytest_ansible/host_manager/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
"""Fixme."""

import ansible
from pkg_resources import parse_version
from pytest_ansible.logger import get_logger
from pytest_ansible.has_version import (
has_ansible_v2,
has_ansible_v24,
has_ansible_v28,
)

# conditionally import ansible libraries
has_ansible_v2 = parse_version(ansible.__version__) >= parse_version('2.0.0')
has_ansible_v24 = parse_version(ansible.__version__) >= parse_version('2.4.0')

log = get_logger(__name__)

Expand Down Expand Up @@ -102,7 +103,9 @@ def get_host_manager(*args, **kwargs):
"""Initialize and return a HostManager instance."""
log.debug("get_host_manager(%s, %s)" % (args, kwargs))

if has_ansible_v24:
if has_ansible_v28:
from pytest_ansible.host_manager.v28 import HostManagerV28 as HostManager
elif has_ansible_v24:
from pytest_ansible.host_manager.v24 import HostManagerV24 as HostManager
# from .v24 import HostManagerV24 as HostManager
elif has_ansible_v2:
Expand Down
27 changes: 27 additions & 0 deletions pytest_ansible/host_manager/v28.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from ansible.parsing.dataloader import DataLoader
from pytest_ansible.logger import get_logger
from pytest_ansible.host_manager import BaseHostManager
from pytest_ansible.module_dispatcher.v28 import ModuleDispatcherV28
from ansible.vars.manager import VariableManager
from ansible.inventory.manager import InventoryManager

log = get_logger(__name__)


class HostManagerV28(BaseHostManager):

"""Fixme."""

def __init__(self, *args, **kwargs):
"""Fixme."""
super(HostManagerV28, self).__init__(*args, **kwargs)
self._dispatcher = ModuleDispatcherV28

def initialize_inventory(self):
log.debug("HostManagerV28.initialize_inventory()")
self.options['loader'] = DataLoader()
self.options['inventory_manager'] = InventoryManager(loader=self.options['loader'],
sources=self.options['inventory'])
self.options['variable_manager'] = VariableManager(loader=self.options['loader'],
inventory=self.options['inventory_manager'])
# self.options['inventory_manager'].clear_caches()
3 changes: 1 addition & 2 deletions pytest_ansible/module_dispatcher/v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@
import ansible.utils
import ansible.errors

from pkg_resources import parse_version
from ansible.runner import Runner
from pytest_ansible.logger import get_logger
from pytest_ansible.module_dispatcher import BaseModuleDispatcher
from pytest_ansible.errors import AnsibleConnectionFailure
from pytest_ansible.results import AdHocResult
from pytest_ansible.has_version import has_ansible_v1

has_ansible_v1 = parse_version(ansible.__version__) < parse_version('2.0.0')

if not has_ansible_v1:
raise ImportError("Only supported with ansible < 2.0")
Expand Down
4 changes: 1 addition & 3 deletions pytest_ansible/module_dispatcher/v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import ansible.utils
import ansible.errors

from pkg_resources import parse_version
from ansible.plugins.callback import CallbackBase
from ansible.executor.task_queue_manager import TaskQueueManager
from ansible.playbook.play import Play
Expand All @@ -13,8 +12,7 @@
from pytest_ansible.module_dispatcher import BaseModuleDispatcher
from pytest_ansible.results import AdHocResult
from pytest_ansible.errors import AnsibleConnectionFailure

has_ansible_v2 = parse_version(ansible.__version__) >= parse_version('2.0.0')
from pytest_ansible.has_version import has_ansible_v2


if not has_ansible_v2:
Expand Down
7 changes: 4 additions & 3 deletions pytest_ansible/module_dispatcher/v24.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import ansible.utils
import ansible.errors

from pkg_resources import parse_version
from ansible.plugins.callback import CallbackBase
from ansible.executor.task_queue_manager import TaskQueueManager
from ansible.playbook.play import Play
Expand All @@ -12,10 +11,12 @@
from pytest_ansible.module_dispatcher.v2 import ModuleDispatcherV2
from pytest_ansible.results import AdHocResult
from pytest_ansible.errors import AnsibleConnectionFailure

has_ansible_v24 = parse_version(ansible.__version__) >= parse_version('2.4.0')
from pytest_ansible.has_version import (
has_ansible_v24,
)

if not has_ansible_v24:

raise ImportError("Only supported with ansible-2.4 and newer")
else:
from ansible.plugins.loader import module_loader
Expand Down
148 changes: 148 additions & 0 deletions pytest_ansible/module_dispatcher/v28.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import warnings
import ansible.constants
import ansible.utils
import ansible.errors

from ansible.plugins.callback import CallbackBase
from ansible.executor.task_queue_manager import TaskQueueManager
from ansible.playbook.play import Play
from ansible.cli.adhoc import AdHocCLI
from pytest_ansible.logger import get_logger
from pytest_ansible.module_dispatcher.v2 import ModuleDispatcherV2
from pytest_ansible.results import AdHocResult
from pytest_ansible.errors import AnsibleConnectionFailure
from pytest_ansible.has_version import has_ansible_v28


if not has_ansible_v28:
raise ImportError("Only supported with ansible-2.8 and newer")
else:
from ansible.plugins.loader import module_loader


log = get_logger(__name__)


class ResultAccumulator(CallbackBase):

"""Fixme."""

def __init__(self, *args, **kwargs):
"""Initialize object."""
super(ResultAccumulator, self).__init__(*args, **kwargs)
self.contacted = {}
self.unreachable = {}

def v2_runner_on_failed(self, result, *args, **kwargs):
self.contacted[result._host.get_name()] = result._result

v2_runner_on_ok = v2_runner_on_failed

def v2_runner_on_unreachable(self, result):
self.unreachable[result._host.get_name()] = result._result

@property
def results(self):
return dict(contacted=self.contacted, unreachable=self.unreachable)


class ModuleDispatcherV28(ModuleDispatcherV2):

"""Pass."""

required_kwargs = ('inventory', 'inventory_manager', 'variable_manager', 'host_pattern', 'loader')

def has_module(self, name):

return module_loader.has_plugin(name)

def _run(self, *module_args, **complex_args):
"""Execute an ansible adhoc command returning the result in a AdhocResult object."""
# Assemble module argument string
if module_args:
complex_args.update(dict(_raw_params=' '.join(module_args)))

# Assert hosts matching the provided pattern exist
hosts = self.options['inventory_manager'].list_hosts()
no_hosts = False
if len(hosts) == 0:
no_hosts = True
warnings.warn("provided hosts list is empty, only localhost is available")

self.options['inventory_manager'].subset(self.options.get('subset'))
hosts = self.options['inventory_manager'].list_hosts(self.options['host_pattern'])
if len(hosts) == 0 and not no_hosts:
raise ansible.errors.AnsibleError("Specified hosts and/or --limit does not match any hosts")

# Log the module and parameters
log.debug("[%s] %s: %s" % (self.options['host_pattern'], self.options['module_name'], complex_args))

# Pass along cli options
args = ['pytest-ansible', '-vvvvv', self.options['host_pattern']]
for argument in ('connection', 'user', 'become', 'become_method', 'become_user', 'module_path'):
arg_value = self.options.get(argument)
argument = argument.replace('_', '-')

if arg_value in (None, False):
continue

if arg_value is True:
args.append('--{0}'.format(argument))
else:
args.append('--{0}={1}'.format(argument, arg_value))

# Use Ansible's own adhoc cli to parse the fake command line we created and then save it
# into Ansible's global context
adhoc = AdHocCLI(args)
adhoc.parse()

# And now we'll never speak of this again
del adhoc

# Initialize callback to capture module JSON responses
cb = ResultAccumulator()

kwargs = dict(
inventory=self.options['inventory_manager'],
variable_manager=self.options['variable_manager'],
loader=self.options['loader'],
stdout_callback=cb,
passwords=dict(conn_pass=None, become_pass=None),
)

# create a pseudo-play to execute the specified module via a single task
play_ds = dict(
name="pytest-ansible",
hosts=self.options['host_pattern'],
gather_facts='no',
tasks=[
dict(
action=dict(
module=self.options['module_name'], args=complex_args
),
),
]
)
log.debug("Play(%s)", play_ds)
play = Play().load(play_ds, variable_manager=self.options['variable_manager'], loader=self.options['loader'])

# now create a task queue manager to execute the play
tqm = None
try:
log.debug("TaskQueueManager(%s)", kwargs)
tqm = TaskQueueManager(**kwargs)
tqm.run(play)
finally:
if tqm:
tqm.cleanup()

# Log the results
log.debug(cb.results)

# Raise exception if host(s) unreachable
# FIXME - if multiple hosts were involved, should an exception be raised?
if cb.unreachable:
raise AnsibleConnectionFailure("Host unreachable", dark=cb.unreachable, contacted=cb.contacted)

# Success!
return AdHocResult(contacted=cb.contacted)
24 changes: 19 additions & 5 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import ansible
import pytest
import logging
from pkg_resources import parse_version
from pytest_ansible.has_version import (
has_ansible_v1,
has_ansible_v24,
)
try:
from ansible.utils import context_objects as co
except ImportError:
# if it does not exist because of old version of ansible, we don't need it
co = None


pytest_plugins = 'pytester',
Expand Down Expand Up @@ -66,16 +73,15 @@
def pytest_runtest_setup(item):
# Conditionally skip tests that are pinned to a specific ansible version
if isinstance(item, pytest.Function):
has_ansible_v1 = parse_version(ansible.__version__) < parse_version('2.0.0')
has_ansible_v24 = parse_version(ansible.__version__) >= parse_version('2.4.0')

# conditionally skip
if item.get_closest_marker('requires_ansible_v1') and not has_ansible_v1:
pytest.skip("requires < ansible-2.*")
if item.get_closest_marker('requires_ansible_v2') and has_ansible_v1:
pytest.skip("requires >= ansible-2.*")
if item.get_closest_marker('requires_ansible_v24') and not has_ansible_v24:
pytest.skip("requires >= ansible-2.4.*")
if item.get_closest_marker('requires_ansible_v28') and not has_ansible_v24:
pytest.skip("requires >= ansible-2.8.*")

# conditionally xfail
mark = item.get_closest_marker('ansible_v1_xfail')
Expand Down Expand Up @@ -125,6 +131,14 @@ def args(self):
return args


@pytest.fixture(autouse=True)
def clear_global_context():
# Reset the stored command line args
# if context object does not exist because of old version of ansible, we don't need it
if co is not None:
co.GlobalCLIArgs._Singleton__instance = None


@pytest.fixture()
def option(request, testdir):
'''Returns an instance of PyTestOption to help tests pass parameters and
Expand Down
12 changes: 10 additions & 2 deletions tests/test_params.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import sys
import pytest
import ansible
import mock
from pkg_resources import parse_version
from pytest_ansible.has_version import has_ansible_v28
try:
import mock
except ImportError:
from unittest import mock
import re
from _pytest.main import EXIT_OK, EXIT_TESTSFAILED, EXIT_USAGEERROR, EXIT_NOTESTSCOLLECTED, EXIT_INTERRUPTED
from pkg_resources import parse_version

if sys.version_info[0] == 2:
import __builtin__ as builtins # NOQA
Expand Down Expand Up @@ -206,6 +210,10 @@ def test_func(ansible_module):


@pytest.mark.requires_ansible_v24
@pytest.mark.skipif(
has_ansible_v28,
reason="requires ansible < 2.8"
)
def test_params_required_with_bogus_inventory_v24(testdir, option, recwarn):
src = """
import pytest
Expand Down
Loading