Skip to content
5 changes: 4 additions & 1 deletion cfbs.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion examples/git-using-lib/git_using_lib.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import os
from cfengine import PromiseModule, ValidationError, Result
from cfengine_module_library import PromiseModule, ValidationError, Result


class GitPromiseTypeModule(PromiseModule):
Expand Down
3 changes: 2 additions & 1 deletion examples/gpg/gpg.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
77 changes: 45 additions & 32 deletions examples/rss/rss.py
Original file line number Diff line number Diff line change
@@ -1,51 +1,59 @@
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:
raise ValidationError("invalid type for promiser: expected string")

# 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)
Expand All @@ -65,81 +73,83 @@ def evaluate_promise(self, promiser, attributes, metadata):

return result


def _get_resource(self, path):
if self._is_url(path):
# fetch from url
self.log_verbose(f"Fetching feed from url '{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)
Expand All @@ -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__":
Expand Down
2 changes: 1 addition & 1 deletion examples/site-up/site_up.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions promise-types/ansible/ansible_promise.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down
13 changes: 9 additions & 4 deletions promise-types/git/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions promise-types/groups/groups.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down
Loading