Mini Shell
#
# 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.
r"""
Execution of Ansible modules from within states
===============================================
With `ansible.call` these states allow individual Ansible module calls to be
made via states. To call an Ansible module function use a :mod:`module.run <salt.states.ansible.call>`
state:
.. code-block:: yaml
some_set_of_tasks:
ansible:
- system.ping
- packaging.os.zypper
- name: emacs
- state: installed
"""
import logging
import os
import sys
import salt.fileclient
import salt.utils.decorators.path
from salt.loader.dunder import __file_client__
from salt.utils.decorators import depends
log = logging.getLogger(__name__)
__virtualname__ = "ansible"
def _file_client():
"""
Return a file client
If the __file_client__ context is set return it, otherwize create a new
file client using __opts__.
"""
if __file_client__:
return __file_client__.value()
return salt.fileclient.get_file_client(__opts__)
@depends("ansible")
class AnsibleState:
"""
Ansible state caller.
"""
def get_args(self, argset):
"""
Get args and kwargs from the argset.
:param argset:
:return:
"""
args = []
kwargs = {}
for element in argset or []:
if isinstance(element, dict):
kwargs.update(element)
else:
args.append(element)
return args, kwargs
def __call__(self, **kwargs):
"""
Call Ansible module.
:return:
"""
ret = {
"name": kwargs.pop("name"),
"changes": {},
"comment": "",
"result": True,
}
for mod_name, mod_params in kwargs.items():
args, kwargs = self.get_args(mod_params)
try:
ans_mod_out = __salt__[f"ansible.{mod_name}"](
**{"__pub_arg": [args, kwargs]}
)
except Exception as err: # pylint: disable=broad-except
ans_mod_out = 'Module "{}" failed. Error message: ({}) {}'.format(
mod_name, err.__class__.__name__, err
)
ret["result"] = False
ret["changes"][mod_name] = ans_mod_out
return ret
def __virtual__():
"""
Disable, if Ansible is not available around on the Minion.
"""
# pylint: disable=unnecessary-lambda
setattr(sys.modules[__name__], "call", lambda **kwargs: AnsibleState()(**kwargs))
# pylint: enable=unnecessary-lambda
return __virtualname__
def _changes(plays):
"""
Find changes in ansible return data
"""
changes = {}
for play in plays["plays"]:
task_changes = {}
for task in play["tasks"]:
host_changes = {}
for host, data in task["hosts"].items():
if data.get("changed", False) is True:
host_changes[host] = data.get("diff", data.get("changes", {}))
elif any(x in data for x in ["failed", "skipped", "unreachable"]):
host_changes[host] = data.get("results", data.get("msg", {}))
if host_changes:
task_changes[task["task"]["name"]] = host_changes
if task_changes:
changes[play["play"]["name"]] = task_changes
return changes
@salt.utils.decorators.path.which("ansible-playbook")
def playbooks(name, rundir=None, git_repo=None, git_kwargs=None, ansible_kwargs=None):
"""
Run Ansible Playbooks
:param name: path to playbook. This can be relative to rundir or the git repo
:param rundir: location to run ansible-playbook from.
:param git_repo: git repository to clone for ansible playbooks. This is cloned
using the `git.latest` state, and is cloned to the `rundir`
if specified, otherwise it is clone to the `cache_dir`
:param git_kwargs: extra kwargs to pass to `git.latest` state module besides
the `name` and `target`
:param ansible_kwargs: extra kwargs to pass to `ansible.playbooks` execution
module besides the `name` and `target`
:return: Ansible playbook output.
.. code-block:: yaml
run nginx install:
ansible.playbooks:
- name: install.yml
- git_repo: git://github.com/gituser/playbook.git
- git_kwargs:
rev: master
"""
ret = {
"result": False,
"changes": {},
"comment": f"Running playbook {name}",
"name": name,
}
if git_repo:
if not isinstance(rundir, str) or not os.path.isdir(rundir):
with _file_client() as client:
rundir = client._extrn_path(git_repo, "base")
log.trace("rundir set to %s", rundir)
if not isinstance(git_kwargs, dict):
log.debug("Setting git_kwargs to empty dict: %s", git_kwargs)
git_kwargs = {}
__states__["git.latest"](name=git_repo, target=rundir, **git_kwargs)
if not isinstance(ansible_kwargs, dict):
log.debug("Setting ansible_kwargs to empty dict: %s", ansible_kwargs)
ansible_kwargs = {}
if __opts__["test"]:
checks = __salt__["ansible.playbooks"](
name, rundir=rundir, check=True, diff=True, **ansible_kwargs
)
if "stats" not in checks:
ret["comment"] = checks.get("stderr", checks)
ret["result"] = False
ret["changes"] = {}
elif all(
not check["changed"] and not check["failures"] and not check["unreachable"]
for check in checks["stats"].values()
):
ret["comment"] = f"No changes to be made from playbook {name}"
ret["result"] = True
elif any(
check["changed"] and not check["failures"] and not check["unreachable"]
for check in checks["stats"].values()
):
ret["comment"] = f"Changes will be made from playbook {name}"
ret["result"] = None
ret["changes"] = _changes(checks)
else:
ret["comment"] = "There were some issues running the playbook {}".format(
name
)
ret["result"] = False
ret["changes"] = _changes(checks)
else:
results = __salt__["ansible.playbooks"](
name, rundir=rundir, diff=True, **ansible_kwargs
)
if "stats" not in results:
ret["comment"] = results.get("stderr", results)
ret["result"] = False
ret["changes"] = {}
elif all(
not check["changed"] and not check["failures"] and not check["unreachable"]
for check in results["stats"].values()
):
ret["comment"] = f"No changes to be made from playbook {name}"
ret["result"] = True
ret["changes"] = _changes(results)
else:
ret["changes"] = _changes(results)
ret["result"] = all(
not check["failures"] and not check["unreachable"]
for check in results["stats"].values()
)
if ret["result"]:
ret["comment"] = f"Changes were made by playbook {name}"
else:
ret["comment"] = f"There were some issues running the playbook {name}"
return ret
Zerion Mini Shell 1.0