From 0e69c388715d543714966dce9314e6aa226b6a0e Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Wed, 2 Jul 2025 13:40:17 +0200 Subject: [PATCH 1/9] JSON promise type: Reformatted README with Prettier Fixed some small whitespace / formatting mistakes Signed-off-by: Ole Herman Schumacher Elgesem --- promise-types/json/README.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/promise-types/json/README.md b/promise-types/json/README.md index 1e4c820..035e298 100644 --- a/promise-types/json/README.md +++ b/promise-types/json/README.md @@ -2,13 +2,13 @@ Promise type for manipulating `json` files ## Attributes -| Name | Type | Description | -|---------------|-----------------------------------------|------------------------------------------------------------------------------------------------------------------------------------| -| `object` | `data container` | json object type. It can also be json arrays | -| `array` | `data array` | json array type | -| `string` | `string` | json string type | -| `number` | `real`, `int` | json number type | -| `primitive` | `string` | Primitives are values that are either `"true"`, `"false"` or `"null"` in json | +| Name | Type | Description | +| ----------- | ---------------- | ----------------------------------------------------------------------------- | +| `object` | `data container` | json object type. It can also be json arrays | +| `array` | `data array` | json array type | +| `string` | `string` | json string type | +| `number` | `real`, `int` | json number type | +| `primitive` | `string` | Primitives are values that are either `"true"`, `"false"` or `"null"` in json | ## Examples @@ -35,7 +35,8 @@ If the `/tmp/newfile.json` doesn't exist, it will be created. If it exists and c ### Write to a specific field -Given a json file `/tmp/oldfile.json`, +Given a json file `/tmp/oldfile.json`, + ```json { "foo": "bar" @@ -68,15 +69,15 @@ If the field doesn't exist, it is appended. If it already exists, its data will In order to write compound type such as arrays containg booleans, numbers, etc... One has to use the `data` type in the policy. -To see what happens if we use +To see what happens if we use ```cfengine3 bundle agent main { - vars: + vars: "json_data" data => '[1.2, true, "hello!"]'; - + "real_list" rlist => {"1.2", "2.3"}; "bool_list" @@ -85,7 +86,7 @@ bundle agent main json: "/tmp/example_1.json:json_data" array => "@(json_data)"; - + "/tmp/example_2.json:real_list" array => "@(real_list)"; "/tmp/example_2.json:bool_list" @@ -116,7 +117,7 @@ The copy attribute allows to copy the content of a json file into another json f ```json { - "hello": "world" + "hello": "world" } ``` @@ -139,7 +140,6 @@ bundle agent main } ``` - ## Authors This software was created by the team at [Northern.tech](https://northern.tech), with many contributions from the community. From 08cd750994c5bb06e248de8eb0f3762d48fc84a9 Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Wed, 2 Jul 2025 15:07:05 +0200 Subject: [PATCH 2/9] promise-type-json: Formatted with black Signed-off-by: Ole Herman Schumacher Elgesem --- promise-types/json/json_promise_type.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/promise-types/json/json_promise_type.py b/promise-types/json/json_promise_type.py index 2d945d7..918f649 100644 --- a/promise-types/json/json_promise_type.py +++ b/promise-types/json/json_promise_type.py @@ -112,7 +112,9 @@ def evaluate_promise(self, promiser, attributes, metadata): filename, _, field = promiser.partition(":") if os.path.exists(filename) and not os.path.isfile(filename): - self.log_error("'{}' already exists and is not a regular file".format(filename)) + self.log_error( + "'{}' already exists and is not a regular file".format(filename) + ) return Result.NOT_KEPT # type conversion @@ -160,8 +162,12 @@ def evaluate_promise(self, promiser, attributes, metadata): os.close(fd) shutil.move(tmp, filename) - if (written != len(json_bytes)): - self.log_error("Couldn't write all the data to the file '{}'. Wrote {} out of {} bytes".format(filename, written, len(json_bytes))) + if written != len(json_bytes): + self.log_error( + "Couldn't write all the data to the file '{}'. Wrote {} out of {} bytes".format( + filename, written, len(json_bytes) + ) + ) return Result.NOT_KEPT self.log_info("Updated '{}'".format(filename)) From 7d67b93c331ab29340b1351c36e283891feba830 Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Wed, 2 Jul 2025 15:55:20 +0200 Subject: [PATCH 3/9] promise-type-json: Fixed already up to date log message Signed-off-by: Ole Herman Schumacher Elgesem --- promise-types/json/json_promise_type.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/promise-types/json/json_promise_type.py b/promise-types/json/json_promise_type.py index 918f649..32bb4c7 100644 --- a/promise-types/json/json_promise_type.py +++ b/promise-types/json/json_promise_type.py @@ -147,12 +147,12 @@ def evaluate_promise(self, promiser, attributes, metadata): ) if field in content and content[field] == data: - self.log_info("'{}' is already up to date") + self.log_info("'{}' is already up to date".format(promiser)) return Result.KEPT content[field] = data else: if content == data: - self.log_info("'{}' is already up to date") + self.log_info("'{}' is already up to date".format(promiser)) return Result.KEPT content = data From c0adf955f62ea3bfe4733c753e0dac8bc6342de5 Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Wed, 2 Jul 2025 16:01:09 +0200 Subject: [PATCH 4/9] CFEngine module library: Improvements to error handling Signed-off-by: Ole Herman Schumacher Elgesem --- libraries/python/cfengine.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libraries/python/cfengine.py b/libraries/python/cfengine.py index e1dcf2a..a80d6f6 100644 --- a/libraries/python/cfengine.py +++ b/libraries/python/cfengine.py @@ -380,6 +380,8 @@ def _handle_evaluate(self, promiser, attributes, request): try: results = self.evaluate_promise(promiser, attributes, metadata) + assert results is not None # Most likely someone forgot to return something + # evaluate_promise should return either a result or a (result, result_classes) pair if type(results) == str: self._result = results @@ -389,7 +391,9 @@ def _handle_evaluate(self, promiser, attributes, request): self._result_classes = results[1] except Exception as e: self.log_critical( - "{error_type}: {error}".format(error_type=type(e).__name__, error=e) + "{error_type}: {error} (Bug in python promise type module, run with --debug for traceback)".format( + error_type=type(e).__name__, error=e + ) ) self._add_traceback_to_response() self._result = Result.ERROR From cb2d8a914fea38175516c9b418879a6748869887 Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Wed, 2 Jul 2025 16:01:37 +0200 Subject: [PATCH 5/9] CFEngine module library: Formatted with black Signed-off-by: Ole Herman Schumacher Elgesem --- libraries/python/cfengine.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/libraries/python/cfengine.py b/libraries/python/cfengine.py index a80d6f6..e069601 100644 --- a/libraries/python/cfengine.py +++ b/libraries/python/cfengine.py @@ -49,8 +49,9 @@ def _should_send_log(level_set, msg_level): # for auditing/changelog and all modules are required to send info: messages # for all REPAIRED promises. A similar logic applies to errors and warnings, # IOW, anything at or above the info level. - return ((_LOG_LEVELS[msg_level] <= _LOG_LEVELS["info"]) or - (_LOG_LEVELS[msg_level] <= _LOG_LEVELS[level_set])) + return (_LOG_LEVELS[msg_level] <= _LOG_LEVELS["info"]) or ( + _LOG_LEVELS[msg_level] <= _LOG_LEVELS[level_set] + ) def _cfengine_type(typing): @@ -71,12 +72,14 @@ class AttributeObject(object): def __init__(self, d): for key, value in d.items(): setattr(self, key, value) + def __repr__(self): return "{}({})".format( self.__class__.__qualname__, - ", ".join("{}={!r}".format(k, v) for k, v in self.__dict__.items()) + ", ".join("{}={!r}".format(k, v) for k, v in self.__dict__.items()), ) + class ValidationError(Exception): def __init__(self, message): self.message = message From 509d979730e77e8b8e54a8908825230d03abfb76 Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Wed, 2 Jul 2025 16:03:18 +0200 Subject: [PATCH 6/9] CFEngine module library: Added docstring Signed-off-by: Ole Herman Schumacher Elgesem --- libraries/python/cfengine.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/libraries/python/cfengine.py b/libraries/python/cfengine.py index e069601..7e07e0f 100644 --- a/libraries/python/cfengine.py +++ b/libraries/python/cfengine.py @@ -1,3 +1,12 @@ +""" +CFEngine module library + +This library can be used to implement CFEngine modules in python. +Currently, this is for implementing custom promise types, +but it might be expanded to other types of modules in the future, +for example custom functions. +""" + import sys import json import traceback From d7d3087dab70b5f676b71c92e4819d8ee46bde80 Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Wed, 2 Jul 2025 16:38:29 +0200 Subject: [PATCH 7/9] Renamed cfengine.py to cfengine_module_library.py Signed-off-by: Ole Herman Schumacher Elgesem --- cfbs.json | 5 ++++- examples/git-using-lib/git_using_lib.py | 2 +- examples/gpg/gpg.py | 3 ++- examples/rss/rss.py | 2 +- examples/site-up/site_up.py | 2 +- libraries/python/{cfengine.py => cfengine_module_library.py} | 0 promise-types/ansible/ansible_promise.py | 2 +- promise-types/git/git.py | 2 +- promise-types/groups/groups.py | 2 +- promise-types/http/http_promise_type.py | 2 +- promise-types/iptables/iptables.py | 2 +- promise-types/json/json_promise_type.py | 2 +- promise-types/symlinks/symlinks.py | 2 +- promise-types/systemd/systemd.py | 2 +- 14 files changed, 17 insertions(+), 13 deletions(-) rename libraries/python/{cfengine.py => cfengine_module_library.py} (100%) diff --git a/cfbs.json b/cfbs.json index 21bbd61..e2641c0 100644 --- a/cfbs.json +++ b/cfbs.json @@ -152,7 +152,10 @@ "library-for-promise-types-in-python": { "description": "Library enabling promise types implemented in python.", "subdirectory": "libraries/python", - "steps": ["copy cfengine.py modules/promises/"] + "steps": [ + "copy cfengine_module_library.py modules/promises/cfengine_module_library.py", + "copy cfengine_module_library.py modules/promises/cfengine.py" + ] }, "maintainers-in-motd": { "description": "Add maintainer and purpose information from CMDB to /etc/motd", diff --git a/examples/git-using-lib/git_using_lib.py b/examples/git-using-lib/git_using_lib.py index 7e1c8b7..07747b1 100644 --- a/examples/git-using-lib/git_using_lib.py +++ b/examples/git-using-lib/git_using_lib.py @@ -1,5 +1,5 @@ import os -from cfengine import PromiseModule, ValidationError, Result +from cfengine_module_library import PromiseModule, ValidationError, Result class GitPromiseTypeModule(PromiseModule): diff --git a/examples/gpg/gpg.py b/examples/gpg/gpg.py index d1f206b..333b992 100644 --- a/examples/gpg/gpg.py +++ b/examples/gpg/gpg.py @@ -44,7 +44,8 @@ import json from subprocess import Popen, PIPE import sys -from cfengine import PromiseModule, ValidationError, Result +from cfengine_module_library import PromiseModule, ValidationError, Result + class GpgKeysPromiseTypeModule(PromiseModule): def __init__(self): diff --git a/examples/rss/rss.py b/examples/rss/rss.py index 0ef6a4c..1ece7fe 100755 --- a/examples/rss/rss.py +++ b/examples/rss/rss.py @@ -1,6 +1,6 @@ import requests, html, re, os, random import xml.etree.ElementTree as ET -from cfengine import PromiseModule, ValidationError, Result +from cfengine_module_library import PromiseModule, ValidationError, Result class RssPromiseTypeModule(PromiseModule): diff --git a/examples/site-up/site_up.py b/examples/site-up/site_up.py index 022919a..ec7775d 100644 --- a/examples/site-up/site_up.py +++ b/examples/site-up/site_up.py @@ -2,7 +2,7 @@ import ssl import urllib.request import urllib.error -from cfengine import PromiseModule, ValidationError, Result +from cfengine_module_library import PromiseModule, ValidationError, Result class SiteUpPromiseTypeModule(PromiseModule): diff --git a/libraries/python/cfengine.py b/libraries/python/cfengine_module_library.py similarity index 100% rename from libraries/python/cfengine.py rename to libraries/python/cfengine_module_library.py diff --git a/promise-types/ansible/ansible_promise.py b/promise-types/ansible/ansible_promise.py index 115ac77..ee9353b 100644 --- a/promise-types/ansible/ansible_promise.py +++ b/promise-types/ansible/ansible_promise.py @@ -2,7 +2,7 @@ from typing import Dict, Tuple, List -from cfengine import PromiseModule, ValidationError, Result +from cfengine_module_library import PromiseModule, ValidationError, Result try: from ansible import context diff --git a/promise-types/git/git.py b/promise-types/git/git.py index b14354a..cb8fecd 100644 --- a/promise-types/git/git.py +++ b/promise-types/git/git.py @@ -4,7 +4,7 @@ from typing import Dict, List, Optional -from cfengine import PromiseModule, ValidationError, Result +from cfengine_module_library import PromiseModule, ValidationError, Result class GitPromiseTypeModule(PromiseModule): diff --git a/promise-types/groups/groups.py b/promise-types/groups/groups.py index 111cf57..9c3f09c 100644 --- a/promise-types/groups/groups.py +++ b/promise-types/groups/groups.py @@ -1,7 +1,7 @@ import re import json from subprocess import Popen, PIPE -from cfengine import PromiseModule, ValidationError, Result +from cfengine_module_library import PromiseModule, ValidationError, Result class GroupsPromiseTypeModule(PromiseModule): diff --git a/promise-types/http/http_promise_type.py b/promise-types/http/http_promise_type.py index 735e5f0..f59ef11 100644 --- a/promise-types/http/http_promise_type.py +++ b/promise-types/http/http_promise_type.py @@ -8,7 +8,7 @@ import json from contextlib import contextmanager -from cfengine import PromiseModule, ValidationError, Result +from cfengine_module_library import PromiseModule, ValidationError, Result _SUPPORTED_METHODS = {"GET", "POST", "PUT", "DELETE", "PATCH"} diff --git a/promise-types/iptables/iptables.py b/promise-types/iptables/iptables.py index c8a90aa..2495851 100644 --- a/promise-types/iptables/iptables.py +++ b/promise-types/iptables/iptables.py @@ -3,7 +3,7 @@ as they are in `man iptables` """ -from cfengine import PromiseModule, ValidationError, Result, AttributeObject +from cfengine_module_library import PromiseModule, ValidationError, Result, AttributeObject from typing import Callable, List, Dict, Tuple from collections import namedtuple from itertools import takewhile, dropwhile diff --git a/promise-types/json/json_promise_type.py b/promise-types/json/json_promise_type.py index 32bb4c7..db48d48 100644 --- a/promise-types/json/json_promise_type.py +++ b/promise-types/json/json_promise_type.py @@ -3,7 +3,7 @@ import tempfile import shutil -from cfengine import PromiseModule, ValidationError, Result, AttributeObject +from cfengine_module_library import PromiseModule, ValidationError, Result, AttributeObject def is_number(num): diff --git a/promise-types/symlinks/symlinks.py b/promise-types/symlinks/symlinks.py index 2803c1c..dcb9af8 100644 --- a/promise-types/symlinks/symlinks.py +++ b/promise-types/symlinks/symlinks.py @@ -1,5 +1,5 @@ import os -from cfengine import PromiseModule, ValidationError, Result +from cfengine_module_library import PromiseModule, ValidationError, Result class SymlinksPromiseTypeModule(PromiseModule): diff --git a/promise-types/systemd/systemd.py b/promise-types/systemd/systemd.py index aae1413..e821980 100644 --- a/promise-types/systemd/systemd.py +++ b/promise-types/systemd/systemd.py @@ -5,7 +5,7 @@ from enum import Enum from typing import Dict, List, Optional, Tuple -from cfengine import PromiseModule, ValidationError, Result +from cfengine_module_library import PromiseModule, ValidationError, Result SYSTEMD_LIB_PATH = "/lib/systemd/system" From 18876d8e17a20409c7556c73681ad0c802ca8333 Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Wed, 2 Jul 2025 16:47:46 +0200 Subject: [PATCH 8/9] Reformatted all python files with black Signed-off-by: Ole Herman Schumacher Elgesem --- examples/rss/rss.py | 75 +++++++------ promise-types/git/git.py | 9 +- promise-types/http/http_promise_type.py | 142 +++++++++++++++++------- promise-types/iptables/iptables.py | 7 +- promise-types/json/json_promise_type.py | 7 +- 5 files changed, 167 insertions(+), 73 deletions(-) diff --git a/examples/rss/rss.py b/examples/rss/rss.py index 1ece7fe..5de683f 100755 --- a/examples/rss/rss.py +++ b/examples/rss/rss.py @@ -7,7 +7,6 @@ class RssPromiseTypeModule(PromiseModule): def __init__(self): super().__init__("rss_promise_module", "0.0.3") - def validate_promise(self, promiser, attributes, metadata): # check promiser type if type(promiser) is not str: @@ -15,37 +14,46 @@ def validate_promise(self, promiser, attributes, metadata): # check that promiser is a valid file path if not self._is_unix_file(promiser) and not self._is_win_file(promiser): - raise ValidationError(f"invalid value '{promiser}' for promiser: must be a filepath") + raise ValidationError( + f"invalid value '{promiser}' for promiser: must be a filepath" + ) # check that required attribute feed is present if "feed" not in attributes: raise ValidationError("Missing required attribute feed") # check that attribute feed has a valid type - feed = attributes['feed'] + feed = attributes["feed"] if type(feed) is not str: raise ValidationError("Invalid type for attribute feed: expected string") # check that attribute feed is a valid file path or url - if not (self._is_unix_file(feed) or self._is_win_file(feed) or self._is_url(feed)): - raise ValidationError(f"Invalid value '{feed}' for attribute feed: must be a file path or url") + if not ( + self._is_unix_file(feed) or self._is_win_file(feed) or self._is_url(feed) + ): + raise ValidationError( + f"Invalid value '{feed}' for attribute feed: must be a file path or url" + ) # additional checks if optional attribute select is present if "select" in attributes: - select = attributes['select'] + select = attributes["select"] # check that attribute select has a valid type if type(select) is not str: - raise ValidationError(f"Invalid type for attribute select: expected string") + raise ValidationError( + f"Invalid type for attribute select: expected string" + ) # check that attribute select has a valid value - if select != 'newest' and select != 'oldest' and select != 'random': - raise ValidationError(f"Invalid value '{select}' for attribute select: must be newest, oldest or random") - + if select != "newest" and select != "oldest" and select != "random": + raise ValidationError( + f"Invalid value '{select}' for attribute select: must be newest, oldest or random" + ) def evaluate_promise(self, promiser, attributes, metadata): # get attriute feed - feed = attributes['feed'] + feed = attributes["feed"] # fetch resource resource = self._get_resource(feed) @@ -65,7 +73,6 @@ def evaluate_promise(self, promiser, attributes, metadata): return result - def _get_resource(self, path): if self._is_url(path): # fetch from url @@ -73,73 +80,76 @@ def _get_resource(self, path): response = requests.get(path) if response.ok: return response.content - self.log_error(f"Failed to fetch feed from url '{path}'': status code '{response.status_code}'") + self.log_error( + f"Failed to fetch feed from url '{path}'': status code '{response.status_code}'" + ) return None # fetch from file try: self.log_verbose(f"Reading feed from file '{path}'") - with open(path, 'r', encoding='utf-8') as f: + with open(path, "r", encoding="utf-8") as f: resource = f.read() return resource except Exception as e: self.log_error(f"Failed to open file '{path}' for reading: {e}") return None - def _get_items(self, res, path): # extract descriptions in /channel/item try: self.log_verbose(f"Parsing feed '{path}'") items = [] root = ET.fromstring(res) - for item in root.findall('./channel/item'): + for item in root.findall("./channel/item"): for child in item: - if child.tag == 'description': + if child.tag == "description": items.append(child.text) return items except Exception as e: self.log_error(f"Failed to parse feed '{path}': {e}") return None - def _pick_item(self, items, attributes): # Pick newest item as default item = items[0] # Select item from feed if "select" in attributes: - select = attributes['select'] - if select == 'random': + select = attributes["select"] + if select == "random": self.log_verbose("Selecting random item from feed") item = random.choice(items) - elif select == 'oldest': + elif select == "oldest": self.log_verbose("Selecting oldest item from feed") - item = items[- 1] + item = items[-1] else: self.log_verbose("Selecting newest item from feed") else: self.log_verbose("Selecting newest item as default") return item - def _write_promiser(self, item, promiser): file_exist = os.path.isfile(promiser) if file_exist: try: - with open(promiser, 'r', encoding='utf-8') as f: + with open(promiser, "r", encoding="utf-8") as f: if f.read() == item: - self.log_verbose(f"File '{promiser}' exists and is up to date, no changes needed") + self.log_verbose( + f"File '{promiser}' exists and is up to date, no changes needed" + ) return Result.KEPT except Exception as e: self.log_error(f"Failed to open file '{promiser}' for reading: {e}") return Result.NOT_KEPT try: - with open(promiser, 'w', encoding='utf-8') as f: + with open(promiser, "w", encoding="utf-8") as f: if file_exist: - self.log_info(f"File '{promiser}' exists but contents differ, updating content") + self.log_info( + f"File '{promiser}' exists but contents differ, updating content" + ) else: self.log_info(f"File '{promiser}' does not exist, creating file") f.write(item) @@ -148,17 +158,20 @@ def _write_promiser(self, item, promiser): self.log_error(f"Failed to open file '{promiser}' for writing: {e}") return Result.NOT_KEPT - def _is_win_file(self, path): return re.search(r"^[a-zA-Z]:\\[\\\S|*\S]?.*$", path) != None - def _is_unix_file(self, path): return re.search(r"^(/[^/ ]*)+/?$", path) != None - def _is_url(self, path): - return re.search(r"^http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+", path) != None + return ( + re.search( + r"^http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+", + path, + ) + != None + ) if __name__ == "__main__": diff --git a/promise-types/git/git.py b/promise-types/git/git.py index cb8fecd..4880b4e 100644 --- a/promise-types/git/git.py +++ b/promise-types/git/git.py @@ -161,7 +161,12 @@ def evaluate_promise(self, promiser: str, attributes: Dict, metadata: Dict): # checkout the branch, if different from the current one output = self._git( model, - [model.executable, "rev-parse", "--abbrev-ref", "HEAD".format()], + [ + model.executable, + "rev-parse", + "--abbrev-ref", + "HEAD".format(), + ], cwd=model.destination, ) detached = False @@ -256,7 +261,7 @@ def _git_envvars(self, model: object): env["GIT_SSH_COMMAND"] = model.ssh_executable if model.ssh_options: env["GIT_SSH_COMMAND"] += " " + model.ssh_options - if not 'HOME' in env: + if not "HOME" in env: # git should have a HOME env var to retrieve .gitconfig, .git-credentials, etc env["HOME"] = str(Path.home()) return env diff --git a/promise-types/http/http_promise_type.py b/promise-types/http/http_promise_type.py index f59ef11..094d924 100644 --- a/promise-types/http/http_promise_type.py +++ b/promise-types/http/http_promise_type.py @@ -13,6 +13,7 @@ _SUPPORTED_METHODS = {"GET", "POST", "PUT", "DELETE", "PATCH"} + class FileInfo: def __init__(self, target): self.target = target @@ -36,7 +37,9 @@ def validate_promise(self, promiser, attributes, metadata): if type(method) != str: raise ValidationError("'method' must be a string") if method not in _SUPPORTED_METHODS: - raise ValidationError("'method' must be one of %s" % ", ".join(_SUPPORTED_METHODS)) + raise ValidationError( + "'method' must be one of %s" % ", ".join(_SUPPORTED_METHODS) + ) if "headers" in attributes: headers = attributes["headers"] @@ -44,23 +47,35 @@ def validate_promise(self, promiser, attributes, metadata): if headers_type == str: headers_lines = headers.splitlines() if any(line.count(":") != 1 for line in headers_lines): - raise ValidationError("'headers' must be string with 'name: value' pairs on separate lines") + raise ValidationError( + "'headers' must be string with 'name: value' pairs on separate lines" + ) elif headers_type == list: if any(line.count(":") != 1 for line in headers): - raise ValidationError("'headers' must be a list of 'name: value' pairs") + raise ValidationError( + "'headers' must be a list of 'name: value' pairs" + ) elif headers_type == dict: # nothing to check for dict? pass else: - raise ValidationError("'headers' must be a string, an slist or a data container" + - " value with 'name: value' pairs") + raise ValidationError( + "'headers' must be a string, an slist or a data container" + + " value with 'name: value' pairs" + ) if "payload" in attributes: payload = attributes["payload"] if type(payload) not in (str, dict): - raise ValidationError("'payload' must be a string or a data container value") + raise ValidationError( + "'payload' must be a string or a data container value" + ) - if type(payload) == str and payload.startswith("@") and not os.path.isabs(payload[1:]): + if ( + type(payload) == str + and payload.startswith("@") + and not os.path.isabs(payload[1:]) + ): raise ValidationError("File-based payload must be an absolute path") if "file" in attributes: @@ -70,18 +85,25 @@ def validate_promise(self, promiser, attributes, metadata): if "insecure" in attributes: insecure = attributes["insecure"] - if type(insecure) != str or insecure not in ("true", "True", "false", "False"): - raise ValidationError("'insecure' must be either \"true\" or \"false\"") + if type(insecure) != str or insecure not in ( + "true", + "True", + "false", + "False", + ): + raise ValidationError('\'insecure\' must be either "true" or "false"') @contextmanager def target_fh(self, file_info): if file_info.target: dirname = os.path.dirname(file_info.target) os.makedirs(dirname, exist_ok=True) - temp_file = file_info.target+".cftemp" + temp_file = file_info.target + ".cftemp" with open(temp_file, "wb") as fh: yield fh - if not os.path.isfile(file_info.target) or not filecmp.cmp(temp_file, file_info.target): + if not os.path.isfile(file_info.target) or not filecmp.cmp( + temp_file, file_info.target + ): os.replace(temp_file, file_info.target) file_info.was_repaired = True else: @@ -90,7 +112,6 @@ def target_fh(self, file_info): # this is to do something like API requests where you don't care about the result other than response code yield open(os.devnull, "wb") - def evaluate_promise(self, promiser, attributes, metadata): url = attributes.get("url", promiser) method = attributes.get("method", "GET") @@ -100,24 +121,39 @@ def evaluate_promise(self, promiser, attributes, metadata): insecure = attributes.get("insecure", False) result = Result.KEPT - canonical_promiser = promiser.translate(str.maketrans({char: "_" for char in ("@", "/", ":", "?", "&", "%")})) + canonical_promiser = promiser.translate( + str.maketrans({char: "_" for char in ("@", "/", ":", "?", "&", "%")}) + ) if headers and type(headers) != dict: if type(headers) == str: - headers = {key: value for key, value in (line.split(":") for line in headers.splitlines())} + headers = { + key: value + for key, value in (line.split(":") for line in headers.splitlines()) + } elif type(headers) == list: - headers = {key: value for key, value in (line.split(":") for line in headers)} + headers = { + key: value for key, value in (line.split(":") for line in headers) + } if payload: if type(payload) == dict: try: payload = json.dumps(payload) except TypeError: - self.log_error("Failed to convert 'payload' to text representation for request '%s'" % url) - return (Result.NOT_KEPT, - ["%s_%s_request_failed" % (canonical_promiser, method), - "%s_%s_payload_failed" % (canonical_promiser, method), - "%s_%s_payload_conversion_failed" % (canonical_promiser, method)]) + self.log_error( + "Failed to convert 'payload' to text representation for request '%s'" + % url + ) + return ( + Result.NOT_KEPT, + [ + "%s_%s_request_failed" % (canonical_promiser, method), + "%s_%s_payload_failed" % (canonical_promiser, method), + "%s_%s_payload_conversion_failed" + % (canonical_promiser, method), + ], + ) if "Content-Type" not in headers: headers["Content-Type"] = "application/json" @@ -129,11 +165,18 @@ def evaluate_promise(self, promiser, attributes, metadata): # scope. Thank you, Python! payload = open(path, "rb") except OSError as e: - self.log_error("Failed to open payload file '%s' for request '%s': %s" % (path, url, e)) - return (Result.NOT_KEPT, - ["%s_%s_request_failed" % (canonical_promiser, method), - "%s_%s_payload_failed" % (canonical_promiser, method), - "%s_%s_payload_file_failed" % (canonical_promiser, method)]) + self.log_error( + "Failed to open payload file '%s' for request '%s': %s" + % (path, url, e) + ) + return ( + Result.NOT_KEPT, + [ + "%s_%s_request_failed" % (canonical_promiser, method), + "%s_%s_payload_failed" % (canonical_promiser, method), + "%s_%s_payload_file_failed" % (canonical_promiser, method), + ], + ) if "Content-Length" not in headers: headers["Content-Length"] = os.path.getsize(path) @@ -142,12 +185,14 @@ def evaluate_promise(self, promiser, attributes, metadata): if type(payload) == str: payload = payload.encode("utf-8") - request = urllib.request.Request(url=url, data=payload, method=method, headers=headers) + request = urllib.request.Request( + url=url, data=payload, method=method, headers=headers + ) SSL_context = None if insecure: # convert to a boolean - insecure = (insecure.lower() == "true") + insecure = insecure.lower() == "true" if insecure: SSL_context = ssl.SSLContext() SSL_context.verify_method = ssl.CERT_NONE @@ -155,8 +200,13 @@ def evaluate_promise(self, promiser, attributes, metadata): try: with urllib.request.urlopen(request, context=SSL_context) as url_req: if not (200 <= url_req.status < 300): - self.log_error("Request for '%s' failed with code %d" % (url, url_req.status)) - return (Result.NOT_KEPT, ["%s_%s_request_failed" % (canonical_promiser, method)]) + self.log_error( + "Request for '%s' failed with code %d" % (url, url_req.status) + ) + return ( + Result.NOT_KEPT, + ["%s_%s_request_failed" % (canonical_promiser, method)], + ) # TODO: log progress when url_req.headers["Content-length"] > REPORTING_THRESHOLD file_info = FileInfo(target) with self.target_fh(file_info) as target_file: @@ -169,22 +219,38 @@ def evaluate_promise(self, promiser, attributes, metadata): result = Result.REPAIRED except urllib.error.URLError as e: self.log_error("Failed to request '%s': %s" % (url, e)) - return (Result.NOT_KEPT, ["%s_%s_request_failed" % (canonical_promiser, method)]) + return ( + Result.NOT_KEPT, + ["%s_%s_request_failed" % (canonical_promiser, method)], + ) except OSError as e: - self.log_error("Failed to store '%s' response to '%s': %s" % (url, target, e)) - return (Result.NOT_KEPT, - ["%s_%s_request_failed" % (canonical_promiser, method), - "%s_%s_file_failed" % (canonical_promiser, method)]) + self.log_error( + "Failed to store '%s' response to '%s': %s" % (url, target, e) + ) + return ( + Result.NOT_KEPT, + [ + "%s_%s_request_failed" % (canonical_promiser, method), + "%s_%s_file_failed" % (canonical_promiser, method), + ], + ) if target: if result == Result.REPAIRED: - self.log_info("Saved request response from '%s' to '%s'" % (url, target)) + self.log_info( + "Saved request response from '%s' to '%s'" % (url, target) + ) else: - self.log_info("No changes in request response from '%s' to '%s'" % (url, target)) + self.log_info( + "No changes in request response from '%s' to '%s'" % (url, target) + ) else: - self.log_info("Successfully executed%s request to '%s'" % ((" " + method if method else ""), - url)) + self.log_info( + "Successfully executed%s request to '%s'" + % ((" " + method if method else ""), url) + ) return (result, ["%s_%s_request_done" % (canonical_promiser, method)]) + if __name__ == "__main__": HTTPPromiseModule().start() diff --git a/promise-types/iptables/iptables.py b/promise-types/iptables/iptables.py index 2495851..a94b77c 100644 --- a/promise-types/iptables/iptables.py +++ b/promise-types/iptables/iptables.py @@ -3,7 +3,12 @@ as they are in `man iptables` """ -from cfengine_module_library import PromiseModule, ValidationError, Result, AttributeObject +from cfengine_module_library import ( + PromiseModule, + ValidationError, + Result, + AttributeObject, +) from typing import Callable, List, Dict, Tuple from collections import namedtuple from itertools import takewhile, dropwhile diff --git a/promise-types/json/json_promise_type.py b/promise-types/json/json_promise_type.py index db48d48..02c968e 100644 --- a/promise-types/json/json_promise_type.py +++ b/promise-types/json/json_promise_type.py @@ -3,7 +3,12 @@ import tempfile import shutil -from cfengine_module_library import PromiseModule, ValidationError, Result, AttributeObject +from cfengine_module_library import ( + PromiseModule, + ValidationError, + Result, + AttributeObject, +) def is_number(num): From 43825b584610fec0a5caabcdd3fd1532353daf73 Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Wed, 2 Jul 2025 16:49:45 +0200 Subject: [PATCH 9/9] Set all promise type modules to version number 0.0.0 See: https://northerntech.atlassian.net/browse/CFE-4553 Signed-off-by: Ole Herman Schumacher Elgesem --- promise-types/ansible/ansible_promise.py | 2 +- promise-types/git/git.py | 2 +- promise-types/groups/groups.py | 2 +- promise-types/http/http_promise_type.py | 2 +- promise-types/iptables/iptables.py | 2 +- promise-types/json/json_promise_type.py | 2 +- promise-types/symlinks/symlinks.py | 2 +- promise-types/systemd/systemd.py | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/promise-types/ansible/ansible_promise.py b/promise-types/ansible/ansible_promise.py index ee9353b..5f2c1c5 100644 --- a/promise-types/ansible/ansible_promise.py +++ b/promise-types/ansible/ansible_promise.py @@ -73,7 +73,7 @@ def v2_playbook_on_stats(self, stats): class AnsiblePromiseTypeModule(PromiseModule): def __init__(self, **kwargs): super(AnsiblePromiseTypeModule, self).__init__( - "ansible_promise_module", "0.2.2", **kwargs + "ansible_promise_module", "0.0.0", **kwargs ) def must_be_absolute(v): diff --git a/promise-types/git/git.py b/promise-types/git/git.py index 4880b4e..3f5a077 100644 --- a/promise-types/git/git.py +++ b/promise-types/git/git.py @@ -10,7 +10,7 @@ class GitPromiseTypeModule(PromiseModule): def __init__(self, **kwargs): super(GitPromiseTypeModule, self).__init__( - "git_promise_module", "0.2.5", **kwargs + "git_promise_module", "0.0.0", **kwargs ) def destination_must_be_absolute(v): diff --git a/promise-types/groups/groups.py b/promise-types/groups/groups.py index 9c3f09c..7d6bd99 100644 --- a/promise-types/groups/groups.py +++ b/promise-types/groups/groups.py @@ -6,7 +6,7 @@ class GroupsPromiseTypeModule(PromiseModule): def __init__(self): - super().__init__("groups_promise_module", "0.2.4") + super().__init__("groups_promise_module", "0.0.0") self._name_regex = re.compile(r"^[a-z_][a-z0-9_-]*[$]?$") self._name_maxlen = 32 diff --git a/promise-types/http/http_promise_type.py b/promise-types/http/http_promise_type.py index 094d924..fe71dce 100644 --- a/promise-types/http/http_promise_type.py +++ b/promise-types/http/http_promise_type.py @@ -21,7 +21,7 @@ def __init__(self, target): class HTTPPromiseModule(PromiseModule): - def __init__(self, name="http_promise_module", version="2.0.1", **kwargs): + def __init__(self, name="http_promise_module", version="0.0.0", **kwargs): super().__init__(name, version, **kwargs) def validate_promise(self, promiser, attributes, metadata): diff --git a/promise-types/iptables/iptables.py b/promise-types/iptables/iptables.py index a94b77c..8001813 100644 --- a/promise-types/iptables/iptables.py +++ b/promise-types/iptables/iptables.py @@ -122,7 +122,7 @@ class IptablesPromiseTypeModule(PromiseModule): } def __init__(self, **kwargs): - super().__init__("iptables_promise_module", "0.2.2", **kwargs) + super().__init__("iptables_promise_module", "0.0.0", **kwargs) def must_be_one_of(items) -> Callable: def validator(v): diff --git a/promise-types/json/json_promise_type.py b/promise-types/json/json_promise_type.py index 02c968e..b72260c 100644 --- a/promise-types/json/json_promise_type.py +++ b/promise-types/json/json_promise_type.py @@ -31,7 +31,7 @@ class JsonPromiseTypeModule(PromiseModule): def __init__(self, **kwargs): super(JsonPromiseTypeModule, self).__init__( - name="json_promise_module", version="0.0.1", **kwargs + name="json_promise_module", version="0.0.0", **kwargs ) self.types = ["object", "array", "string", "number", "primitive"] diff --git a/promise-types/symlinks/symlinks.py b/promise-types/symlinks/symlinks.py index dcb9af8..1b05864 100644 --- a/promise-types/symlinks/symlinks.py +++ b/promise-types/symlinks/symlinks.py @@ -7,7 +7,7 @@ class SymlinksPromiseTypeModule(PromiseModule): def __init__(self, **kwargs): super(SymlinksPromiseTypeModule, self).__init__( name="symlinks_promise_module", - version="0.0.1", + version="0.0.0", **kwargs, ) diff --git a/promise-types/systemd/systemd.py b/promise-types/systemd/systemd.py index e821980..7b2821a 100644 --- a/promise-types/systemd/systemd.py +++ b/promise-types/systemd/systemd.py @@ -22,7 +22,7 @@ class SystemdPromiseTypeStates(Enum): class SystemdPromiseTypeModule(PromiseModule): def __init__(self, **kwargs): super(SystemdPromiseTypeModule, self).__init__( - "systemd_promise_module", "0.2.3", **kwargs + "systemd_promise_module", "0.0.0", **kwargs ) def state_must_be_valid(v):