256 lines
8.1 KiB
Python
256 lines
8.1 KiB
Python
"""
|
|
This module is responsible for handling all events
|
|
that must be raised to the user somehow. The main idea
|
|
behind this module is to centralize all events that happens
|
|
during the execution of Pro commands and allows us to report
|
|
those events in real time or through a machine-readable format.
|
|
"""
|
|
|
|
import enum
|
|
import json
|
|
import sys
|
|
from typing import Any, Dict, List, Optional, Set, Union # noqa: F401
|
|
|
|
from uaclient.yaml import safe_dump
|
|
|
|
JSON_SCHEMA_VERSION = "0.1"
|
|
EventFieldErrorType = Optional[Union[str, Dict[str, str]]]
|
|
_event_logger = None
|
|
|
|
|
|
def get_event_logger():
|
|
global _event_logger
|
|
|
|
if _event_logger is None:
|
|
_event_logger = EventLogger()
|
|
|
|
return _event_logger
|
|
|
|
|
|
@enum.unique
|
|
class EventLoggerMode(enum.Enum):
|
|
"""
|
|
Defines event logger supported modes.
|
|
Currently, we only support the cli and machine-readable mode. On cli mode,
|
|
we will print to stdout/stderr any event that we receive. Otherwise, we
|
|
will store those events and parse them for the specified format.
|
|
"""
|
|
|
|
CLI = object()
|
|
JSON = object()
|
|
YAML = object()
|
|
|
|
|
|
def format_machine_readable_output(status: Dict[str, Any]) -> Dict[str, Any]:
|
|
from uaclient.util import get_pro_environment
|
|
|
|
status["environment_vars"] = [
|
|
{"name": name, "value": value}
|
|
for name, value in sorted(get_pro_environment().items())
|
|
]
|
|
|
|
# We don't need the origin info in the json output
|
|
status.pop("origin", "")
|
|
|
|
# In case there is an error during status and the services were
|
|
# not processed
|
|
status.setdefault("services", [])
|
|
|
|
# We are redacting every variant information from the status output
|
|
# because we still are not sure on the best way to represent this
|
|
# information on the status machine readable output
|
|
for service in status.get("services", []):
|
|
if "variants" in service:
|
|
service.pop("variants")
|
|
|
|
return status
|
|
|
|
|
|
class EventLogger:
|
|
def __init__(self):
|
|
self._error_events = [] # type: List[Dict[str, EventFieldErrorType]]
|
|
self._warning_events = [] # type: List[Dict[str, EventFieldErrorType]]
|
|
self._processed_services = set() # type: Set[str]
|
|
self._failed_services = set() # type: Set[str]
|
|
self._needs_reboot = False
|
|
self._command = ""
|
|
self._output_content = {}
|
|
|
|
# By default, the event logger will be on CLI mode,
|
|
# printing every event it receives.
|
|
self._event_logger_mode = EventLoggerMode.CLI
|
|
|
|
def reset(self):
|
|
"""Reset the state of the event logger attributes."""
|
|
self._error_events = []
|
|
self._warning_events = []
|
|
self._processed_services = set()
|
|
self._failed_services = set()
|
|
self._needs_reboot = False
|
|
self._command = ""
|
|
self._output_content = {}
|
|
self._event_logger_mode = EventLoggerMode.CLI
|
|
|
|
def set_event_mode(self, event_mode: EventLoggerMode):
|
|
"""Set the event logger mode.
|
|
|
|
We currently support the CLI, JSON and YAML modes.
|
|
"""
|
|
self._event_logger_mode = event_mode
|
|
|
|
def set_command(self, command: str):
|
|
"""Set the event logger command.
|
|
|
|
The command will tell the process_events method which output method
|
|
to use.
|
|
"""
|
|
self._command = command
|
|
|
|
def set_output_content(self, output_content: Dict):
|
|
"""Set the event logger output content.
|
|
|
|
The command will tell the process_events method which content
|
|
to use.
|
|
"""
|
|
self._output_content = output_content
|
|
|
|
def info(self, info_msg: str, file_type=None, end: Optional[str] = None):
|
|
"""
|
|
Print the info message if the event logger is on CLI mode.
|
|
"""
|
|
if not file_type:
|
|
file_type = sys.stdout
|
|
|
|
if self._event_logger_mode == EventLoggerMode.CLI:
|
|
print(info_msg, file=file_type, end=end)
|
|
|
|
def _record_dict_event(
|
|
self,
|
|
msg: str,
|
|
service: Optional[str],
|
|
event_dict: List[Dict[str, EventFieldErrorType]],
|
|
code: Optional[str] = None,
|
|
event_type: Optional[str] = None,
|
|
additional_info: Optional[Dict[str, str]] = None,
|
|
):
|
|
if event_type is None:
|
|
event_type = "service" if service else "system"
|
|
|
|
event_entry = {
|
|
"type": event_type,
|
|
"service": service,
|
|
"message": msg,
|
|
"message_code": code,
|
|
} # type: Dict[str, EventFieldErrorType]
|
|
|
|
if additional_info:
|
|
event_entry["additional_info"] = additional_info
|
|
|
|
event_dict.append(event_entry)
|
|
|
|
def error(
|
|
self,
|
|
error_msg: str,
|
|
error_code: Optional[str] = None,
|
|
service: Optional[str] = None,
|
|
error_type: Optional[str] = None,
|
|
additional_info: Optional[Dict[str, str]] = None,
|
|
):
|
|
"""
|
|
Store an error in the event logger.
|
|
|
|
However, the error will only be stored if the event logger
|
|
is not on CLI mode.
|
|
"""
|
|
if self._event_logger_mode != EventLoggerMode.CLI:
|
|
self._record_dict_event(
|
|
msg=error_msg,
|
|
service=service,
|
|
event_dict=self._error_events,
|
|
code=error_code,
|
|
event_type=error_type,
|
|
additional_info=additional_info,
|
|
)
|
|
|
|
def warning(self, warning_msg: str, service: Optional[str] = None):
|
|
"""
|
|
Store a warning in the event logger.
|
|
|
|
However, the warning will only be stored if the event logger
|
|
is not on CLI mode.
|
|
"""
|
|
if self._event_logger_mode != EventLoggerMode.CLI:
|
|
self._record_dict_event(
|
|
msg=warning_msg,
|
|
service=service,
|
|
event_dict=self._warning_events,
|
|
)
|
|
|
|
def service_processed(self, service: str):
|
|
self._processed_services.add(service)
|
|
|
|
def services_failed(self, services: List[str]):
|
|
self._failed_services.update(services)
|
|
|
|
def service_failed(self, service: str):
|
|
self._failed_services.add(service)
|
|
|
|
def needs_reboot(self, reboot_required: bool):
|
|
self._needs_reboot = reboot_required
|
|
|
|
def _generate_failed_services(self):
|
|
services_with_error = {
|
|
error["service"]
|
|
for error in self._error_events
|
|
if error["service"]
|
|
}
|
|
return list(set.union(self._failed_services, services_with_error))
|
|
|
|
def _process_events_services(self):
|
|
response = {
|
|
"_schema_version": JSON_SCHEMA_VERSION,
|
|
"result": "success" if not self._error_events else "failure",
|
|
"processed_services": sorted(self._processed_services),
|
|
"failed_services": sorted(self._generate_failed_services()),
|
|
"errors": self._error_events,
|
|
"warnings": self._warning_events,
|
|
"needs_reboot": self._needs_reboot,
|
|
}
|
|
|
|
from uaclient.util import DatetimeAwareJSONEncoder
|
|
|
|
print(
|
|
json.dumps(response, cls=DatetimeAwareJSONEncoder, sort_keys=True)
|
|
)
|
|
|
|
def _process_events_status(self):
|
|
output = format_machine_readable_output(self._output_content)
|
|
output["result"] = "success" if not self._error_events else "failure"
|
|
output["errors"] = self._error_events
|
|
output["warnings"] = self._warning_events
|
|
|
|
if self._event_logger_mode == EventLoggerMode.JSON:
|
|
from uaclient.util import DatetimeAwareJSONEncoder
|
|
|
|
print(
|
|
json.dumps(
|
|
output, cls=DatetimeAwareJSONEncoder, sort_keys=True
|
|
)
|
|
)
|
|
elif self._event_logger_mode == EventLoggerMode.YAML:
|
|
print(safe_dump(output, default_flow_style=False))
|
|
|
|
def process_events(self) -> None:
|
|
"""
|
|
Creates a json response based on all of the
|
|
events stored in the event logger.
|
|
|
|
The json response will only be created if the event logger
|
|
is not on CLI mode.
|
|
"""
|
|
if self._event_logger_mode != EventLoggerMode.CLI:
|
|
if self._command == "status":
|
|
self._process_events_status()
|
|
else:
|
|
self._process_events_services()
|