Lots of additions from now on I will create smaller commits
This commit is contained in:
+40
-25
@@ -4,13 +4,13 @@ import os
|
||||
import sys
|
||||
import yaml
|
||||
import json
|
||||
import time
|
||||
import importlib.util
|
||||
from datetime import datetime, timezone
|
||||
from typing import Dict, List, Any, Optional
|
||||
from dataclasses import dataclass
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from playbook.models import Status, StepLog
|
||||
from playbook.models import Status, StepLog, timed_run
|
||||
from playbook.action_registry import ActionFn, ActionRegistry
|
||||
from playbook.premade_steps_registry import registry
|
||||
|
||||
@@ -38,22 +38,13 @@ class CustomStep(StepIF):
|
||||
def get_action_names(self) -> List[str]:
|
||||
return [action.__name__ for action in self.__actions]
|
||||
|
||||
def __timed_run(self, op: ActionFn) -> StepLog:
|
||||
"""Function that sets the duration_sec field on the log."""
|
||||
start_time: int = time.perf_counter_ns()
|
||||
log: StepLog = op(self.__context, self.name)
|
||||
log.duration_sec = time.perf_counter_ns() - start_time
|
||||
|
||||
return log
|
||||
|
||||
# NOTE: refactor this method to be more readable
|
||||
def run(self) -> StepLog:
|
||||
""" Run the step. """
|
||||
start_time: int = time.perf_counter_ns()
|
||||
|
||||
substeps: List[StepLog] = []
|
||||
status: Status = Status.GOOD
|
||||
for op in self.__actions:
|
||||
log: StepLog = self.__timed_run(op)
|
||||
log: StepLog = timed_run(op, self.__context, op.__name__)
|
||||
status = Status.BAD if log.failed else Status.GOOD
|
||||
|
||||
substeps.append(log)
|
||||
@@ -61,8 +52,6 @@ class CustomStep(StepIF):
|
||||
if status == Status.BAD:
|
||||
break
|
||||
|
||||
delta: int = time.perf_counter_ns() - start_time
|
||||
|
||||
errors = []
|
||||
msg: str = ""
|
||||
if status == Status.BAD:
|
||||
@@ -71,7 +60,7 @@ class CustomStep(StepIF):
|
||||
msg = "success"
|
||||
|
||||
return StepLog(
|
||||
step_name=self.name, status=status, duration_sec=delta,
|
||||
step_name=self.name, status=status,
|
||||
msg=msg, error=errors, substeps=substeps,
|
||||
)
|
||||
|
||||
@@ -90,6 +79,7 @@ class StepEntry(object):
|
||||
class Play(object):
|
||||
def __init__(self, name : str, registries: Optional[List[ActionRegistry]] = None):
|
||||
self.name: str = name
|
||||
self.log_dir: str = ""
|
||||
self.__steps: List[StepEntry] = []
|
||||
self.__registries: List[ActionRegistry] = [registry] + (registries or [])
|
||||
|
||||
@@ -141,7 +131,34 @@ class Play(object):
|
||||
return json.dumps(data)
|
||||
|
||||
def play(self) -> StepLog:
|
||||
start_time: int = time.perf_counter_ns()
|
||||
log: StepLog = timed_run(self.__play)
|
||||
|
||||
if self.log_dir != "":
|
||||
log.log_file_path = os.path.join(
|
||||
self.log_dir, self.log_file_name(log.start_date_timestamp)
|
||||
)
|
||||
self.__write_log(log)
|
||||
|
||||
return log
|
||||
|
||||
def log_file_name(self, start_date_ns: int) -> str:
|
||||
""" Format to ISO 8601. """
|
||||
seconds = start_date_ns // 1_000_000_000
|
||||
ms = (start_date_ns % 1_000_000_000) // 1_000_000
|
||||
|
||||
dt = datetime.fromtimestamp(seconds, tz=timezone.utc).astimezone()
|
||||
|
||||
return dt.strftime(f"{self.name}_%Y-%m-%d_%H-%M-%S.{ms:03d}.log")
|
||||
|
||||
def __write_log(self, log: StepLog):
|
||||
try:
|
||||
with open(log.log_file_path, "w") as file:
|
||||
json.dump(log, file)
|
||||
except Exception as e:
|
||||
return f"failed to create log file {e}"
|
||||
return ""
|
||||
|
||||
def __play(self) -> StepLog:
|
||||
playLog: StepLog = StepLog(
|
||||
step_name=self.name,
|
||||
status=Status.GOOD,
|
||||
@@ -152,18 +169,15 @@ class Play(object):
|
||||
)
|
||||
|
||||
for s in self.__steps:
|
||||
log: StepLog = s.step.run()
|
||||
log: StepLog = timed_run(s.step.run)
|
||||
|
||||
playLog.substeps.append(log)
|
||||
|
||||
if log.failed:
|
||||
playLog.error.append(f"failed in step: {s.name}")
|
||||
playLog.error.append({"status": "failed", "output": f"failed in step: {s.name}"})
|
||||
playLog.status = Status.BAD
|
||||
break
|
||||
|
||||
delta: int = time.perf_counter_ns() - start_time
|
||||
playLog.duration_sec = delta
|
||||
|
||||
if playLog.status == Status.GOOD:
|
||||
playLog.msg = "success"
|
||||
|
||||
@@ -227,16 +241,17 @@ class Play(object):
|
||||
raise PlaybookError(f"Error loading registry '{reg_path}': {e}") from e
|
||||
|
||||
playbook = cls(data["playbook_name"], registries=custom_registries)
|
||||
if data["log_dir"]:
|
||||
playbook.log_dir = data["log_dir"]
|
||||
|
||||
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("context", {})
|
||||
|
||||
if not isinstance(actions, dict) or len(actions) != 3:
|
||||
if not isinstance(actions, dict) or len(actions) <= 0:
|
||||
raise PlaybookError(
|
||||
f"Step '{step_name}' (index {idx}) must define an 'actions' map "
|
||||
"containing 'pre', 'play' and 'post'"
|
||||
f"Step '{step_name}' (index {idx}) must define at least one action "
|
||||
)
|
||||
|
||||
try:
|
||||
|
||||
+47
-4
@@ -1,6 +1,8 @@
|
||||
import time
|
||||
from enum import StrEnum
|
||||
from typing import List, Dict, Any, Callable
|
||||
from datetime import datetime, timezone
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum, StrEnum
|
||||
from typing import Callable, Dict, List, Any
|
||||
|
||||
|
||||
class Status(StrEnum):
|
||||
@@ -13,14 +15,55 @@ class StepLog(object):
|
||||
step_name: str
|
||||
status: Status = Status.BAD # By default set it to bad so the it fails by default
|
||||
# The user needs to be explicit
|
||||
duration_sec: int = 0
|
||||
cmd: str = ""
|
||||
start_date_timestamp: int = 0
|
||||
start_date: str = ""
|
||||
end_date: str = ""
|
||||
duration_sec: float = 0.0
|
||||
human_readable_duration: str = ""
|
||||
msg: str = ""
|
||||
error: List[str] = field(default_factory=list)
|
||||
error: List[Dict[Any, Any]] = field(default_factory=list)
|
||||
substeps: List[StepLog] = field(default_factory=list)
|
||||
log_file_path: str = ""
|
||||
|
||||
@property
|
||||
def failed(self) -> bool:
|
||||
""" Checks if the step failed """
|
||||
return len(self.error) > 0 or self.status == Status.BAD
|
||||
|
||||
@classmethod
|
||||
def ok(cls, name: str, msg: str = "success") -> StepLog:
|
||||
return cls(step_name=name, status=Status.GOOD, msg=msg)
|
||||
|
||||
@classmethod
|
||||
def fail(cls, name: str, errors: List[Any]) -> StepLog:
|
||||
return cls(step_name=name, status=Status.BAD, error=errors)
|
||||
|
||||
|
||||
def human_readable_date(time: int) -> str:
|
||||
seconds = time // 1_000_000_000
|
||||
nanos = time % 1_000_000_000
|
||||
|
||||
dt = datetime.fromtimestamp(seconds, tz=timezone.utc).astimezone()
|
||||
|
||||
return dt.strftime(f"%Y-%m-%dT%H:%M:%S.{nanos:09d}%:z")
|
||||
|
||||
|
||||
def timed_run(op: Callable[..., StepLog], *args: Any, **kwargs: Any) -> StepLog:
|
||||
start_date: int = time.time_ns()
|
||||
start_time: int = time.perf_counter_ns()
|
||||
|
||||
try:
|
||||
log: StepLog = op(*args, **kwargs)
|
||||
finally:
|
||||
delta = time.perf_counter_ns() - start_time
|
||||
|
||||
# NOTE: we could simplyfy this by having a marshalling method
|
||||
if isinstance(log, StepLog):
|
||||
end_date: int = time.time_ns()
|
||||
log.duration_sec = delta / 1_000_000_000.0
|
||||
log.start_date = human_readable_date(start_date)
|
||||
log.start_date_timestamp = start_date
|
||||
log.end_date = human_readable_date(end_date)
|
||||
|
||||
return log
|
||||
|
||||
Reference in New Issue
Block a user