From a7c0dfca6775cb8f725c17eb0dcc84c321530951 Mon Sep 17 00:00:00 2001 From: Michael Sekania Date: Sat, 27 Jan 2024 21:29:37 +0100 Subject: [PATCH 01/14] extend: fuctionality --- plugins/modules/folder.py | 538 +++++++++++++++++++++++--------------- 1 file changed, 321 insertions(+), 217 deletions(-) diff --git a/plugins/modules/folder.py b/plugins/modules/folder.py index 5de4fe787..9e4b99e62 100644 --- a/plugins/modules/folder.py +++ b/plugins/modules/folder.py @@ -38,20 +38,28 @@ description: - The attributes of your folder as described in the API documentation. B(Attention! This option OVERWRITES all existing attributes!) + As of Check MK v2.2.0p7 and v2.3.0b1, simultaneous use of I(attributes), + I(remove_attributes), and I(update_attributes) is no longer supported. type: raw - default: {} + required: false update_attributes: description: - The update_attributes of your host as described in the API documentation. This will only update the given attributes. + As of Check MK v2.2.0p7 and v2.3.0b1, simultaneous use of I(attributes), + I(remove_attributes), and I(update_attributes) is no longer supported. type: raw - default: {} + required: false remove_attributes: description: - The remove_attributes of your host as described in the API documentation. + B(If a list of strings is supplied, the listed attributes are removed.) + B(If instead a dict is supplied, the attributes {key: value} that exactly match the passed attributes are removed.) This will only remove the given attributes. + As of Check MK v2.2.0p7 and v2.3.0b1, simultaneous use of I(attributes), + I(remove_attributes), and I(update_attributes) is no longer supported. type: raw - default: [] + required: false state: description: The state of your folder. type: str @@ -68,10 +76,10 @@ # Create a single folder. - name: "Create a single folder." checkmk.general.folder: - server_url: "http://localhost/" + server_url: "http://my_server/" site: "my_site" - automation_user: "automation" - automation_secret: "$SECRET" + automation_user: "my_user" + automation_secret: "my_secret" path: "/my_folder" name: "My Folder" state: "present" @@ -79,10 +87,10 @@ # Create a folder who's hosts should be hosted on a remote site. - name: "Create a single folder." checkmk.general.folder: - server_url: "http://localhost/" + server_url: "http://my_server/" site: "my_site" - automation_user: "automation" - automation_secret: "$SECRET" + automation_user: "my_user" + automation_secret: "my_secret" path: "/my_remote_folder" name: "My Remote Folder" attributes: @@ -92,10 +100,10 @@ # Create a folder with Criticality set to a Test system and Networking Segment WAN (high latency)" - name: "Create a folder with tag_criticality test and tag_networking wan" checkmk.general.folder: - server_url: "http://localhost/" + server_url: "http://my_server/" site: "my_site" - automation_user: "automation" - automation_secret: "$SECRET" + automation_user: "my_user" + automation_secret: "my_secret" path: "/my_remote_folder" attributes: tag_criticality: "test" @@ -105,10 +113,10 @@ # Update only specified attributes - name: "Update only specified attributes" checkmk.general.folder: - server_url: "http://localhost/" + server_url: "http://my_server/" site: "my_site" - automation_user: "automation" - automation_secret: "$SECRET" + automation_user: "my_user" + automation_secret: "my_secret" path: "/my_folder" update_attributes: tag_networking: "dmz" @@ -117,10 +125,10 @@ # Remove specified attributes - name: "Remove specified attributes" checkmk.general.folder: - server_url: "http://localhost/" + server_url: "http://my_server/" site: "my_site" - automation_user: "automation" - automation_secret: "$SECRET" + automation_user: "my_user" + automation_secret: "my_secret" path: "/my_folder" remove_attributes: - tag_networking @@ -141,13 +149,22 @@ # https://docs.ansible.com/ansible/latest/dev_guide/testing/sanity/import.html from ansible.module_utils.basic import AnsibleModule, missing_required_lib -from ansible.module_utils.common.dict_transformations import dict_merge -from ansible.module_utils.urls import fetch_url +from ansible.module_utils.common.dict_transformations import dict_merge, recursive_diff +from ansible_collections.checkmk.general.plugins.module_utils.api import CheckmkAPI +from ansible_collections.checkmk.general.plugins.module_utils.types import RESULT +from ansible_collections.checkmk.general.plugins.module_utils.utils import ( + result_as_dict, +) +from ansible_collections.checkmk.general.plugins.module_utils.version import ( + CheckmkVersion, +) + +PYTHON_VERSION = 3 +HAS_PATHLIB2_LIBRARY = True +PATHLIB2_LIBRARY_IMPORT_ERROR = None if sys.version[0] == "3": from pathlib import Path - - PYTHON_VERSION = 3 else: PYTHON_VERSION = 2 try: @@ -155,139 +172,294 @@ except ImportError: HAS_PATHLIB2_LIBRARY = False PATHLIB2_LIBRARY_IMPORT_ERROR = traceback.format_exc() - else: - HAS_PATHLIB2_LIBRARY = True - PATHLIB2_LIBRARY_IMPORT_ERROR = None +FOLDER = ( + "customer", + "attributes", + "update_attributes", + "remove_attributes", +) -def exit_failed(module, msg): - result = {"msg": msg, "changed": False, "failed": True} - module.fail_json(**result) +class FolderHTTPCodes: + # http_code: (changed, failed, "Message") + get = { + 200: (False, False, "Folder found, nothing changed"), + 404: (False, False, "Folder not found"), + } -def exit_changed(module, msg): - result = {"msg": msg, "changed": True, "failed": False} - module.exit_json(**result) + create = {200: (True, False, "Folder created")} + edit = {200: (True, False, "Folder modified")} + delete = {204: (True, False, "Folder deleted")} -def exit_ok(module, msg): - result = {"msg": msg, "changed": False, "failed": False} - module.exit_json(**result) +class FolderEndpoints: + default = "/objects/folder_config" + create = "/domain-types/folder_config/collections/all" -def cleanup_path(path): - p = Path(path) - if not p.is_absolute(): - p = Path("/").joinpath(p) - return str(p.parent).lower(), p.name +class FolderAPI(CheckmkAPI): + def __init__(self, module): + super().__init__(module) + self.desired = {} -def path_for_url(module): - return module.params["path"].replace("/", "~") + (self.desired["parent"], self.desired["name"]) = _normalize_path( + self.params.get("path") + ) + self.desired["title"] = self.params.get("title", self.desired["name"]) + + for key in FOLDER: + if self.params.get(key): + self.desired[key] = self.params.get(key) + + # Get the current folder from the API and set some parameters + self._get_current() + self._changed_items = self._detect_changes() + + self._verify_compatibility() + + def _verify_compatibility(self): + # Check if parameters are compatible with CMK version + if ( + sum( + [ + 1 + for el in ["attributes", "remove_attributes", "update_attributes"] + if self.module.params.get(el) + ] + ) + > 1 + ): + ver = self.getversion() + msg = ( + "As of Check MK v2.2.0p7 and v2.3.0b1, simultaneous use of" + " attributes, remove_attributes, and update_attributes is no longer supported." + ) -def get_current_folder_state(module, base_url, headers): - current_state = "unknown" - current_explicit_attributes = {} - current_title = "" - etag = "" + if ver >= CheckmkVersion("2.2.0p7"): + result = RESULT( + http_code=0, + msg=msg, + content="", + etag="", + failed=True, + changed=False, + ) + self.module.exit_json(**result_as_dict(result)) + else: + self.module.warn(msg) + + @staticmethod + def _normalize_path(path): + p = Path(path) + if not p.is_absolute(): + p = Path("/").joinpath(p) + return str(p.parent).lower(), p.name + + @staticmethod + def _urlize_path(path): + return path.replace("/", "~").replace("~~", "~") + + def _build_default_endpoint(self): + return "%s/%s" % ( + FolderEndpoints.default, + _urlize_path("%s/%s" % (self.desired["parent"], self.desired["name"])), + ) - api_endpoint = "/objects/folder_config/" + path_for_url(module) - parameters = "?show_hosts=false" - url = base_url + api_endpoint + parameters + def _detect_changes(self): + current_attributes = self.current.get("attributes", {}) + desired_attributes = self.desired.copy() + changes = [] - response, info = fetch_url(module, url, data=None, headers=headers, method="GET") + if desired_attributes.get("update_attributes"): + merged_attributes = dict_merge( + current_attributes, desired_attributes.get("update_attributes") + ) - if info["status"] == 200: - body = json.loads(response.read()) - current_state = "present" - etag = info.get("etag", "") - extensions = body.get("extensions", {}) - current_explicit_attributes = extensions.get("attributes", {}) - current_title = "%s" % body.get("title", "") - if "meta_data" in current_explicit_attributes: - del current_explicit_attributes["meta_data"] + if merged_attributes != current_attributes: + try: + (_, m_c) = recursive_diff(current_attributes, merged_attributes) + changes.append("update attributes: %" % json.dumps(m_c)) + except Exception as e: + changes.append("update attributes") + desired_attributes["update_attributes"] = merged_attributes + + if desired_attributes.get( + "attributes" + ) and current_attributes != desired_attributes.get("attributes"): + changes.append("attributes") + + if self.current.get("title") != desired_attributes.get("title"): + changes.append("title") + + if desired_attributes.get("remove_attributes"): + tmp_remove_attributes = desired_attributes.get("remove_attributes") + if isinstance(tmp_remove_attributes, list): + removes_which = [a for a in tmp_remove_attributes if current_attributes.get(a)] + if len(removes_which) > 0: + changes.append("remove attributes: %s" % " ".join(removes_which) ) + elif isinstance(tmp_remove_attributes, dict): + try: + (c_m, _) = recursive_diff(current_attributes, tmp_remove_attributes) + (c_c_m, _) = recursive_diff(current_attributes, c_m) + if c_c_m: + changes.append("remove attributes: %" % json.dumps(c_c_m)) + self.desired.pop("remove_attributes") + self.desired["retained_attributes"] = c_m + except Exception as e: + module.fail_json( + msg="ERROR: incompatible parameter: remove_attributes!", + exception=e, + ) + else: + module.fail_json( + msg="ERROR: The parameter remove_attributes can be a list of strings or a dictionary!", + exception=e, + ) - elif info["status"] == 404: - current_state = "absent" + return changes - else: - exit_failed( - module, - "Error calling API. HTTP code %d. Details: %s." - % (info["status"], info.get("body", "N/A")), + def _get_current(self): + result = self._fetch( + code_mapping=FolderHTTPCodes.get, + endpoint=self._build_default_endpoint(), + method="GET", ) - return current_state, current_explicit_attributes, current_title, etag + if result.http_code == 200: + self.state = "present" + content = json.loads(result.content) -def set_folder_attributes(module, attributes, base_url, headers, params): - api_endpoint = "/objects/folder_config/" + path_for_url(module) - url = base_url + api_endpoint + self.current["title"] = content["title"] - response, info = fetch_url( - module, url, module.jsonify(params), headers=headers, method="PUT" - ) + extensions = content["extensions"] + for key, value in extensions.items(): + if key == "attributes": + value.pop("meta_data") + self.current[key] = value - if ( - info["status"] == 400 - and params.get("remove_attributes") - and not params.get("title") - and not params.get("attributes") - and not params.get("update_attributes") - ): - # "Folder attributes allready removed." - return False - elif info["status"] != 200: - exit_failed( - module, - "Error calling API. HTTP code %d. Details: %s, " - % (info["status"], info["body"]), + self.etag = result.etag + + else: + self.state = "absent" + + def _check_output(self, mode): + return RESULT( + http_code=0, + msg="Running in check mode. Would have done an %s" % mode, + content="", + etag="", + failed=False, + changed=False, ) - return True + def needs_update(self): + return len(self._changed_items) > 0 + def needs_reduction(self): + return ("retained_attributes" in self.desired) -def create_folder(module, attributes, base_url, headers): - parent, foldername = cleanup_path(module.params["path"]) - name = module.params.get("name", foldername) + def create(self): + data = self.desired.copy() + if not data.get("attributes"): + data["attributes"] = data.pop("update_attributes", {}) - api_endpoint = "/domain-types/folder_config/collections/all" - params = { - "name": foldername, - "title": name, - "parent": parent, - "attributes": attributes, - } - url = base_url + api_endpoint + if data.get("remove_attributes"): + data.pop("remove_attributes") - response, info = fetch_url( - module, url, module.jsonify(params), headers=headers, method="POST" - ) + if data.get("retained_attributes"): + data.pop("retained_attributes") - if info["status"] != 200: - exit_failed( - module, - "Error calling API. HTTP code %d. Details: %s, " - % (info["status"], info["body"]), + if self.module.check_mode: + return self._check_output("create") + + result = self._fetch( + code_mapping=FolderHTTPCodes.create, + endpoint=FolderEndpoints.create, + data=data, + method="POST", ) + return result + + def edit(self): + data = self.desired.copy() + data.pop("name") + data.pop("parent") + self.headers["if-Match"] = self.etag -def delete_folder(module, base_url, headers): - api_endpoint = "/objects/folder_config/" + path_for_url(module) - url = base_url + api_endpoint + if data.get("retained_attributes"): + data.pop("retained_attributes") - response, info = fetch_url(module, url, data=None, headers=headers, method="DELETE") + if self.module.check_mode: + return self._check_output("edit") - if info["status"] != 204: - exit_failed( - module, - "Error calling API. HTTP code %d. Details: %s, " - % (info["status"], info["body"]), + result = self._fetch( + code_mapping=FolderHTTPCodes.edit, + endpoint=self._build_default_endpoint(), + data=data, + method="PUT", + ) + + return result._replace( + msg=result.msg + ". Changed: %s" % ", ".join(self._changed_items) + ) + + def reduct(self): + data = self.desired.copy() + + if data.get("attributes"): + data.pop("attributes") + + if data.get("update_attributes"): + data.pop("remove_attributes") + + if data.get("remove_attributes"): + data.pop("remove_attributes") + + if self.module.check_mode: + return self._check_output("reduct (remove_attributes supplied by dict object)") + + result = self._fetch( + code_mapping=FolderHTTPCodes.create, + endpoint=FolderEndpoints.create, + data=data, + method="POST", + ) + + return result._replace( + msg=result.msg + ". Changed: %s" % ", ".join(self._changed_items) + ) + + def delete(self): + if self.module.check_mode: + return self._check_output("delete") + + result = self._fetch( + code_mapping=FolderHTTPCodes.delete, + endpoint=self._build_default_endpoint(), + method="DELETE", + ) + + return result + + +def _exit_if_missing_pathlib(module): + # Handle library import error according to the following link: + # https://docs.ansible.com/ansible/latest/dev_guide/testing/sanity/import.html + if PYTHON_VERSION == 2 and not HAS_PATHLIB2_LIBRARY: + # Needs: from ansible.module_utils.basic import missing_required_lib + module.fail_json( + msg=missing_required_lib("pathlib2"), + exception=PATHLIB2_LIBRARY_IMPORT_ERROR, ) def run_module(): + # define available arguments/parameters a user can pass to the module module_args = dict( server_url=dict(type="str", required=True), site=dict(type="str", required=True), @@ -300,116 +472,48 @@ def run_module(): required=False, aliases=["title"], ), - attributes=dict(type="raw", default={}), - remove_attributes=dict(type="raw", default=[]), - update_attributes=dict(type="raw", default={}), - state=dict(type="str", default="present", choices=["present", "absent"]), + attributes=dict(type="raw", required=False), + remove_attributes=dict(type="raw", required=False), + update_attributes=dict(type="raw", required=False), + state=dict( + type="str", required=False, default="present", choices=["present", "absent"] + ), ) module = AnsibleModule(argument_spec=module_args, supports_check_mode=True) - # Handle library import error according to the following link: - # https://docs.ansible.com/ansible/latest/dev_guide/testing/sanity/import.html - if PYTHON_VERSION == 2 and not HAS_PATHLIB2_LIBRARY: - # Needs: from ansible.module_utils.basic import missing_required_lib - module.fail_json( - msg=missing_required_lib("pathlib2"), - exception=PATHLIB2_LIBRARY_IMPORT_ERROR, - ) + _exit_if_missing_pathlib(module) - # Use the parameters to initialize some common variables - headers = { - "Accept": "application/json", - "Content-Type": "application/json", - "Authorization": "Bearer %s %s" - % ( - module.params.get("automation_user", ""), - module.params.get("automation_secret", ""), - ), - } + # Create an API object that contains the current and desired state + current_folder = FolderAPI(module) - base_url = "%s/%s/check_mk/api/1.0" % ( - module.params.get("server_url", ""), - module.params.get("site", ""), + result = RESULT( + http_code=0, + msg="No changes needed.", + content="", + etag="", + failed=False, + changed=False, ) - # Determine desired state and attributes - attributes = module.params.get("attributes", {}) - remove_attributes = module.params.get("remove_attributes", []) - update_attributes = module.params.get("update_attributes", {}) - if attributes == []: - attributes = {} - state = module.params.get("state", "present") - - # Determine the current state of this particular folder - ( - current_state, - current_explicit_attributes, - current_title, - etag, - ) = get_current_folder_state(module, base_url, headers) - - # Handle the folder accordingly to above findings and desired state - if state == "present" and current_state == "present": - headers["If-Match"] = etag - msg_tokens = [] - - merged_attributes = dict_merge(current_explicit_attributes, update_attributes) - - params = {} - changed = False - if module.params["name"] and current_title != module.params["name"]: - params["title"] = module.params.get("name") - changed = True - - if attributes != {} and current_explicit_attributes != attributes: - params["attributes"] = attributes - changed = True - - if update_attributes != {} and current_explicit_attributes != merged_attributes: - params["update_attributes"] = merged_attributes - changed = True - - if remove_attributes != []: - for el in remove_attributes: - if current_explicit_attributes.get(el): - changed = True - break - params["remove_attributes"] = remove_attributes - - if params != {}: - if not module.check_mode: - changed = set_folder_attributes( - module, attributes, base_url, headers, params - ) - - if changed: - msg_tokens.append("Folder attributes updated.") - - if len(msg_tokens) >= 1: - exit_changed(module, " ".join(msg_tokens)) + desired_state = current_folder.params.get("state") + if current_folder.state == "present": + result = result._replace( + msg="Folder already exists with the desired parameters." + ) + if desired_state == "absent": + result = current_folder.delete() else: - exit_ok( - module, "Folder already present. All explicit attributes as desired." - ) - - elif state == "present" and current_state == "absent": - if update_attributes != {} and attributes == {}: - attributes = update_attributes - if not module.check_mode: - create_folder(module, attributes, base_url, headers) - exit_changed(module, "Folder created.") - - elif state == "absent" and current_state == "absent": - exit_ok(module, "Folder already absent.") - - elif state == "absent" and current_state == "present": - if not module.check_mode: - delete_folder(module, base_url, headers) - exit_changed(module, "Folder deleted.") - - else: - exit_failed(module, "Unknown error") + if current_folder.needs_update(): + result = current_folder.edit() + if current_folder.needs_reduction() + result = current_folder.reduct() + elif current_folder.state == "absent": + result = result._replace(msg="Folder already absent.") + if desired_state in ("present"): + result = current_folder.create() + + module.exit_json(**result_as_dict(result)) def main(): From 53548e2ac66cf57adb7c10ec0766cd2b91c01a63 Mon Sep 17 00:00:00 2001 From: Michael Sekania Date: Sat, 27 Jan 2024 21:46:29 +0100 Subject: [PATCH 02/14] fix: --- plugins/modules/folder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/folder.py b/plugins/modules/folder.py index 9e4b99e62..031856454 100644 --- a/plugins/modules/folder.py +++ b/plugins/modules/folder.py @@ -506,7 +506,7 @@ def run_module(): else: if current_folder.needs_update(): result = current_folder.edit() - if current_folder.needs_reduction() + if current_folder.needs_reduction(): result = current_folder.reduct() elif current_folder.state == "absent": result = result._replace(msg="Folder already absent.") From a5368adc05896de7c90f3f08b34a02edd0373b77 Mon Sep 17 00:00:00 2001 From: Michael Sekania Date: Sat, 27 Jan 2024 21:49:45 +0100 Subject: [PATCH 03/14] fix: style --- plugins/modules/folder.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/plugins/modules/folder.py b/plugins/modules/folder.py index 031856454..e27ca7e5b 100644 --- a/plugins/modules/folder.py +++ b/plugins/modules/folder.py @@ -297,9 +297,11 @@ def _detect_changes(self): if desired_attributes.get("remove_attributes"): tmp_remove_attributes = desired_attributes.get("remove_attributes") if isinstance(tmp_remove_attributes, list): - removes_which = [a for a in tmp_remove_attributes if current_attributes.get(a)] + removes_which = [ + a for a in tmp_remove_attributes if current_attributes.get(a) + ] if len(removes_which) > 0: - changes.append("remove attributes: %s" % " ".join(removes_which) ) + changes.append("remove attributes: %s" % " ".join(removes_which)) elif isinstance(tmp_remove_attributes, dict): try: (c_m, _) = recursive_diff(current_attributes, tmp_remove_attributes) @@ -360,7 +362,7 @@ def needs_update(self): return len(self._changed_items) > 0 def needs_reduction(self): - return ("retained_attributes" in self.desired) + return "retained_attributes" in self.desired def create(self): data = self.desired.copy() @@ -421,7 +423,9 @@ def reduct(self): data.pop("remove_attributes") if self.module.check_mode: - return self._check_output("reduct (remove_attributes supplied by dict object)") + return self._check_output( + "reduct (remove_attributes supplied by dict object)" + ) result = self._fetch( code_mapping=FolderHTTPCodes.create, From 1f864bdf418f458050925b45bd75f6fc62e8a253 Mon Sep 17 00:00:00 2001 From: Michael Sekania Date: Sat, 27 Jan 2024 21:54:41 +0100 Subject: [PATCH 04/14] fix: linting --- plugins/modules/folder.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/plugins/modules/folder.py b/plugins/modules/folder.py index e27ca7e5b..5cf92c3fe 100644 --- a/plugins/modules/folder.py +++ b/plugins/modules/folder.py @@ -54,7 +54,8 @@ description: - The remove_attributes of your host as described in the API documentation. B(If a list of strings is supplied, the listed attributes are removed.) - B(If instead a dict is supplied, the attributes {key: value} that exactly match the passed attributes are removed.) + B(If instead a dict is supplied, the attributes (key: value) that + exactly match the passed attributes are removed.) This will only remove the given attributes. As of Check MK v2.2.0p7 and v2.3.0b1, simultaneous use of I(attributes), I(remove_attributes), and I(update_attributes) is no longer supported. @@ -204,7 +205,7 @@ def __init__(self, module): self.desired = {} - (self.desired["parent"], self.desired["name"]) = _normalize_path( + (self.desired["parent"], self.desired["name"]) = self._normalize_path( self.params.get("path") ) self.desired["title"] = self.params.get("title", self.desired["name"]) @@ -251,21 +252,19 @@ def _verify_compatibility(self): else: self.module.warn(msg) - @staticmethod - def _normalize_path(path): + def _normalize_path(self, path): p = Path(path) if not p.is_absolute(): p = Path("/").joinpath(p) return str(p.parent).lower(), p.name - @staticmethod - def _urlize_path(path): + def _urlize_path(self, path): return path.replace("/", "~").replace("~~", "~") def _build_default_endpoint(self): return "%s/%s" % ( FolderEndpoints.default, - _urlize_path("%s/%s" % (self.desired["parent"], self.desired["name"])), + self._urlize_path("%s/%s" % (self.desired["parent"], self.desired["name"])), ) def _detect_changes(self): @@ -280,7 +279,7 @@ def _detect_changes(self): if merged_attributes != current_attributes: try: - (_, m_c) = recursive_diff(current_attributes, merged_attributes) + (c_m, m_c) = recursive_diff(current_attributes, merged_attributes) changes.append("update attributes: %" % json.dumps(m_c)) except Exception as e: changes.append("update attributes") @@ -304,8 +303,8 @@ def _detect_changes(self): changes.append("remove attributes: %s" % " ".join(removes_which)) elif isinstance(tmp_remove_attributes, dict): try: - (c_m, _) = recursive_diff(current_attributes, tmp_remove_attributes) - (c_c_m, _) = recursive_diff(current_attributes, c_m) + (c_m, m_c) = recursive_diff(current_attributes, tmp_remove_attributes) + (c_c_m, c_m_c) = recursive_diff(current_attributes, c_m) if c_c_m: changes.append("remove attributes: %" % json.dumps(c_c_m)) self.desired.pop("remove_attributes") From d51703ea21ef46d9f92d4513904c340c0e8c6661 Mon Sep 17 00:00:00 2001 From: Michael Sekania Date: Sat, 27 Jan 2024 21:58:21 +0100 Subject: [PATCH 05/14] fix: --- plugins/modules/folder.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/plugins/modules/folder.py b/plugins/modules/folder.py index 5cf92c3fe..aea50ba6f 100644 --- a/plugins/modules/folder.py +++ b/plugins/modules/folder.py @@ -280,7 +280,7 @@ def _detect_changes(self): if merged_attributes != current_attributes: try: (c_m, m_c) = recursive_diff(current_attributes, merged_attributes) - changes.append("update attributes: %" % json.dumps(m_c)) + changes.append("update attributes: %s" % json.dumps(m_c)) except Exception as e: changes.append("update attributes") desired_attributes["update_attributes"] = merged_attributes @@ -303,10 +303,12 @@ def _detect_changes(self): changes.append("remove attributes: %s" % " ".join(removes_which)) elif isinstance(tmp_remove_attributes, dict): try: - (c_m, m_c) = recursive_diff(current_attributes, tmp_remove_attributes) + (c_m, m_c) = recursive_diff( + current_attributes, tmp_remove_attributes + ) (c_c_m, c_m_c) = recursive_diff(current_attributes, c_m) if c_c_m: - changes.append("remove attributes: %" % json.dumps(c_c_m)) + changes.append("remove attributes: %s" % json.dumps(c_c_m)) self.desired.pop("remove_attributes") self.desired["retained_attributes"] = c_m except Exception as e: From d81428bc144f2745ac155a8bab7a592a21a19d0b Mon Sep 17 00:00:00 2001 From: Michael Sekania Date: Sat, 27 Jan 2024 22:00:59 +0100 Subject: [PATCH 06/14] fix: --- plugins/modules/folder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/folder.py b/plugins/modules/folder.py index aea50ba6f..372429e2c 100644 --- a/plugins/modules/folder.py +++ b/plugins/modules/folder.py @@ -54,8 +54,8 @@ description: - The remove_attributes of your host as described in the API documentation. B(If a list of strings is supplied, the listed attributes are removed.) - B(If instead a dict is supplied, the attributes (key: value) that - exactly match the passed attributes are removed.) + B(If instead a dict is supplied, the attributes that exactly match + the passed attributes are removed.) This will only remove the given attributes. As of Check MK v2.2.0p7 and v2.3.0b1, simultaneous use of I(attributes), I(remove_attributes), and I(update_attributes) is no longer supported. From c16f50c93afe5f2749f29927279f88dbdc894be4 Mon Sep 17 00:00:00 2001 From: Michael Sekania Date: Sat, 27 Jan 2024 22:24:13 +0100 Subject: [PATCH 07/14] fix: --- plugins/modules/folder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/folder.py b/plugins/modules/folder.py index 372429e2c..907b55640 100644 --- a/plugins/modules/folder.py +++ b/plugins/modules/folder.py @@ -312,12 +312,12 @@ def _detect_changes(self): self.desired.pop("remove_attributes") self.desired["retained_attributes"] = c_m except Exception as e: - module.fail_json( + self.module.fail_json( msg="ERROR: incompatible parameter: remove_attributes!", exception=e, ) else: - module.fail_json( + self.module.fail_json( msg="ERROR: The parameter remove_attributes can be a list of strings or a dictionary!", exception=e, ) From bb6f5df85c0f39889ab4c9065f9337304a1c2e9c Mon Sep 17 00:00:00 2001 From: Michael Sekania Date: Wed, 31 Jan 2024 00:48:15 +0100 Subject: [PATCH 08/14] fix: tested --- plugins/modules/folder.py | 96 ++++++++++++++++++--------------------- 1 file changed, 44 insertions(+), 52 deletions(-) diff --git a/plugins/modules/folder.py b/plugins/modules/folder.py index 907b55640..3d5524f64 100644 --- a/plugins/modules/folder.py +++ b/plugins/modules/folder.py @@ -54,7 +54,7 @@ description: - The remove_attributes of your host as described in the API documentation. B(If a list of strings is supplied, the listed attributes are removed.) - B(If instead a dict is supplied, the attributes that exactly match + B(If extended_functionality and a dict is supplied, the attributes that exactly match the passed attributes are removed.) This will only remove the given attributes. As of Check MK v2.2.0p7 and v2.3.0b1, simultaneous use of I(attributes), @@ -66,6 +66,10 @@ type: str default: present choices: [present, absent] + extended_functionality: + description: The state of your folder. + type: bool + default: true author: - Robin Gierse (@robin-checkmk) @@ -203,6 +207,8 @@ class FolderAPI(CheckmkAPI): def __init__(self, module): super().__init__(module) + self.extended_functionality = self.params.get("extended_functionality", True) + self.desired = {} (self.desired["parent"], self.desired["name"]) = self._normalize_path( @@ -295,6 +301,7 @@ def _detect_changes(self): if desired_attributes.get("remove_attributes"): tmp_remove_attributes = desired_attributes.get("remove_attributes") + if isinstance(tmp_remove_attributes, list): removes_which = [ a for a in tmp_remove_attributes if current_attributes.get(a) @@ -302,26 +309,47 @@ def _detect_changes(self): if len(removes_which) > 0: changes.append("remove attributes: %s" % " ".join(removes_which)) elif isinstance(tmp_remove_attributes, dict): - try: - (c_m, m_c) = recursive_diff( - current_attributes, tmp_remove_attributes - ) - (c_c_m, c_m_c) = recursive_diff(current_attributes, c_m) - if c_c_m: - changes.append("remove attributes: %s" % json.dumps(c_c_m)) - self.desired.pop("remove_attributes") - self.desired["retained_attributes"] = c_m - except Exception as e: + if not self.extended_functionality: self.module.fail_json( - msg="ERROR: incompatible parameter: remove_attributes!", - exception=e, + msg="ERROR: The parameter remove_attributes of dict type is not supported for set paramter extended_functionality: false!", ) + + (tmp_remove, tmp_rest) = (current_attributes, {}) + if current_attributes != tmp_remove_attributes: + try: + (c_m, m_c) = recursive_diff( + current_attributes, tmp_remove_attributes + ) + + if c_m: + # if nothing to remove + if current_attributes == c_m: + (tmp_remove, tmp_rest) = ({}, current_attributes) + else: + (c_c_m, c_m_c) = recursive_diff(current_attributes, c_m) + (tmp_remove, tmp_rest) = (c_c_m, c_m) + except Exception as e: + self.module.fail_json( + msg="ERROR: incompatible parameter: remove_attributes!", + exception=e, + ) + + desired_attributes.pop("remove_attributes") + if tmp_remove != {}: + changes.append("remove attributes: %s" % json.dumps(tmp_remove)) + if tmp_rest != {}: + desired_attributes["update_attributes"] = tmp_rest else: self.module.fail_json( msg="ERROR: The parameter remove_attributes can be a list of strings or a dictionary!", exception=e, ) + if self.extended_functionality: + self.desired = desired_attributes.copy() + + # self.module.fail_json(json.dumps(desired_attributes)) + return changes def _get_current(self): @@ -362,9 +390,6 @@ def _check_output(self, mode): def needs_update(self): return len(self._changed_items) > 0 - def needs_reduction(self): - return "retained_attributes" in self.desired - def create(self): data = self.desired.copy() if not data.get("attributes"): @@ -373,9 +398,6 @@ def create(self): if data.get("remove_attributes"): data.pop("remove_attributes") - if data.get("retained_attributes"): - data.pop("retained_attributes") - if self.module.check_mode: return self._check_output("create") @@ -394,9 +416,6 @@ def edit(self): data.pop("parent") self.headers["if-Match"] = self.etag - if data.get("retained_attributes"): - data.pop("retained_attributes") - if self.module.check_mode: return self._check_output("edit") @@ -411,34 +430,6 @@ def edit(self): msg=result.msg + ". Changed: %s" % ", ".join(self._changed_items) ) - def reduct(self): - data = self.desired.copy() - - if data.get("attributes"): - data.pop("attributes") - - if data.get("update_attributes"): - data.pop("remove_attributes") - - if data.get("remove_attributes"): - data.pop("remove_attributes") - - if self.module.check_mode: - return self._check_output( - "reduct (remove_attributes supplied by dict object)" - ) - - result = self._fetch( - code_mapping=FolderHTTPCodes.create, - endpoint=FolderEndpoints.create, - data=data, - method="POST", - ) - - return result._replace( - msg=result.msg + ". Changed: %s" % ", ".join(self._changed_items) - ) - def delete(self): if self.module.check_mode: return self._check_output("delete") @@ -483,6 +474,9 @@ def run_module(): state=dict( type="str", required=False, default="present", choices=["present", "absent"] ), + extended_functionality=dict( + type="bool", required=False, default=True + ), ) module = AnsibleModule(argument_spec=module_args, supports_check_mode=True) @@ -511,8 +505,6 @@ def run_module(): else: if current_folder.needs_update(): result = current_folder.edit() - if current_folder.needs_reduction(): - result = current_folder.reduct() elif current_folder.state == "absent": result = result._replace(msg="Folder already absent.") if desired_state in ("present"): From 680caf7a60e0475944a5727fdec71028c423ff33 Mon Sep 17 00:00:00 2001 From: Michael Sekania Date: Wed, 31 Jan 2024 00:50:04 +0100 Subject: [PATCH 09/14] fix: QA --- plugins/modules/folder.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/plugins/modules/folder.py b/plugins/modules/folder.py index 3d5524f64..91367b485 100644 --- a/plugins/modules/folder.py +++ b/plugins/modules/folder.py @@ -474,9 +474,7 @@ def run_module(): state=dict( type="str", required=False, default="present", choices=["present", "absent"] ), - extended_functionality=dict( - type="bool", required=False, default=True - ), + extended_functionality=dict(type="bool", required=False, default=True), ) module = AnsibleModule(argument_spec=module_args, supports_check_mode=True) From b7869054a0a4fe213cb3f45168988d1b31c964f2 Mon Sep 17 00:00:00 2001 From: Michael Sekania Date: Wed, 31 Jan 2024 19:04:30 +0100 Subject: [PATCH 10/14] add: parents parse --- plugins/modules/folder.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/plugins/modules/folder.py b/plugins/modules/folder.py index 91367b485..9e241b62e 100644 --- a/plugins/modules/folder.py +++ b/plugins/modules/folder.py @@ -155,6 +155,7 @@ # https://docs.ansible.com/ansible/latest/dev_guide/testing/sanity/import.html from ansible.module_utils.basic import AnsibleModule, missing_required_lib from ansible.module_utils.common.dict_transformations import dict_merge, recursive_diff +from ansible.module_utils.common.validation import check_type_list from ansible_collections.checkmk.general.plugins.module_utils.api import CheckmkAPI from ansible_collections.checkmk.general.plugins.module_utils.types import RESULT from ansible_collections.checkmk.general.plugins.module_utils.utils import ( @@ -185,6 +186,11 @@ "remove_attributes", ) +FOLDER_PARENTS_PARSE = ( + "attributes", + "update_attributes", +) + class FolderHTTPCodes: # http_code: (changed, failed, "Message") @@ -220,6 +226,13 @@ def __init__(self, module): if self.params.get(key): self.desired[key] = self.params.get(key) + for key in FOLDER_PARENTS_PARSE: + if self.desired.get(key): + if self.desired.get(key).get("parents"): + self.desired[key]["parents"] = check_type_list( + self.desired.get(key).get("parents") + ) + # Get the current folder from the API and set some parameters self._get_current() self._changed_items = self._detect_changes() @@ -500,9 +513,8 @@ def run_module(): ) if desired_state == "absent": result = current_folder.delete() - else: - if current_folder.needs_update(): - result = current_folder.edit() + elif current_folder.needs_update(): + result = current_folder.edit() elif current_folder.state == "absent": result = result._replace(msg="Folder already absent.") if desired_state in ("present"): From b4e1aaaabd53a86062be0e092a2e793e5ad1ebe7 Mon Sep 17 00:00:00 2001 From: Michael Sekania Date: Fri, 9 Feb 2024 11:15:27 +0100 Subject: [PATCH 11/14] add: requested changes --- plugins/modules/folder.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/modules/folder.py b/plugins/modules/folder.py index 9e241b62e..55ad7f84d 100644 --- a/plugins/modules/folder.py +++ b/plugins/modules/folder.py @@ -67,7 +67,7 @@ default: present choices: [present, absent] extended_functionality: - description: The state of your folder. + description: Allow extended functionality instead of the expected REST API behavior. type: bool default: true @@ -304,9 +304,7 @@ def _detect_changes(self): changes.append("update attributes") desired_attributes["update_attributes"] = merged_attributes - if desired_attributes.get( - "attributes" - ) and current_attributes != desired_attributes.get("attributes"): + if current_attributes != desired_attributes.get("attributes", {}): changes.append("attributes") if self.current.get("title") != desired_attributes.get("title"): @@ -324,7 +322,7 @@ def _detect_changes(self): elif isinstance(tmp_remove_attributes, dict): if not self.extended_functionality: self.module.fail_json( - msg="ERROR: The parameter remove_attributes of dict type is not supported for set paramter extended_functionality: false!", + msg="ERROR: The parameter remove_attributes of dict type is not supported for the paramter extended_functionality: false!", ) (tmp_remove, tmp_rest) = (current_attributes, {}) @@ -383,6 +381,8 @@ def _get_current(self): for key, value in extensions.items(): if key == "attributes": value.pop("meta_data") + if "network_scan_results" in value: + value.pop("network_scan_results") self.current[key] = value self.etag = result.etag @@ -405,7 +405,7 @@ def needs_update(self): def create(self): data = self.desired.copy() - if not data.get("attributes"): + if data.get("attributes", {}) != {}: data["attributes"] = data.pop("update_attributes", {}) if data.get("remove_attributes"): From fd4141883058a595106fc3f277ec49dc6359ecc4 Mon Sep 17 00:00:00 2001 From: Lars Getwan Date: Fri, 9 Feb 2024 16:09:00 +0100 Subject: [PATCH 12/14] Did some merging magic (actually, all manually...). --- plugins/modules/folder.py | 41 --------------------------------------- 1 file changed, 41 deletions(-) diff --git a/plugins/modules/folder.py b/plugins/modules/folder.py index 07264331f..55ad7f84d 100644 --- a/plugins/modules/folder.py +++ b/plugins/modules/folder.py @@ -169,10 +169,6 @@ HAS_PATHLIB2_LIBRARY = True PATHLIB2_LIBRARY_IMPORT_ERROR = None -PYTHON_VERSION = 3 -HAS_PATHLIB2_LIBRARY = True -PATHLIB2_LIBRARY_IMPORT_ERROR = None - if sys.version[0] == "3": from pathlib import Path else: @@ -471,43 +467,6 @@ def _exit_if_missing_pathlib(module): ) -def get_version_ge_220p7(module, checkmkversion): - if "p" in checkmkversion[2]: - patchlevel = checkmkversion[2].split("p") - patchtype = "p" - elif "a" in checkmkversion[2]: - patchlevel = checkmkversion[2].split("a") - patchtype = "a" - elif "b" in checkmkversion[2]: - patchlevel = checkmkversion[2].split("b") - patchtype = "b" - else: - exit_failed( - module, - "Not supported patch-level schema: %s" % (checkmkversion[2]), - ) - - if ( - int(checkmkversion[0]) > 2 - or (int(checkmkversion[0]) == 2 and int(checkmkversion[1]) > 2) - or ( - int(checkmkversion[0]) == 2 - and int(checkmkversion[1]) == 2 - and int(patchlevel[0]) > 0 - ) - or ( - int(checkmkversion[0]) == 2 - and int(checkmkversion[1]) == 2 - and int(patchlevel[0]) == 0 - and patchtype == "p" - and int(patchlevel[1]) >= 7 - ) - ): - return True - else: - return False - - def run_module(): # define available arguments/parameters a user can pass to the module module_args = dict( From ea6706cc3ceef185f86e3d6b1ec08ef205acfe83 Mon Sep 17 00:00:00 2001 From: Michael Sekania Date: Fri, 9 Feb 2024 16:59:40 +0100 Subject: [PATCH 13/14] fix: bug, idempotency --- plugins/modules/folder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/folder.py b/plugins/modules/folder.py index 55ad7f84d..9d50b28b0 100644 --- a/plugins/modules/folder.py +++ b/plugins/modules/folder.py @@ -304,7 +304,7 @@ def _detect_changes(self): changes.append("update attributes") desired_attributes["update_attributes"] = merged_attributes - if current_attributes != desired_attributes.get("attributes", {}): + if desired_attributes.get("attributes") and current_attributes != desired_attributes.get("attributes"): changes.append("attributes") if self.current.get("title") != desired_attributes.get("title"): From 4e55a628d0e1c95c44cfbb8686644c7e3537302a Mon Sep 17 00:00:00 2001 From: Michael Sekania Date: Fri, 9 Feb 2024 17:02:57 +0100 Subject: [PATCH 14/14] fix: style --- plugins/modules/folder.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/modules/folder.py b/plugins/modules/folder.py index 9d50b28b0..87a5a9945 100644 --- a/plugins/modules/folder.py +++ b/plugins/modules/folder.py @@ -304,7 +304,9 @@ def _detect_changes(self): changes.append("update attributes") desired_attributes["update_attributes"] = merged_attributes - if desired_attributes.get("attributes") and current_attributes != desired_attributes.get("attributes"): + if desired_attributes.get( + "attributes" + ) and current_attributes != desired_attributes.get("attributes"): changes.append("attributes") if self.current.get("title") != desired_attributes.get("title"):