diff --git a/plugins/callback/fail_on_no_hosts.py b/plugins/callback/fail_on_no_hosts.py new file mode 100644 index 00000000..862ae9a4 --- /dev/null +++ b/plugins/callback/fail_on_no_hosts.py @@ -0,0 +1,30 @@ +import sys + +from ansible.plugins.callback import CallbackBase + +DOCUMENTATION = ''' +name: fail_on_no_hosts +callback_type: aggregate +requirements: + - enable in configuration +short_description: Exits with code 1 if no play hosts are matched +version_added: "2.0" +description: + - This callback overrides the default 'v2_playbook_on_no_hosts_matched' method with one that exits instead of just notifying. +''' + +class CallbackModule(CallbackBase): + """ + This callback module exists non-zero if no hosts match + """ + CALLBACK_VERSION = 2.0 + CALLBACK_TYPE = 'aggregate' + CALLBACK_NAME = 'fail_on_no_hosts' + CALLBACK_NEEDS_WHITELIST = False + + def __init__(self): + super(CallbackModule, self).__init__() + + def v2_playbook_on_no_hosts_matched(self): + self._display.display("failed: no hosts matched", color=C.COLOR_ERROR) + sys.exit(1) diff --git a/plugins/vars/sops_vars.py b/plugins/vars/sops_vars.py deleted file mode 100644 index eca3f3b9..00000000 --- a/plugins/vars/sops_vars.py +++ /dev/null @@ -1,163 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright: (c) 2019, Arduino, srl -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -# -############################################# - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -DOCUMENTATION = ''' - vars: sops_vars - author: Edoardo Tenani (@endorama) - version_added: "2.10" - short_description: Loading sops-encrypted vars files - description: - - Load encrypted YAML files into correspondind groups/hosts in group_vars/ and host_vars/ directories. - - Files are encrypted prior to reading, making this plugin an effective companion to host_group_vars plugin. - - Files are restricted to .sops.yaml, .sops.yml, .sops.json extensions. - - Hidden files are ignored. - options: - _valid_extensions: - default: [".sops.yml", ".sops.yaml", ".sops.json"] - description: - - "Check all of these extensions when looking for 'variable' files which should be YAML or JSON or vaulted versions of these." - - 'This affects vars_files, include_vars, inventory and vars plugins among others.' - type: list -''' - -import os -from ansible import constants as C -from ansible.errors import AnsibleParserError -from ansible.module_utils._text import to_bytes, to_native, to_text -from ansible.plugins.vars import BaseVarsPlugin -from ansible.inventory.host import Host -from ansible.inventory.group import Group -from ansible.utils.vars import combine_vars -from ansible.errors import AnsibleError -from subprocess import Popen, PIPE -from ansible.utils.display import Display -display = Display() - -FOUND = {} -DEFAULT_VALID_EXTENSIONS = [".sops.yaml", ".sops.yml", ".sops.json"] - -# From https://github.com/mozilla/sops/blob/master/cmd/sops/codes/codes.go -# Should be manually updated -sops_error_codes = { - 1: "SopsErrorGeneric", - 2: "SopsCouldNotReadInputFile", - 3: "SopsCouldNotWriteOutputFile", - 4: "SopsErrorDumpingTree", - 5: "SopsErrorReadingConfig", - 6: "SopsErrorInvalidKMSEncryptionContextFormat", - 7: "SopsErrorInvalidSetFormat", - 8: "SopsErrorConflictingParameters", - 21: "SopsErrorEncryptingMac", - 23: "SopsErrorEncryptingTree", - 24: "SopsErrorDecryptingMac", - 25: "SopsErrorDecryptingTree", - 49: "SopsCannotChangeKeysFromNonExistentFile", - 51: "SopsMacMismatch", - 52: "SopsMacNotFound", - 61: "SopsConfigFileNotFound", - 85: "SopsKeyboardInterrupt", - 91: "SopsInvalidTreePathFormat", - 100: "SopsNoFileSpecified", - 128: "SopsCouldNotRetrieveKey", - 111: "SopsNoEncryptionKeyFound", - 200: "SopsFileHasNotBeenModified", - 201: "SopsNoEditorFound", - 202: "SopsFailedToCompareVersions", - 203: "SopsFileAlreadyEncrypted" -} - - -class SopsError(AnsibleError): - ''' extend AnsibleError class with sops specific informations ''' - - def __init__(self, filename, exit_code, message,): - exception_name = sops_error_codes[exit_code] - message = "error with file %s: %s exited with code %d: %s" % (filename, exception_name, exit_code, message) - super(SopsError, self).__init__(message=message) - - -def decrypt_with_sops(filename): - display.vvvv(u"sops --decrypt %s" % filename) - - # Run sops directly as python module is deprecated - process = Popen(["sops", "--decrypt", filename], stdout=PIPE, stderr=PIPE) - (output, err) = process.communicate() - exit_code = process.wait() - - # DO NOT display output - # is the decrypted secret and would easily end in logs :) - # if output: - # display.vvvv(output) - - # sops logs always to stderr ( stdout is used for file content ) - if err: - display.vvvv(err) - - if exit_code > 0: - if exit_code in sops_error_codes.keys(): - raise SopsError(filename, exit_code, err) - else: - raise AnsibleError(message=err) - - return output - - -class VarsModule(BaseVarsPlugin): - - def get_vars(self, loader, path, entities, cache=True): - ''' parses the inventory file ''' - - if not isinstance(entities, list): - entities = [entities] - - super(VarsModule, self).get_vars(loader, path, entities) - - data = {} - for entity in entities: - if isinstance(entity, Host): - subdir = 'host_vars' - elif isinstance(entity, Group): - subdir = 'group_vars' - else: - raise AnsibleParserError("Supplied entity must be Host or Group, got %s instead" % (type(entity))) - - # avoid 'chroot' type inventory hostnames /path/to/chroot - if not entity.name.startswith(os.path.sep): - try: - found_files = [] - # load vars - b_opath = os.path.realpath(to_bytes(os.path.join(self._basedir, subdir))) - opath = to_text(b_opath) - key = '%s.%s' % (entity.name, opath) - self._display.vvvv("key: %s" % (key)) - if cache and key in FOUND: - found_files = FOUND[key] - else: - # no need to do much if path does not exist for basedir - if os.path.exists(b_opath): - if os.path.isdir(b_opath): - self._display.debug("\tprocessing dir %s" % opath) - found_files = loader.find_vars_files(opath, entity.name) - found_files = [file_path for file_path in found_files - if any(file_path.endswith(extension) for extension in DEFAULT_VALID_EXTENSIONS)] - FOUND[key] = found_files - else: - self._display.warning("Found %s that is not a directory, skipping: %s" % (subdir, opath)) - - for found in found_files: - file_content = decrypt_with_sops(found) - new_data = loader.load(file_content) - if new_data: # ignore empty files - data = combine_vars(data, new_data) - - except Exception as e: - raise AnsibleParserError(to_native(e)) - - return data