diff --git a/cfbs.json b/cfbs.json index 1beb3bb..19c738a 100644 --- a/cfbs.json +++ b/cfbs.json @@ -180,6 +180,15 @@ "append enable.cf services/init.cf" ] }, + "promise-type-ini": { + "description": "A Custom CFEngine promise module, installing, and using the Ansible INI module to provide an INI promise type for INI files", + "subdirectory": "promise-types/ini", + "dependencies": ["library-for-promise-types-in-python"], + "steps": [ + "copy ini.py modules/promises/", + "append enable.cf services/init.cf" + ] + }, "uninstall-packages": { "description": "Allows you to specify a list of packages you want uninstalled on your hosts.", "subdirectory": "security/uninstall-packages", diff --git a/libraries/python/cfengine.py b/libraries/python/cfengine.py index 8f45447..c3a945e 100644 --- a/libraries/python/cfengine.py +++ b/libraries/python/cfengine.py @@ -384,6 +384,9 @@ def _handle_evaluate(self, promiser, attributes, request): except Exception as e: self.log_critical( "{error_type}: {error}".format(error_type=type(e).__name__, error=e) + + e.message + if hasattr(e, "message") + else "" ) self._add_traceback_to_response() self._result = Result.ERROR diff --git a/promise-types/ini/README.md b/promise-types/ini/README.md new file mode 100644 index 0000000..e6f4da8 --- /dev/null +++ b/promise-types/ini/README.md @@ -0,0 +1,30 @@ +# Ansible INI promise type + +## Synopsis + +* *Name*: `Ansible INI - Custom Promise Module` +* *Version*: `0.0.1` +* *Description*: Manage configuration files through the Ansible INI module in CFEngine. + +## Requirements + +* Python installed on the system +* `ansible` pip package +* Correct path to the `ini_file.py` in the custom promise module + +## Attributes + +See [anible_ini module](https://docs.ansible.com/ansible/latest/collections/community/general/ini_file_module.html). + +## Example + +```cfengine3 +bundle agent main +{ + ini: + "/path/to/file.ini" + section => "foo", + option => "bar", + value => "baz"; +} +``` diff --git a/promise-types/ini/enable.cf b/promise-types/ini/enable.cf new file mode 100644 index 0000000..4b9db9d --- /dev/null +++ b/promise-types/ini/enable.cf @@ -0,0 +1,6 @@ +promise agent ini +# @brief Define ini promise type +{ + path => "$(sys.workdir)/modules/promises/ini.py"; + interpreter => "/usr/bin/python3"; +} diff --git a/promise-types/ini/example.cf b/promise-types/ini/example.cf new file mode 100644 index 0000000..39ff808 --- /dev/null +++ b/promise-types/ini/example.cf @@ -0,0 +1,20 @@ +promise agent ini +# @brief Define ini promise type +{ + path => "$(sys.workdir)/modules/promises/ini.py"; + interpreter => "/usr/bin/python3"; +} + +bundle agent main +{ + + meta: + "bundle_version" string => "0.0.1"; + "promise_type" string => "ini"; + + ini: + "/tmp/ini/test.ini" + section => "foo", + option => "bar", + value => "baz"; +} diff --git a/promise-types/ini/ini.py b/promise-types/ini/ini.py new file mode 100644 index 0000000..de5648a --- /dev/null +++ b/promise-types/ini/ini.py @@ -0,0 +1,105 @@ +"""A CFEngine custom promise module for INI files""" + +import json +import subprocess +import sys + +from cfengine import PromiseModule, ValidationError, Result + + +class AnsiballINIModule(PromiseModule): + def __init__(self): + super().__init__("ansible_ini_promise_module", "0.0.1") + + self.add_attribute("path", str, default_to_promiser=True) + + def validate_attributes(self, promiser, attributes, meta): + # Just pass the attributes on transparently to Ansible INI The Ansible + # module will report if the missing parameters are not in Ansible attributes + + return True + + def validate_promise(self, promiser: str, attributes: dict, meta: dict) -> None: + self.log_error( + "Validating the ansible ini promise: %s %s %s" + % (promiser, attributes, meta) + ) + if not meta.get("promise_type"): + raise ValidationError("Promise type not specified") + + assert meta.get("promise_type") == "ini" + + def evaluate_promise(self, promiser: str, attributes: dict, meta: dict): + self.log_error( + "Evaluating the ansible ini promise %s, %s, %s" + % (promiser, attributes, meta) + ) + + if "module_path" not in attributes: + attributes.setdefault( + "module_path", + "/tmp/ini_file.py", + ) + + # NOTE: INI module specific - should not be passed on to Ansible + module_path = attributes["module_path"] + del attributes["module_path"] + + # NOTE - needed because 'default_to_promiser' is not respected + attributes.setdefault("path", promiser) + + self.log_error( + "Evaluating the ansible ini promise %s, %s, %s" + % (promiser, attributes, meta) + ) + + proc = subprocess.run( + [ + "python", + module_path, + ], + input=json.dumps({"ANSIBLE_MODULE_ARGS": attributes}).encode("utf-8"), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + if not proc: + self.log_error("Failed to run the ansible module") + return ( + Result.NOT_KEPT, + [], + ) + + if proc.returncode != 0: + self.log_error("Failed to run the ansible module") + self.log_error("Ansible INI module returned(stdout): %s" % proc.stdout) + self.log_error("Ansible INI module returned(stderr): %s" % proc.stderr) + return ( + Result.NOT_KEPT, + [], + ) + + self.log_debug("Received output: %s (stdout)" % proc.stdout) + self.log_debug("Received output: (stderr): %s" % proc.stderr) + + try: + d = json.loads(proc.stdout.decode("UTF-8").strip()) + if d.get("changed", False): + self.log_info( + "Edited content of '%s' (%s)" % (promiser, d.get("msg", "")) + ) + except Exception as e: + self.log_error( + "Failed to decode the JSON returned from the Ansible INI module. Error: %s" + % e + ) + return (Result.NOT_KEPT, []) + + return ( + Result.KEPT, + [], + ) + + +if __name__ == "__main__": + AnsiballINIModule().start()