Lots of additions from now on I will create smaller commits

This commit is contained in:
2026-07-27 18:17:41 +01:00
parent f3d430d772
commit d130722390
6 changed files with 290 additions and 116 deletions
+31 -23
View File
@@ -1,34 +1,42 @@
# directory actions registry
from playbook.action_registry import ActionRegistry
from playbook.models import StepLog, Status
from playbook.models import StepLog
import os
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
if not os.path.exists(ctx["repoPath"]):
return StepLog.fail(name, [
{"status": "failed", "output": f"dir '{ctx["repoPath"]}' doesn't exist"}]
)
return StepLog.ok(name, "directory exists")
@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="create_dir", version="stat1.0")
def create_dir(ctx, name) -> StepLog:
try:
os.makedirs(ctx["repoPath"])
except Exception as e:
return StepLog.fail(name, [{"status": "failed", "output": str(e)}])
return StepLog.ok(name, "dir create successfully")
@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
@diract.register(name="create_dir_if_not_exists", version="stat1.0")
def create_dir_if_not_exists(ctx, name) -> StepLog:
log: StepLog = directory_exists(ctx, name + f".{directory_exists.__name__}")
msg: str = ""
if log.failed:
dir_creation_log: StepLog = create_dir(ctx, name + f".{create_dir.__name__}")
if dir_creation_log.failed:
return StepLog.fail(name, [{"status": "failed", "output": dir_creation_log}])
msg = "dir created succesfully"
else:
msg = "dir already exists"
return StepLog.ok(name, msg)
+161 -40
View File
@@ -1,54 +1,175 @@
# borg actions registry
# restic actions registry
from playbook.action_registry import ActionRegistry
from playbook.models import StepLog, Status
from playbook.models import StepLog
import sys
import json
import subprocess
from typing import List, Dict, Any, Tuple
# NOTE:
# most restic run commands seem to be the same pattern of:
# restic command -> parse output -> grab errors
# we can create a class for this I reckon
def parse_output(line: str) -> List[Dict[str, Any]]:
errors = []
for line in line.split('\n'):
if line.startswith("{"): # } treesitter is borked lmao
data = json.loads(line)
errors.append(data)
return errors
def find_restic_errors(data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
errors = []
for s in data:
if s["message_type"] == "exit_error":
errors.append(s)
return errors
def run_restic_command(cmd: List[str]) -> Tuple[int, List[Dict[str, Any]]]:
"""
Executes a restic command, streams output live to terminal,
and returns (returncode, parsed_json_objects).
"""
parsed_json = []
# Start process with combined stdout/stderr pipe
process = subprocess.Popen(
cmd,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1
)
# NOTE: is this what we want?
assert process.stdout is not None
# Read line-by-line in real-time
for line in iter(process.stdout.readline, ''):
# Write live output directly to terminal screen
sys.stdout.write(line)
sys.stdout.flush()
# Parse and capture valid JSON objects on the fly
clean_line = line.strip()
if clean_line.startswith('{'): # }
try:
data = json.loads(clean_line)
parsed_json.append(data)
except json.JSONDecodeError:
pass
process.stdout.close()
returncode = process.wait()
return returncode, parsed_json
restic = ActionRegistry("restic_actions_reg")
@restic.register(name="check_repo_exists", version="preborg1.0")
def check_repo_exists(ctx, name) -> StepLog:
passwordFile: str = ctx["passwordFile"]
repoPath: str = ctx["repoPath"]
@restic.register(name="check_restic_environment", version="")
def check_restic_environment(ctx, name: str = "") -> StepLog:
for v in ["passwordFile", "repoPath", "sourcePath", "serviceTag"]:
if not ctx.get(v):
return StepLog.fail(name, [f"{v} variable is empty"])
if passwordFile == "":
return StepLog(
step_name=name,
status=Status.BAD,
error=["password file variable is empty"],
)
return StepLog.ok(name, "environment configuration seems to be valid")
if repoPath == "":
return StepLog(
step_name=name,
status=Status.BAD,
error=["password file variable is empty"]
)
# If the path doesn't exist restic will make sure of warning us about it
return StepLog(
step_name=name,
status=Status.GOOD,
msg="password file variable is empty",
@restic.register(name="create_restic_repo", version="")
def create_restic_repo(ctx, name: str = "") -> StepLog:
cmd = ["restic", "init", "--json", "-r", ctx["repoPath"],
"--password-file", ctx["passwordFile"]]
returncode, json_output = run_restic_command(cmd)
errors: List[Dict[str, Any]] = find_restic_errors(json_output)
# Handle error messages in JSON output
if len(errors) > 0:
return StepLog.fail(name, errors)
# Make sure the return code is good too
if returncode != 0:
return StepLog.fail(name, [{"status": "failed", "output": json_output}])
return StepLog.ok(name, "repo create succesfully")
@restic.register(name="create_repo_if_not_exists", version="")
def create_repo_if_not_exists(ctx, name: str = "") -> StepLog:
log: StepLog = check_if_repo_exists(ctx, "")
msg: str = ""
if log.failed:
# Repo doesn't exist is error code 10
if log.error[0]["code"] != 10:
return StepLog.fail(name, [{"status": "failed", "output": log.error}])
# Create repo
repoCreateLog: StepLog = create_restic_repo(ctx, name + f".{create_restic_repo}")
if repoCreateLog.failed:
return StepLog.fail(name, [{"status": "failed", "output": repoCreateLog.error}])
msg = "repo created"
else:
msg = "repo already exists"
return StepLog.ok(name, msg)
@restic.register(name="check_if_repo_exists", version="")
def check_if_repo_exists(ctx, name: str = "") -> StepLog:
cmd = ["restic", "-r", ctx["repoPath"], "cat", "config", "--json",
"--password-file", ctx["passwordFile"]]
returncode, json_output = run_restic_command(cmd)
errors: List[Dict[str, Any]] = find_restic_errors(json_output)
# Handle error messages in JSON output
if len(errors) > 0:
return StepLog.fail(name, errors)
# Make sure the return code is good too
if returncode != 0:
return StepLog.fail(name, [{"status": "failed", "output": json_output}])
return StepLog.ok(name, "repo seems to exist")
@restic.register(name="backup_data_to_restic_repo", version="")
def backup_data_to_restic_repo(ctx, name) -> StepLog:
out = subprocess.run(
["restic", "backup", ctx["sourcePath"], "--json", "--quiet",
"-r", ctx["repoPath"], "--password-file", ctx["passwordFile"]],
text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
)
json_output: List[Dict[str, Any]] = parse_output(out.stdout)
errors: List[Dict[str, Any]] = find_restic_errors(json_output)
@restic.register(name="backup_data_to_repo", version="playborg1.0")
def backup_data_to_repo(ctx, name) -> StepLog:
log: StepLog = StepLog(
step_name=name,
status=Status.GOOD,
msg="directory was copied/movied/borged/rsynced",
)
return log
# Handle error messages in JSON output
if len(errors) > 0:
return StepLog.fail(name, errors)
# Make sure the return code is good too
if out.returncode != 0:
return StepLog.fail(name, [{"status": "failed", "output": out.stdout}])
@restic.register(name="post", version="postborg1.0")
def validate_data_backup(ctx, name) -> StepLog:
log: StepLog = StepLog(
step_name=name,
status=Status.GOOD,
msg="copied/movied/borged/rsynced exists and is valid",
)
return log
# Return code is 0, so it was successful, output the message
try:
data = json.loads(out.stdout)
msg = data
except:
msg = {
"status": "ran succesfully, but an error occurred parsing the output",
"output": out.stdout,
}
return StepLog.ok(name, str(msg))