From b9ff9118470a1a0cfa94bf3ea4a3d341b25a6bd8 Mon Sep 17 00:00:00 2001 From: Sebastian Wagner Date: Sun, 7 Jun 2020 00:49:43 +0200 Subject: [PATCH] mgr/orch: Add OrchestratorEvent class Signed-off-by: Sebastian Wagner --- src/pybind/mgr/orchestrator/__init__.py | 1 + src/pybind/mgr/orchestrator/_interface.py | 80 +++++++++++++++++++++-- 2 files changed, 76 insertions(+), 5 deletions(-) diff --git a/src/pybind/mgr/orchestrator/__init__.py b/src/pybind/mgr/orchestrator/__init__.py index fa32bc4850d..fcb6a1313f2 100644 --- a/src/pybind/mgr/orchestrator/__init__.py +++ b/src/pybind/mgr/orchestrator/__init__.py @@ -14,5 +14,6 @@ from ._interface import \ OrchestratorValidationError, OrchestratorError, NoOrchestrator, \ ServiceDescription, InventoryFilter, HostSpec, \ DaemonDescription, \ + OrchestratorEvent, \ InventoryHost, DeviceLightLoc, \ UpgradeStatusSpec diff --git a/src/pybind/mgr/orchestrator/_interface.py b/src/pybind/mgr/orchestrator/_interface.py index 7ca1438310b..0f004090730 100644 --- a/src/pybind/mgr/orchestrator/_interface.py +++ b/src/pybind/mgr/orchestrator/_interface.py @@ -1222,7 +1222,9 @@ class DaemonDescription(object): started=None, last_configured=None, osdspec_affinity=None, - last_deployed=None): + last_deployed=None, + events: Optional[List['OrchestratorEvent']]=None): + # Host is at the same granularity as InventoryHost self.hostname: str = hostname @@ -1262,6 +1264,8 @@ class DaemonDescription(object): # Affinity to a certain OSDSpec self.osdspec_affinity = osdspec_affinity # type: Optional[str] + self.events: List[OrchestratorEvent] = events or [] + def name(self): return '%s.%s' % (self.daemon_type, self.daemon_id) @@ -1339,6 +1343,9 @@ class DaemonDescription(object): if getattr(self, k): out[k] = getattr(self, k).strftime(DATEFMT) + if self.events: + out['events'] = [e.to_json() for e in self.events] + empty = [k for k, v in out.items() if v is None] for e in empty: del out[e] @@ -1348,11 +1355,13 @@ class DaemonDescription(object): @handle_type_error def from_json(cls, data): c = data.copy() + event_strs = c.pop('events', []) for k in ['last_refresh', 'created', 'started', 'last_deployed', 'last_configured']: if k in c: c[k] = datetime.datetime.strptime(c[k], DATEFMT) - return cls(**c) + events = [OrchestratorEvent.from_json(e) for e in event_strs] + return cls(events=events, **c) def __copy__(self): # feel free to change this: @@ -1387,7 +1396,8 @@ class ServiceDescription(object): last_refresh=None, created=None, size=0, - running=0): + running=0, + events: Optional[List['OrchestratorEvent']]=None): # Not everyone runs in containers, but enough people do to # justify having the container_image_id (image hash) and container_image # (image name) @@ -1414,6 +1424,8 @@ class ServiceDescription(object): self.spec: ServiceSpec = spec + self.events: List[OrchestratorEvent] = events or [] + def service_type(self): return self.spec.service_type @@ -1430,13 +1442,15 @@ class ServiceDescription(object): 'size': self.size, 'running': self.running, 'last_refresh': self.last_refresh, - 'created': self.created + 'created': self.created, } for k in ['last_refresh', 'created']: if getattr(self, k): status[k] = getattr(self, k).strftime(DATEFMT) status = {k: v for (k, v) in status.items() if v is not None} out['status'] = status + if self.events: + out['events'] = [e.to_json() for e in self.events] return out @classmethod @@ -1444,13 +1458,15 @@ class ServiceDescription(object): def from_json(cls, data: dict): c = data.copy() status = c.pop('status', {}) + event_strs = c.pop('events', []) spec = ServiceSpec.from_json(c) c_status = status.copy() for k in ['last_refresh', 'created']: if k in c_status: c_status[k] = datetime.datetime.strptime(c_status[k], DATEFMT) - return cls(spec=spec, **c_status) + events = [OrchestratorEvent.from_json(e) for e in event_strs] + return cls(spec=spec, events=events, **c_status) @staticmethod def yaml_representer(dumper: 'yaml.SafeDumper', data: 'DaemonDescription'): @@ -1558,6 +1574,60 @@ class DeviceLightLoc(namedtuple('DeviceLightLoc', ['host', 'dev', 'path'])): __slots__ = () +class OrchestratorEvent: + """ + Similar to K8s Events. + + Some form of "important" log message attached to something. + """ + INFO = 'INFO' + ERROR = 'ERROR' + regex_v1 = re.compile(r'^([^ ]+) ([^:]+):([^ ]+) \[([^\]]+)\] "(.*)"$') + + def __init__(self, created: Union[str, datetime.datetime], kind, subject, level, message): + if isinstance(created, str): + created = datetime.datetime.strptime(created, DATEFMT) + self.created: datetime.datetime = created + + assert kind in "service daemon".split() + self.kind: str = kind + + # service name, or daemon danem or something + self.subject: str = subject + + # Events are not meant for debugging. debugs should end in the log. + assert level in "INFO ERROR".split() + self.level = level + + self.message: str = message + + __slots__ = ('created', 'kind', 'subject', 'level', 'message') + + def kind_subject(self) -> str: + return f'{self.kind}:{self.subject}' + + def to_json(self) -> str: + # Make a long list of events readable. + created = self.created.strftime(DATEFMT) + return f'{created} {self.kind_subject()} [{self.level}] "{self.message}"' + + + @classmethod + @handle_type_error + def from_json(cls, data) -> "OrchestratorEvent": + """ + >>> OrchestratorEvent.from_json('''2020-06-10T10:20:25.691255 daemon:crash.ubuntu [INFO] "Deployed crash.ubuntu on host 'ubuntu'"''').to_json() + '2020-06-10T10:20:25.691255 daemon:crash.ubuntu [INFO] "Deployed crash.ubuntu on host \\'ubuntu\\'"' + + :param data: + :return: + """ + match = cls.regex_v1.match(data) + if match: + return cls(*match.groups()) + raise ValueError(f'Unable to match: "{data}"') + + def _mk_orch_methods(cls): # Needs to be defined outside of for. # Otherwise meth is always bound to last key -- 2.39.5