From 503f29de14496e5726ee7b8fd935ed6fb8c95e26 Mon Sep 17 00:00:00 2001 From: Ole Petter Date: Wed, 10 Jan 2024 14:49:30 +0100 Subject: [PATCH] Ansible INI custom promise module wrapper This adds a custom promise module for wrapping the existing Ansible INI module. The module simply passes on values from the custom 'ini' promise type, to the Ansible module. With the example follows an example policy for downloading the required Ansible INI module. Signed-off-by: Ole Petter --- cfbs.json | 10 +++++ promise-types/ini/README.md | 28 ++++++++++++ promise-types/ini/enable.cf | 6 +++ promise-types/ini/example.cf | 79 +++++++++++++++++++++++++++++++++ promise-types/ini/ini.py | 86 ++++++++++++++++++++++++++++++++++++ 5 files changed, 209 insertions(+) create mode 100644 promise-types/ini/README.md create mode 100644 promise-types/ini/enable.cf create mode 100644 promise-types/ini/example.cf create mode 100644 promise-types/ini/ini.py diff --git a/cfbs.json b/cfbs.json index 1beb3bb..2e6bfee 100644 --- a/cfbs.json +++ b/cfbs.json @@ -180,6 +180,16 @@ "append enable.cf services/init.cf" ] }, + "promise-type-ini": { + "name": "CFEngine Ansible INI wrapper", + "type": "module", + "description": "A Custom CFEngine promise module, installing, and using the Ansible INI module to provide an INI promise type for INI files", + "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..095160b --- /dev/null +++ b/promise-types/ini/README.md @@ -0,0 +1,28 @@ +# 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 (?) + +## 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..583c63e --- /dev/null +++ b/promise-types/ini/enable.cf @@ -0,0 +1,6 @@ +promise agent ini +# @brief Define ini promise type +{ + path => "$(sys.workdir)/inputs/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..f6caba1 --- /dev/null +++ b/promise-types/ini/example.cf @@ -0,0 +1,79 @@ +promise agent ini +# @brief Define ini promise type +{ + path => "$(sys.workdir)/inputs/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])"; + + + packages: + + "python" + policy => "present", + package_method => generic, + comment => "Install python"; + + + 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"; + "$(this.bundle): $(ansible_ini_file)"; + + !got_ini_file:: + "$(this.bundle): failed to get the Ansible INI policy file $(ansible_ini_file[error_message])"; + +} + +bundle agent main +{ + + meta: + "tags" slist => { "autorun" }; + "bundle_version" string => "0.0.1"; + "promise_type" string => "ini"; + + ini: + "/home/olepor/cfengine/modules/promise-types/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..c986e57 --- /dev/null +++ b/promise-types/ini/ini.py @@ -0,0 +1,86 @@ +"""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) + return ( + Result.KEPT, + [], + ) + + +if __name__ == "__main__": + AnsiballINIModule().start()