Added custom registries for functions

This commit is contained in:
2026-07-25 15:49:39 +01:00
parent 026ab23a89
commit 7918977279
6 changed files with 165 additions and 37 deletions
+4
View File
@@ -1,4 +1,8 @@
playbook_name: "test_playbook"
registries:
- "./registry/directory_actions.py"
- "./registry/borg_actions.py"
steps:
- name: "backup dir1"
actions:
+57 -10
View File
@@ -1,11 +1,14 @@
from __future__ import annotations
import os
import sys
import yaml
import json
import time
import datetime
import importlib.util
from os import error, name
from typing import Callable, Dict, List, Any
from typing import Callable, Dict, List, Any, Optional
from dataclasses import dataclass, field
from abc import ABC, abstractmethod
@@ -122,10 +125,33 @@ class StepEntry(object):
class Play(object):
def __init__(self, name : str):
def __init__(self, name : str, registries: Optional[List[ActionRegistry]] = None):
self.name: str = name
self.__steps: List[StepEntry] = []
self.__registries: List[ActionRegistry] = [registry]
self.__registries: List[ActionRegistry] = [registry] + (registries or [])
@staticmethod
def __load_registry_file(file_path: str) -> List[ActionRegistry]:
abs_path = os.path.abspath(file_path)
module_name = os.path.splitext(os.path.basename(abs_path))[0]
spec = importlib.util.spec_from_file_location(module_name, abs_path)
if spec is None or spec.loader is None:
raise ImportError(f"could not create spec for registry file: {file_path}")
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)
discovered_registries = [
obj for obj in vars(module).values()
if isinstance(obj, ActionRegistry)
]
if not discovered_registries:
raise PlaybookError(f"No ActionRegistry instances were found in '{file_path}'")
return discovered_registries
def add_step(self, name: str, stepRunner : StepIF):
self.__steps.append(StepEntry(name=name, step=stepRunner))
@@ -138,8 +164,13 @@ class Play(object):
"actions": s.step.get_action_names()
})
registry_data = []
for reg in self.__registries:
registry_data.append(reg.manifest())
data = {
"name": self.name,
"registries": registry_data,
"number_of_steps": len(steps_info),
"steps": steps_info,
}
@@ -196,7 +227,7 @@ class Play(object):
return CustomStep(name=name, pre_fn=pre_fn, play_fn=play_fn, post_fn=post_fn, context=ctx)
@classmethod
def from_yaml(cls, fp: str) -> Play:
def from_yaml(cls, fp: str, registries: Optional[List[ActionRegistry]] = None) -> Play:
""" Loads the playbook from a given YAML file. """
try:
with open(fp, "r") as f:
@@ -205,23 +236,39 @@ class Play(object):
raise PlaybookError(f"Playbook file not found: {fp}") from e
except Exception as e:
raise PlaybookError(f"Failed to read file {fp}: {e}") from e
return cls.__from_yaml_str(file_contents)
yaml_dir = os.path.dirname(os.path.abspath(fp))
return cls.__from_yaml_str(file_contents, base_dir=yaml_dir)
@classmethod
def __from_yaml_str(cls, yaml_str: str) -> Play:
def __from_yaml_str(cls, yaml_str: str, base_dir: str = ".") -> Play:
data = None
try:
data = yaml.safe_load(yaml_str)
except Exception as e:
raise e
raise PlaybookError(f"YAML Syntax error: {e}") from e
playbook = cls(data["playbook_name"])
if not isinstance(data, dict) or "playbook_name" not in data:
raise PlaybookError("YAML must contain a top-level 'playbook_name' field.")
# LOAD THE CUSTOM REGISTRIES
custom_registries: List[ActionRegistry] = []
registry_paths = data.get("registries", [])
for reg_path in registry_paths:
full_path = os.path.join(base_dir, reg_path) if not os.path.isabs(reg_path) else reg_path
try:
regs = cls.__load_registry_file(full_path)
custom_registries.extend(regs)
except Exception as e:
raise PlaybookError(f"Error loading registry '{reg_path}': {e}") from e
playbook = cls(data["playbook_name"], registries=custom_registries)
for idx, step_cfg in enumerate(data.get("steps", [])):
step_name = step_cfg.get("name", f"step_{idx}")
actions = step_cfg.get("actions", {})
context = step_cfg.get("actions", {})
context = step_cfg.get("context", {})
if not isinstance(actions, dict) or len(actions) != 3:
raise PlaybookError(
+6
View File
@@ -44,3 +44,9 @@ class ActionRegistry:
raise ValueError(f"Version '{ver}' is not registered for function '{name}'")
return versions_dict[ver]
def manifest(self):
functions = {}
for fn_name, versions_dict in self.__actions.items():
functions[fn_name] = list(versions_dict.keys())
return {"registry_name": self.name, "functions": functions}
+34
View File
@@ -0,0 +1,34 @@
# borg actions registry
from playbook.action_registry import ActionRegistry
from playbook.models import StepLog, Status
borgact = ActionRegistry("borg_actions_reg")
@borgact.register(name="pre", version="preborg1.0")
def pre(ctx, name) -> StepLog:
log: StepLog = StepLog(
step_name=name,
status=Status.GOOD,
msg="source directory exists",
)
return log
@borgact.register(name="play", version="playborg1.0")
def play(ctx, name) -> StepLog:
log: StepLog = StepLog(
step_name=name,
status=Status.GOOD,
msg="directory was copied/movied/borged/rsynced",
)
return log
@borgact.register(name="post", version="postborg1.0")
def post(ctx, name) -> StepLog:
log: StepLog = StepLog(
step_name=name,
status=Status.GOOD,
msg="copied/movied/borged/rsynced exists and is valid",
)
return log
+34
View File
@@ -0,0 +1,34 @@
# directory actions registry
from playbook.action_registry import ActionRegistry
from playbook.models import StepLog, Status
diract = ActionRegistry("directory_actions_reg")
@diract.register(name="directory_exists", version="stat1.0")
def directory_exists(ctx, name) -> StepLog:
log: StepLog = StepLog(
step_name=name,
status=Status.GOOD,
msg="source directory exists",
)
return log
@diract.register(name="copy_directory", version="cp1.0")
def copy_directory(ctx, name) -> StepLog:
log: StepLog = StepLog(
step_name=name,
status=Status.GOOD,
msg="directory was copied/movied/borged/rsynced",
)
return log
@diract.register(name="verify_copy", version="verify1.0")
def verify_copy(ctx, name) -> StepLog:
log: StepLog = StepLog(
step_name=name,
status=Status.GOOD,
msg="copied/movied/borged/rsynced exists and is valid",
)
return log
+30 -27
View File
@@ -1,49 +1,52 @@
import os
import sys
import json
import argparse
import traceback
import importlib.util
from typing import List
from playbook import StepLog, CustomStep, Play, Status
from dataclasses import asdict
def pre(ctx) -> StepLog:
ctx["a"] = 5
from playbook.action_registry import ActionRegistry
return StepLog(
"pre something",
Status.GOOD,
0,
msg="set 5",
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Playbook Taskrunner")
# Flag for the playbook file
parser.add_argument(
"-p", "--playbook",
type=str,
required=True,
help="Path to the playbook yaml file"
)
def play(ctx):
ctx["a"] = ctx["a"] + 10
return StepLog(
"play something",
Status.BAD,
0,
msg="added 10",
# Flag for the custom registry files
parser.add_argument(
"-r", "--registry",
nargs="+",
default=[],
help="One or more python files with actions"
)
def post(ctx):
ctx["a"] = ctx["a"] / 2
return parser.parse_args()
return StepLog(
"post something",
Status.GOOD,
0,
msg=f"halved {ctx["a"]}",
)
def main():
args: argparse.Namespace = parse_args()
extra_regs: List[ActionRegistry] = []
newPlay: Play
try:
newPlay = Play.from_yaml("./playbook.yaml")
newPlay = Play.from_yaml(args.playbook, registries=extra_regs)
except Exception as e:
print(f"Failed to load playbook: {e}")
traceback.print_exc()
exit(1)
print(newPlay.view_playbook())
# print(json.dumps(asdict(newPlay.play()), indent=2))
print(json.dumps(asdict(newPlay.play()), indent=2))
if __name__ == "__main__":