mirror of
https://github.com/saltstack/salt.git
synced 2025-04-17 10:10:20 +00:00
No hacks when interacting with ansible. Access all available modules, not just internal.
This commit is contained in:
parent
d83dd0319f
commit
de6f56ccaf
5 changed files with 306 additions and 425 deletions
|
@ -1,19 +1,6 @@
|
|||
#
|
||||
# Author: Bo Maryniuk <bo@suse.de>
|
||||
#
|
||||
# Copyright 2017 SUSE LLC
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
Ansible Support
|
||||
===============
|
||||
|
@ -27,30 +14,21 @@ The timeout is how many seconds Salt should wait for
|
|||
any Ansible module to respond.
|
||||
"""
|
||||
|
||||
|
||||
import fnmatch
|
||||
import importlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
import salt.utils.decorators.path
|
||||
import salt.utils.json
|
||||
import salt.utils.path
|
||||
import salt.utils.platform
|
||||
import salt.utils.stringutils
|
||||
import salt.utils.timed_subprocess
|
||||
import salt.utils.yaml
|
||||
from salt.exceptions import CommandExecutionError, LoaderError
|
||||
from salt.utils.decorators import depends
|
||||
|
||||
try:
|
||||
import ansible
|
||||
import ansible.constants # pylint: disable=no-name-in-module
|
||||
import ansible.modules # pylint: disable=no-name-in-module
|
||||
except ImportError:
|
||||
ansible = None
|
||||
from salt.exceptions import CommandExecutionError
|
||||
|
||||
# Function alias to make sure not to shadow built-in's
|
||||
__func_alias__ = {"list_": "list"}
|
||||
|
@ -59,155 +37,14 @@ __virtualname__ = "ansible"
|
|||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
INVENTORY = """
|
||||
hosts:
|
||||
vars:
|
||||
ansible_connection: local
|
||||
"""
|
||||
DEFAULT_TIMEOUT = 1200 # seconds (20 minutes)
|
||||
|
||||
class AnsibleModuleResolver:
|
||||
"""
|
||||
This class is to resolve all available modules in Ansible.
|
||||
"""
|
||||
|
||||
def __init__(self, opts):
|
||||
self.opts = opts
|
||||
self._modules_map = {}
|
||||
|
||||
def _get_modules_map(self, path=None):
|
||||
"""
|
||||
Get installed Ansible modules
|
||||
:return:
|
||||
"""
|
||||
paths = {}
|
||||
root = ansible.modules.__path__[0]
|
||||
if not path:
|
||||
path = root
|
||||
for p_el in os.listdir(path):
|
||||
p_el_path = os.path.join(path, p_el)
|
||||
if os.path.islink(p_el_path):
|
||||
continue
|
||||
if os.path.isdir(p_el_path):
|
||||
paths.update(self._get_modules_map(p_el_path))
|
||||
else:
|
||||
if (
|
||||
any(p_el.startswith(elm) for elm in ["__", "."])
|
||||
or not p_el.endswith(".py")
|
||||
or p_el in ansible.constants.IGNORE_FILES
|
||||
):
|
||||
continue
|
||||
p_el_path = p_el_path.replace(root, "").split(".")[0]
|
||||
als_name = (
|
||||
p_el_path.replace(".", "").replace("/", "", 1).replace("/", ".")
|
||||
)
|
||||
paths[als_name] = p_el_path
|
||||
|
||||
return paths
|
||||
|
||||
def load_module(self, module):
|
||||
"""
|
||||
Introspect Ansible module.
|
||||
|
||||
:param module:
|
||||
:return:
|
||||
"""
|
||||
m_ref = self._modules_map.get(module)
|
||||
if m_ref is None:
|
||||
raise LoaderError('Module "{}" was not found'.format(module))
|
||||
mod = importlib.import_module(
|
||||
"ansible.modules{}".format(
|
||||
".".join([elm.split(".")[0] for elm in m_ref.split(os.path.sep)])
|
||||
)
|
||||
)
|
||||
|
||||
return mod
|
||||
|
||||
def get_modules_list(self, pattern=None):
|
||||
"""
|
||||
Return module map references.
|
||||
:return:
|
||||
"""
|
||||
if pattern and "*" not in pattern:
|
||||
pattern = "*{}*".format(pattern)
|
||||
modules = []
|
||||
for m_name, m_path in self._modules_map.items():
|
||||
m_path = m_path.split(".")[0]
|
||||
m_name = ".".join([elm for elm in m_path.split(os.path.sep) if elm])
|
||||
if pattern and fnmatch.fnmatch(m_name, pattern) or not pattern:
|
||||
modules.append(m_name)
|
||||
return sorted(modules)
|
||||
|
||||
def resolve(self):
|
||||
log.debug("Resolving Ansible modules")
|
||||
self._modules_map = self._get_modules_map()
|
||||
return self
|
||||
|
||||
def install(self):
|
||||
log.debug("Installing Ansible modules")
|
||||
return self
|
||||
|
||||
|
||||
class AnsibleModuleCaller:
|
||||
DEFAULT_TIMEOUT = 1200 # seconds (20 minutes)
|
||||
OPT_TIMEOUT_KEY = "ansible_timeout"
|
||||
|
||||
def __init__(self, resolver):
|
||||
self._resolver = resolver
|
||||
self.timeout = self._resolver.opts.get(
|
||||
self.OPT_TIMEOUT_KEY, self.DEFAULT_TIMEOUT
|
||||
)
|
||||
|
||||
def call(self, module, *args, **kwargs):
|
||||
"""
|
||||
Call an Ansible module by invoking it.
|
||||
:param module: the name of the module.
|
||||
:param args: Arguments to the module
|
||||
:param kwargs: keywords to the module
|
||||
:return:
|
||||
"""
|
||||
|
||||
module = self._resolver.load_module(module)
|
||||
if not hasattr(module, "main"):
|
||||
raise CommandExecutionError(
|
||||
"This module is not callable "
|
||||
'(see "ansible.help {}")'.format(
|
||||
module.__name__.replace("ansible.modules.", "")
|
||||
)
|
||||
)
|
||||
if args:
|
||||
kwargs["_raw_params"] = " ".join(args)
|
||||
js_args = salt.utils.json.dumps({"ANSIBLE_MODULE_ARGS": kwargs})
|
||||
|
||||
proc_exc = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
"-c",
|
||||
"import sys, {0}; print({0}.main(), file=sys.stdout); sys.stdout.flush()".format(
|
||||
module.__name__
|
||||
),
|
||||
],
|
||||
input=js_args,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
timeout=self.timeout,
|
||||
universal_newlines=True,
|
||||
check=True,
|
||||
shell=False,
|
||||
)
|
||||
|
||||
try:
|
||||
out = salt.utils.json.loads(proc_exc.stdout)
|
||||
except ValueError as ex:
|
||||
out = {"Error": (proc_exc.stderr and (proc_exc.stderr + ".") or str(ex))}
|
||||
if proc_exc.stdout:
|
||||
out["Given JSON output"] = proc_exc.stdout
|
||||
return out
|
||||
|
||||
if "invocation" in out:
|
||||
del out["invocation"]
|
||||
|
||||
out["timeout"] = self.timeout
|
||||
|
||||
return out
|
||||
|
||||
|
||||
_resolver = None
|
||||
_caller = None
|
||||
__load__ = __non_ansible_functions__ = ["help", "list_", "call", "playbooks"][:]
|
||||
|
||||
|
||||
def _set_callables(modules):
|
||||
|
@ -221,99 +58,203 @@ def _set_callables(modules):
|
|||
Create a Salt function for the Ansible module.
|
||||
"""
|
||||
|
||||
def _cmd(*args, **kw):
|
||||
def _cmd(*args, **kwargs):
|
||||
"""
|
||||
Call an Ansible module as a function from the Salt.
|
||||
"""
|
||||
kwargs = {}
|
||||
for _kw in kw.get("__pub_arg", []):
|
||||
if isinstance(_kw, dict):
|
||||
kwargs = _kw
|
||||
break
|
||||
else:
|
||||
kwargs = {k: v for (k, v) in kw.items() if not k.statsrtwith("__pub")}
|
||||
|
||||
return _caller.call(cmd_name, *args, **kwargs)
|
||||
return call(cmd_name, *args, **kwargs)
|
||||
|
||||
_cmd.__doc__ = doc
|
||||
return _cmd
|
||||
|
||||
for mod in modules:
|
||||
setattr(sys.modules[__name__], mod, _set_function(mod, "Available"))
|
||||
for mod, doc in modules.items():
|
||||
__load__.append(mod)
|
||||
setattr(sys.modules[__name__], mod, _set_function(mod, doc))
|
||||
|
||||
|
||||
def __virtual__():
|
||||
"""
|
||||
Ansible module caller.
|
||||
:return:
|
||||
"""
|
||||
if salt.utils.platform.is_windows():
|
||||
return False, "The ansiblegate module isn't supported on Windows"
|
||||
if ansible is None:
|
||||
return False, "Ansible is not installed on this system"
|
||||
global _resolver
|
||||
global _caller
|
||||
_resolver = AnsibleModuleResolver(__opts__).resolve().install()
|
||||
_caller = AnsibleModuleCaller(_resolver)
|
||||
_set_callables(list_())
|
||||
ansible_bin = salt.utils.path.which("ansible")
|
||||
if not ansible_bin:
|
||||
return False, "The 'ansible' binary was not found."
|
||||
ansible_doc_bin = salt.utils.path.which("ansible-doc")
|
||||
if not ansible_doc_bin:
|
||||
return False, "The 'ansible-doc' binary was not found."
|
||||
ansible_playbook_bin = salt.utils.path.which("ansible-playbook")
|
||||
if not ansible_playbook_bin:
|
||||
return False, "The 'ansible-playbook' binary was not found."
|
||||
|
||||
proc = subprocess.run(
|
||||
[ansible_doc_bin, "--list", "--json", "--type=module"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
check=False,
|
||||
shell=False,
|
||||
universal_newlines=True,
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
return (
|
||||
False,
|
||||
"Failed to get the listing of ansible modules:\n{}".format(proc.stderr),
|
||||
)
|
||||
|
||||
ansible_module_listing = salt.utils.json.loads(proc.stdout)
|
||||
for key in list(ansible_module_listing):
|
||||
if key.startswith("ansible."):
|
||||
# Fyi, str.partition() is faster than str.replace()
|
||||
_, _, alias = key.partition(".")
|
||||
ansible_module_listing[alias] = ansible_module_listing[key]
|
||||
_set_callables(ansible_module_listing)
|
||||
return __virtualname__
|
||||
|
||||
|
||||
@depends("ansible")
|
||||
def help(module=None, *args):
|
||||
"""
|
||||
Display help on Ansible standard module.
|
||||
|
||||
:param module:
|
||||
:return:
|
||||
:param module: The module to get the help
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt * ansible.help ping
|
||||
"""
|
||||
if not module:
|
||||
raise CommandExecutionError(
|
||||
"Please tell me what module you want to have helped with. "
|
||||
'Or call "ansible.list" to know what is available.'
|
||||
)
|
||||
try:
|
||||
module = _resolver.load_module(module)
|
||||
except (ImportError, LoaderError) as err:
|
||||
raise CommandExecutionError(
|
||||
'Module "{}" is currently not functional on your system.'.format(module)
|
||||
)
|
||||
|
||||
doc = {}
|
||||
ret = {}
|
||||
for docset in module.DOCUMENTATION.split("---"):
|
||||
try:
|
||||
docset = salt.utils.yaml.safe_load(docset)
|
||||
if docset:
|
||||
doc.update(docset)
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
log.error("Error parsing doc section: %s", err)
|
||||
ansible_doc_bin = salt.utils.path.which("ansible-doc")
|
||||
|
||||
proc = subprocess.run(
|
||||
[ansible_doc_bin, "--json", "--type=module", module],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
check=True,
|
||||
shell=False,
|
||||
universal_newlines=True,
|
||||
)
|
||||
data = salt.utils.json.loads(proc.stdout)
|
||||
doc = data[next(iter(data))]
|
||||
if not args:
|
||||
if "description" in doc:
|
||||
description = doc.get("description") or ""
|
||||
del doc["description"]
|
||||
ret["Description"] = description
|
||||
ret[
|
||||
'Available sections on module "{}"'.format(
|
||||
module.__name__.replace("ansible.modules.", "")
|
||||
)
|
||||
] = list(doc)
|
||||
ret = doc["doc"]
|
||||
for section in ("examples", "return", "metadata"):
|
||||
section_data = doc.get(section)
|
||||
if section_data:
|
||||
ret[section] = section_data
|
||||
else:
|
||||
ret = {}
|
||||
for arg in args:
|
||||
info = doc.get(arg)
|
||||
if info is not None:
|
||||
ret[arg] = info
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
@depends("ansible")
|
||||
def list_(pattern=None):
|
||||
"""
|
||||
Lists available modules.
|
||||
:return:
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt * ansible.list
|
||||
salt * ansible.list '*win*' # To get all modules matching 'win' on it's name
|
||||
"""
|
||||
return _resolver.get_modules_list(pattern=pattern)
|
||||
if pattern is None:
|
||||
module_list = set(__load__)
|
||||
module_list.discard(set(__non_ansible_functions__))
|
||||
return sorted(module_list)
|
||||
return sorted(fnmatch.filter(__load__, pattern))
|
||||
|
||||
|
||||
def call(module, *args, **kwargs):
|
||||
"""
|
||||
Call an Ansible module by invoking it.
|
||||
|
||||
:param module: the name of the module.
|
||||
:param args: Arguments to pass to the module
|
||||
:param kwargs: keywords to pass to the module
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt * ansible.call ping data=foobar
|
||||
"""
|
||||
|
||||
module_args = []
|
||||
for arg in args:
|
||||
module_args.append(salt.utils.json.dumps(arg))
|
||||
|
||||
_kwargs = {}
|
||||
for _kw in kwargs.get("__pub_arg", []):
|
||||
if isinstance(_kw, dict):
|
||||
_kwargs = _kw
|
||||
break
|
||||
else:
|
||||
_kwargs = {k: v for (k, v) in kwargs.items() if not k.startswith("__pub")}
|
||||
|
||||
for key, value in _kwargs.items():
|
||||
module_args.append("{}={}".format(key, salt.utils.json.dumps(value)))
|
||||
|
||||
with NamedTemporaryFile(mode="w") as inventory:
|
||||
|
||||
ansible_binary_path = salt.utils.path.which("ansible")
|
||||
log.debug("Calling ansible module %r", module)
|
||||
try:
|
||||
proc_exc = subprocess.run(
|
||||
[
|
||||
ansible_binary_path,
|
||||
"localhost",
|
||||
"--limit",
|
||||
"127.0.0.1",
|
||||
"-m",
|
||||
module,
|
||||
"-a",
|
||||
" ".join(module_args),
|
||||
"-i",
|
||||
inventory.name,
|
||||
],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
timeout=__opts__.get("ansible_timeout", DEFAULT_TIMEOUT),
|
||||
universal_newlines=True,
|
||||
check=True,
|
||||
shell=False,
|
||||
)
|
||||
|
||||
original_output = proc_exc.stdout
|
||||
proc_out = original_output.splitlines()
|
||||
if proc_out[0].endswith("{"):
|
||||
proc_out[0] = "{"
|
||||
try:
|
||||
out = salt.utils.json.loads("\n".join(proc_out))
|
||||
except ValueError as exc:
|
||||
out = {
|
||||
"Error": proc_exc.stderr or str(exc),
|
||||
"Output": original_output,
|
||||
}
|
||||
return out
|
||||
elif proc_out[0].endswith(">>"):
|
||||
out = {"output": "\n".join(proc_out[1:])}
|
||||
else:
|
||||
out = {"output": original_output}
|
||||
|
||||
except subprocess.CalledProcessError as exc:
|
||||
out = {"Exitcode": exc.returncode, "Error": exc.stderr or str(exc)}
|
||||
if exc.stdout:
|
||||
out["Given JSON output"] = exc.stdout
|
||||
return out
|
||||
|
||||
for key in ("invocation", "changed"):
|
||||
out.pop(key, None)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
@salt.utils.decorators.path.which("ansible-playbook")
|
||||
|
|
|
@ -1,44 +1,43 @@
|
|||
import pytest
|
||||
import salt.modules.ansiblegate as ansiblegate
|
||||
|
||||
pytestmark = [
|
||||
pytest.mark.skipif(ansiblegate.ansible is None, reason="Ansible is not installed"),
|
||||
pytest.mark.skip_on_windows(reason="Not supported on Windows"),
|
||||
pytest.mark.skip_if_binaries_missing(
|
||||
"ansible",
|
||||
"ansible-doc",
|
||||
"ansible-playbook",
|
||||
check_all=True,
|
||||
reason="ansible is not installed",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def test_ansible_functions_loaded(modules):
|
||||
"""
|
||||
Test that the ansible functions are actually loaded
|
||||
"""
|
||||
@pytest.fixture
|
||||
def ansible_ping_func(modules):
|
||||
if "ansible.system.ping" in modules:
|
||||
# we need to go by getattr() because salt's loader will try to find "system" in the dictionary and fail
|
||||
# The ansible hack injects, in this case, "system.ping" as an attribute to the loaded module
|
||||
ret = getattr(modules.ansible, "system.ping")()
|
||||
elif "ansible.ping" in modules:
|
||||
# Ansible >= 2.10
|
||||
ret = modules.ansible.ping()
|
||||
else:
|
||||
pytest.fail("Where is the ping function these days in Ansible?!")
|
||||
return getattr(modules.ansible, "system.ping")
|
||||
|
||||
ret.pop("timeout", None)
|
||||
if "ansible.ping" in modules:
|
||||
# Ansible >= 2.10
|
||||
return modules.ansible.ping
|
||||
|
||||
pytest.fail("Where is the ping function these days in Ansible?!")
|
||||
|
||||
|
||||
def test_ansible_functions_loaded(ansible_ping_func):
|
||||
"""
|
||||
Test that the ansible functions are actually loaded
|
||||
"""
|
||||
ret = ansible_ping_func()
|
||||
assert ret == {"ping": "pong"}
|
||||
|
||||
|
||||
def test_passing_data_to_ansible_modules(modules):
|
||||
def test_passing_data_to_ansible_modules(ansible_ping_func):
|
||||
"""
|
||||
Test that the ansible functions are actually loaded
|
||||
"""
|
||||
expected = "foobar"
|
||||
if "ansible.system.ping" in modules:
|
||||
# we need to go by getattr() because salt's loader will try to find "system" in the dictionary and fail
|
||||
# The ansible hack injects, in this case, "system.ping" as an attribute to the loaded module
|
||||
ret = getattr(modules.ansible, "system.ping")(data=expected)
|
||||
elif "ansible.ping" in modules:
|
||||
# Ansible >= 2.10
|
||||
ret = modules.ansible.ping(data=expected)
|
||||
else:
|
||||
pytest.fail("Where is the ping function these days in Ansible?!")
|
||||
|
||||
ret.pop("timeout", None)
|
||||
ret = ansible_ping_func(data=expected)
|
||||
assert ret == {"ping": expected}
|
||||
|
|
|
@ -17,16 +17,13 @@ pytestmark = [
|
|||
# Because of the above, these are also destructive tests
|
||||
pytest.mark.destructive_test,
|
||||
pytest.mark.skip_if_binaries_missing(
|
||||
"ansible-playbook", message="ansible-playbook is not installed"
|
||||
"ansible-playbook", reason="ansible-playbook is not installed"
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def ansible_inventory_directory(tmp_path_factory, grains):
|
||||
import pprint
|
||||
|
||||
pprint.pprint(grains)
|
||||
if grains["os_family"] != "RedHat":
|
||||
pytest.skip("Currently, the test targets the RedHat OS familly only.")
|
||||
tmp_dir = tmp_path_factory.mktemp("ansible")
|
||||
|
@ -38,7 +35,7 @@ def ansible_inventory_directory(tmp_path_factory, grains):
|
|||
|
||||
@pytest.fixture(scope="module", autouse=True)
|
||||
def ansible_inventory(ansible_inventory_directory, sshd_server):
|
||||
inventory = ansible_inventory_directory / "inventory"
|
||||
inventory = str(ansible_inventory_directory / "inventory")
|
||||
client_key = str(sshd_server.config_dir / "client_key")
|
||||
data = {
|
||||
"all": {
|
||||
|
@ -56,9 +53,9 @@ def ansible_inventory(ansible_inventory_directory, sshd_server):
|
|||
},
|
||||
},
|
||||
}
|
||||
with salt.utils.files.fopen(str(inventory), "w") as yaml_file:
|
||||
with salt.utils.files.fopen(inventory, "w") as yaml_file:
|
||||
yaml.dump(data, yaml_file, default_flow_style=False)
|
||||
return str(inventory)
|
||||
return inventory
|
||||
|
||||
|
||||
@pytest.mark.requires_sshd_server
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
# Author: Bo Maryniuk <bo@suse.de>
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import salt.modules.ansiblegate as ansiblegate
|
||||
from salt.exceptions import LoaderError
|
||||
from tests.support.mock import MagicMock, patch
|
||||
import salt.utils.json
|
||||
from tests.support.mock import ANY, MagicMock, patch
|
||||
|
||||
pytestmark = [
|
||||
pytest.mark.skip_on_windows(reason="Not supported on Windows"),
|
||||
|
@ -18,165 +15,112 @@ def configure_loader_modules():
|
|||
return {ansiblegate: {}}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def resolver():
|
||||
_resolver = ansiblegate.AnsibleModuleResolver({})
|
||||
_resolver._modules_map = {
|
||||
"one.two.three": os.sep + os.path.join("one", "two", "three.py"),
|
||||
"four.five.six": os.sep + os.path.join("four", "five", "six.py"),
|
||||
"three.six.one": os.sep + os.path.join("three", "six", "one.py"),
|
||||
}
|
||||
return _resolver
|
||||
|
||||
|
||||
def test_ansible_module_help(resolver):
|
||||
def test_ansible_module_help():
|
||||
"""
|
||||
Test help extraction from the module
|
||||
:return:
|
||||
"""
|
||||
extension = {
|
||||
"foo": {
|
||||
"doc": {"description": "The description of foo"},
|
||||
"examples": "These are the examples",
|
||||
"return": {"a": "A return"},
|
||||
}
|
||||
}
|
||||
|
||||
class Module:
|
||||
"""
|
||||
An ansible module mock.
|
||||
"""
|
||||
|
||||
__name__ = "foo"
|
||||
DOCUMENTATION = """
|
||||
---
|
||||
one:
|
||||
text here
|
||||
---
|
||||
two:
|
||||
text here
|
||||
description:
|
||||
describe the second part
|
||||
"""
|
||||
|
||||
with patch.object(ansiblegate, "_resolver", resolver), patch.object(
|
||||
ansiblegate._resolver, "load_module", MagicMock(return_value=Module())
|
||||
):
|
||||
ret = ansiblegate.help("dummy")
|
||||
assert sorted(
|
||||
ret.get('Available sections on module "{}"'.format(Module().__name__))
|
||||
) == ["one", "two"]
|
||||
assert ret.get("Description") == "describe the second part"
|
||||
with patch("subprocess.run") as proc_run_mock:
|
||||
proc_run_mock.return_value.stdout = salt.utils.json.dumps(extension)
|
||||
ret = ansiblegate.help("foo")
|
||||
assert ret["description"] == extension["foo"]["doc"]["description"]
|
||||
|
||||
|
||||
def test_module_resolver_modlist(resolver):
|
||||
"""
|
||||
Test Ansible resolver modules list.
|
||||
:return:
|
||||
"""
|
||||
assert resolver.get_modules_list() == [
|
||||
"four.five.six",
|
||||
"one.two.three",
|
||||
"three.six.one",
|
||||
]
|
||||
for ptr in ["five", "fi", "ve"]:
|
||||
assert resolver.get_modules_list(ptr) == ["four.five.six"]
|
||||
for ptr in ["si", "ix", "six"]:
|
||||
assert resolver.get_modules_list(ptr) == ["four.five.six", "three.six.one"]
|
||||
assert resolver.get_modules_list("one") == ["one.two.three", "three.six.one"]
|
||||
assert resolver.get_modules_list("one.two") == ["one.two.three"]
|
||||
assert resolver.get_modules_list("four") == ["four.five.six"]
|
||||
|
||||
|
||||
def test_resolver_module_loader_failure(resolver):
|
||||
"""
|
||||
Test Ansible module loader.
|
||||
:return:
|
||||
"""
|
||||
mod = "four.five.six"
|
||||
with pytest.raises(ImportError):
|
||||
resolver.load_module(mod)
|
||||
|
||||
mod = "i.even.do.not.exist.at.all"
|
||||
with pytest.raises(LoaderError):
|
||||
resolver.load_module(mod)
|
||||
|
||||
|
||||
def test_resolver_module_loader(resolver):
|
||||
"""
|
||||
Test Ansible module loader.
|
||||
:return:
|
||||
"""
|
||||
with patch("salt.modules.ansiblegate.importlib", MagicMock()), patch(
|
||||
"salt.modules.ansiblegate.importlib.import_module", lambda x: x
|
||||
):
|
||||
assert resolver.load_module("four.five.six") == "ansible.modules.four.five.six"
|
||||
|
||||
|
||||
def test_resolver_module_loader_import_failure(resolver):
|
||||
"""
|
||||
Test Ansible module loader failure.
|
||||
:return:
|
||||
"""
|
||||
with patch("salt.modules.ansiblegate.importlib", MagicMock()), patch(
|
||||
"salt.modules.ansiblegate.importlib.import_module", lambda x: x
|
||||
):
|
||||
with pytest.raises(LoaderError):
|
||||
resolver.load_module("something.strange")
|
||||
|
||||
|
||||
def test_virtual_function(resolver):
|
||||
def test_virtual_function(subtests):
|
||||
"""
|
||||
Test Ansible module __virtual__ when ansible is not installed on the minion.
|
||||
:return:
|
||||
"""
|
||||
with patch("salt.modules.ansiblegate.ansible", None):
|
||||
assert ansiblegate.__virtual__() == (
|
||||
False,
|
||||
"Ansible is not installed on this system",
|
||||
)
|
||||
|
||||
with subtests.test("missing ansible binary"):
|
||||
with patch("salt.utils.path.which", side_effect=[None]):
|
||||
assert ansiblegate.__virtual__() == (
|
||||
False,
|
||||
"The 'ansible' binary was not found.",
|
||||
)
|
||||
|
||||
with subtests.test("missing ansible-doc binary"):
|
||||
with patch(
|
||||
"salt.utils.path.which", side_effect=["/path/to/ansible", None],
|
||||
):
|
||||
assert ansiblegate.__virtual__() == (
|
||||
False,
|
||||
"The 'ansible-doc' binary was not found.",
|
||||
)
|
||||
|
||||
with subtests.test("missing ansible-playbook binary"):
|
||||
with patch(
|
||||
"salt.utils.path.which",
|
||||
side_effect=["/path/to/ansible", "/path/to/ansible-doc", None],
|
||||
):
|
||||
assert ansiblegate.__virtual__() == (
|
||||
False,
|
||||
"The 'ansible-playbook' binary was not found.",
|
||||
)
|
||||
|
||||
with subtests.test("Failing to load the ansible modules listing"):
|
||||
with patch(
|
||||
"salt.utils.path.which",
|
||||
side_effect=[
|
||||
"/path/to/ansible",
|
||||
"/path/to/ansible-doc",
|
||||
"/path/to/ansible-playbook",
|
||||
],
|
||||
):
|
||||
with patch("subprocess.run") as proc_run_mock:
|
||||
proc_run_mock.return_value.retcode = 1
|
||||
proc_run_mock.return_value.stderr = "bar"
|
||||
proc_run_mock.return_value.stdout = "{}"
|
||||
assert ansiblegate.__virtual__() == (
|
||||
False,
|
||||
"Failed to get the listing of ansible modules:\nbar",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.version_info < (3, 6),
|
||||
reason="Skipped on Py3.5, the mock of subprocess.run is different",
|
||||
)
|
||||
def test_ansible_module_call(resolver):
|
||||
def test_ansible_module_call():
|
||||
"""
|
||||
Test Ansible module call from ansible gate module
|
||||
:return:
|
||||
"""
|
||||
|
||||
class Module:
|
||||
"""
|
||||
An ansible module mock.
|
||||
"""
|
||||
with patch("subprocess.run") as proc_run_mock:
|
||||
proc_run_mock.return_value.stdout = (
|
||||
'localhost | SUCCESS => {\n "completed": true \n}'
|
||||
)
|
||||
|
||||
__name__ = "one.two.three"
|
||||
__file__ = "foofile"
|
||||
|
||||
def main(): # pylint: disable=no-method-argument
|
||||
pass
|
||||
|
||||
with patch.object(ansiblegate, "_resolver", resolver), patch.object(
|
||||
ansiblegate._resolver, "load_module", MagicMock(return_value=Module())
|
||||
):
|
||||
_ansible_module_caller = ansiblegate.AnsibleModuleCaller(ansiblegate._resolver)
|
||||
with patch("subprocess.run") as proc_run_mock:
|
||||
proc_run_mock.return_value.stdout = '{"completed": true}'
|
||||
|
||||
ret = _ansible_module_caller.call("one.two.three", "arg_1", kwarg1="foobar")
|
||||
proc_run_mock.assert_any_call(
|
||||
[
|
||||
sys.executable,
|
||||
"-c",
|
||||
"import sys, one.two.three; print(one.two.three.main(), file=sys.stdout); sys.stdout.flush()",
|
||||
],
|
||||
input='{"ANSIBLE_MODULE_ARGS": {"kwarg1": "foobar", "_raw_params": "arg_1"}}',
|
||||
stdout=-1,
|
||||
stderr=-1,
|
||||
check=True,
|
||||
shell=False,
|
||||
universal_newlines=True,
|
||||
timeout=1200,
|
||||
)
|
||||
assert ret == {"completed": True, "timeout": 1200}
|
||||
ret = ansiblegate.call("one.two.three", "arg_1", kwarg1="foobar")
|
||||
proc_run_mock.assert_any_call(
|
||||
[
|
||||
ANY,
|
||||
"localhost",
|
||||
"--limit",
|
||||
"127.0.0.1",
|
||||
"-m",
|
||||
"one.two.three",
|
||||
"-a",
|
||||
'"arg_1" kwarg1="foobar"',
|
||||
"-i",
|
||||
ANY,
|
||||
],
|
||||
check=True,
|
||||
shell=False,
|
||||
stderr=-1,
|
||||
stdout=-1,
|
||||
timeout=1200,
|
||||
universal_newlines=True,
|
||||
)
|
||||
assert ret == {"completed": True}
|
||||
|
||||
|
||||
def test_ansible_playbooks_return_retcode(resolver):
|
||||
def test_ansible_playbooks_return_retcode():
|
||||
"""
|
||||
Test ansible.playbooks execution module function include retcode in the return.
|
||||
:return:
|
||||
|
|
|
@ -29,16 +29,17 @@ def test_ansible_playbooks_states_success(playbooks_examples_dir):
|
|||
with patch.dict(
|
||||
ansiblegate.__salt__,
|
||||
{"ansible.playbooks": MagicMock(return_value=success_output)},
|
||||
), patch("salt.utils.path.which", MagicMock(return_value=True)):
|
||||
with patch.dict(ansiblegate.__opts__, {"test": False}):
|
||||
ret = ansiblegate.playbooks("foobar")
|
||||
assert ret["result"] is True
|
||||
assert ret["comment"] == "Changes were made by playbook foobar"
|
||||
assert ret["changes"] == {
|
||||
"py2hosts": {
|
||||
"Ansible copy file to remote server": {"centos7-host1.tf.local": {}}
|
||||
}
|
||||
), patch("salt.utils.path.which", return_value=True), patch.dict(
|
||||
ansiblegate.__opts__, {"test": False}
|
||||
):
|
||||
ret = ansiblegate.playbooks("foobar")
|
||||
assert ret["result"] is True
|
||||
assert ret["comment"] == "Changes were made by playbook foobar"
|
||||
assert ret["changes"] == {
|
||||
"py2hosts": {
|
||||
"Ansible copy file to remote server": {"centos7-host1.tf.local": {}}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def test_ansible_playbooks_states_failed(playbooks_examples_dir):
|
||||
|
@ -52,19 +53,18 @@ def test_ansible_playbooks_states_failed(playbooks_examples_dir):
|
|||
with patch.dict(
|
||||
ansiblegate.__salt__,
|
||||
{"ansible.playbooks": MagicMock(return_value=failed_output)},
|
||||
), patch("salt.utils.path.which", MagicMock(return_value=True)):
|
||||
with patch.dict(ansiblegate.__opts__, {"test": False}):
|
||||
ret = ansiblegate.playbooks("foobar")
|
||||
assert ret["result"] is False
|
||||
assert (
|
||||
ret["comment"] == "There were some issues running the playbook foobar"
|
||||
)
|
||||
assert ret["changes"] == {
|
||||
"py2hosts": {
|
||||
"yum": {
|
||||
"centos7-host1.tf.local": [
|
||||
"No package matching 'rsyndc' found available, installed or updated"
|
||||
]
|
||||
}
|
||||
), patch("salt.utils.path.which", return_value=True), patch.dict(
|
||||
ansiblegate.__opts__, {"test": False}
|
||||
):
|
||||
ret = ansiblegate.playbooks("foobar")
|
||||
assert ret["result"] is False
|
||||
assert ret["comment"] == "There were some issues running the playbook foobar"
|
||||
assert ret["changes"] == {
|
||||
"py2hosts": {
|
||||
"yum": {
|
||||
"centos7-host1.tf.local": [
|
||||
"No package matching 'rsyndc' found available, installed or updated"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue