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/promise-types/ini/README.md b/promise-types/ini/README.md new file mode 100644 index 0000000..1cc02b1 --- /dev/null +++ b/promise-types/ini/README.md @@ -0,0 +1,29 @@ +# Ansible INI promise type + +## Synopsis + +* *Name*: `Ansible INI - Custom Promise Module` +* *Version*: `0.0.1` +* *Description*: Manage TOML configuration files through the Ansible INI module in CFEngine. + +## Requirements + +* Python installed on the system +* `ansible-core` pip package + +## 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.toml" + 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..fe7f2f9 --- /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..3e1f2a7 --- /dev/null +++ b/promise-types/ini/example.cf @@ -0,0 +1,69 @@ +promise agent ini +# @brief Define ini promise type +{ + path => "$(sys.workdir)/modules/promises/ini.py"; + interpreter => "/usr/bin/python3"; +} + +body common control { + bundlesequence => {"dependencies", "main"}; +} + +body perms m_rxdirs( mode, rxdirs ) +{ + mode => "$(mode)"; + rxdirs => "$(rxdirs)"; +} + +bundle agent dependencies { + vars: + + "options_str" string => ' +{ + "url.max_content": 1048576, + "url.verbose": 0 +} '; + "options" data => parsejson($(options_str)); + "url" string => "https://raw.githubusercontent.com/ansible-collections/community.general/9946f758af5fe0fe41f1cf7584d670ca1d6c2d52/plugins/modules/ini_file.py"; + "ansible_ini_file" data => url_get($(url), options); + + classes: + + "got_ini_file" expression => "$(ansible_ini_file[success])"; + + + files: + + got_ini_file:: + "/tmp/cfe/ini_file.py" + content => "$(ansible_ini_file[content])"; + + + got_ini_file:: + "/tmp/cfe/ini_file.py" + perms => m_rxdirs( "0755", "true" ); + + reports: + + got_ini_file:: + "$(this.bundle): Successfully retrieved the Ansible INI policy file"; + + !got_ini_file:: + "$(this.bundle): failed to get the Ansible INI policy file $(ansible_ini_file[error_message])"; + +} + +bundle agent main +{ + + meta: + "bundle_version" string => "0.0.1"; + "promise_type" string => "ini"; + + ini: + "/tmp/ini/test.toml" + module_path => "/tmp/cfe/ini_file.py", + 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..8379f63 --- /dev/null +++ b/promise-types/ini/ini.py @@ -0,0 +1,98 @@ +"""The ultimate CFEngine custom promise module""" + +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") + + # AnsiballINIModule specific (the path from the policy) + self.add_attribute("module_path", str) + + 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 an Ansible attributes + if not attributes.get("module_path"): + self.log_error("'module_path' is a required promise attribute") + return (result.NOT_KEPT, []) + + return True + + def validate_promise(self, promiser: str, attributes: dict, meta: dict) -> None: + self.log_debug( + "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_debug( + "Evaluating the ansible ini promise %s, %s, %s" + % (promiser, attributes, meta) + ) + + # 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) + + 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("Ansible INI returned msg: %s" % 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()