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..5de683f 100755 --- a/examples/rss/rss.py +++ b/examples/rss/rss.py @@ -1,13 +1,12 @@ 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): 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/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 96% rename from libraries/python/cfengine.py rename to libraries/python/cfengine_module_library.py index e1dcf2a..7e07e0f 100644 --- a/libraries/python/cfengine.py +++ b/libraries/python/cfengine_module_library.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 @@ -49,8 +58,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 +81,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 @@ -380,6 +392,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 +403,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 diff --git a/promise-types/ansible/ansible_promise.py b/promise-types/ansible/ansible_promise.py index 115ac77..5f2c1c5 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 @@ -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 b14354a..3f5a077 100644 --- a/promise-types/git/git.py +++ b/promise-types/git/git.py @@ -4,13 +4,13 @@ from typing import Dict, List, Optional -from cfengine import PromiseModule, ValidationError, Result +from cfengine_module_library import PromiseModule, ValidationError, Result 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): @@ -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/groups/groups.py b/promise-types/groups/groups.py index 111cf57..7d6bd99 100644 --- a/promise-types/groups/groups.py +++ b/promise-types/groups/groups.py @@ -1,12 +1,12 @@ 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): 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 735e5f0..fe71dce 100644 --- a/promise-types/http/http_promise_type.py +++ b/promise-types/http/http_promise_type.py @@ -8,11 +8,12 @@ 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"} + class FileInfo: def __init__(self, target): self.target = target @@ -20,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): @@ -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 c8a90aa..8001813 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 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 @@ -117,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/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. diff --git a/promise-types/json/json_promise_type.py b/promise-types/json/json_promise_type.py index 2d945d7..b72260c 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 import PromiseModule, ValidationError, Result, AttributeObject +from cfengine_module_library import ( + PromiseModule, + ValidationError, + Result, + AttributeObject, +) def is_number(num): @@ -26,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"] @@ -112,7 +117,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 @@ -145,12 +152,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 @@ -160,8 +167,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)) diff --git a/promise-types/symlinks/symlinks.py b/promise-types/symlinks/symlinks.py index 2803c1c..1b05864 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): @@ -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 aae1413..7b2821a 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" @@ -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):