diff --git a/hotsos/core/host_helpers/cli.py b/hotsos/core/host_helpers/cli.py index cb672be7d..1a1b366f0 100644 --- a/hotsos/core/host_helpers/cli.py +++ b/hotsos/core/host_helpers/cli.py @@ -837,6 +837,8 @@ def command_catalog(self): 'pacemaker_crm_status': [BinCmd('crm status'), FileCmd('sos_commands/pacemaker/crm_status')], + 'pebble_services': + [BinCmd('pebble services')], 'ps': [BinCmd('ps auxwww'), FileCmd('ps')], diff --git a/hotsos/core/host_helpers/pebble.py b/hotsos/core/host_helpers/pebble.py new file mode 100644 index 000000000..7bf2259ea --- /dev/null +++ b/hotsos/core/host_helpers/pebble.py @@ -0,0 +1,178 @@ +import re + +from hotsos.core.log import log +from hotsos.core.factory import FactoryBase +from hotsos.core.host_helpers import CLIHelper +from hotsos.core.utils import cached_property, sorted_dict + +SVC_EXPR_TEMPLATES = { + "absolute": r".+\S+bin/({})(?:\s+.+|$)", + "snap": r".+\S+\d+/({})(?:\s+.+|$)", + "relative": r".+\s({})(?:\s+.+|$)", + } + + +class PebbleService(object): + + def __init__(self, name, state): + self.name = name + self.state = state + + def __repr__(self): + return ("name={}, state={}".format(self.name, self.state)) + + +class PebbleHelper(object): + """ Helper class used to query pebble services. """ + + def __init__(self, service_exprs, ps_allow_relative=True): + """ + @param service_exprs: list of python.re expressions used to match + service names. + @param ps_allow_relative: whether to allow commands to be identified + from ps as run using an relative binary + path e.g. mycmd as opposed to /bin/mycmd. + """ + self._ps_allow_relative = ps_allow_relative + self._service_exprs = set(service_exprs) + self._cached_unit_files_exprs = {} + + def _unit_files_expr(self, svc_name_expr): + """ + Returns search expression used to match unit files based in service + name expression. + + @param svc_name_expr: expression to match service name. + """ + if svc_name_expr in self._cached_unit_files_exprs: + return self._cached_unit_files_exprs[svc_name_expr] + + # Add snap prefix/suffixes + base_expr = r"(?:snap\.)?{}(?:\.daemon)?".format(svc_name_expr) + # NOTE: we include indirect services (ending with @) so that + # we can search for related units later. + unit_expr = r'^\s*({}@?)\.service\s+(\S+)'.format(base_expr) + # match entries in systemctl list-unit-files + self._cached_unit_files_exprs[svc_name_expr] = re.compile(unit_expr) + return self._cached_unit_files_exprs[svc_name_expr] + + @cached_property + def services(self): + """ + Return a dict of identified pebble services and their state. + + Service units are either direct or indirect. We unify these types, + taking the state of whichever is actually in use i.e. has in-memory + instances. Enabled units are aggregated but masked units are not so + that they can be identified and reported. + """ + _services = {} + for line in CLIHelper().pebble_services(): + for svc_name_expr in self._service_exprs: + ret = self._unit_files_expr(svc_name_expr).match(line) + if not ret: + continue + + unit = ret.group(1) + state = ret.group(2) + _services[unit] = PebbleService(unit, state) + + return _services + + def get_process_cmd_from_line(self, line, expr): + for expr_type, expr_tmplt in SVC_EXPR_TEMPLATES.items(): + if expr_type == 'relative' and not self._ps_allow_relative: + continue + + ret = re.compile(expr_tmplt.format(expr)).match(line) + if ret: + svc = ret.group(1) + log.debug("matched process %s with %s expr", svc, + expr_type) + return svc + + def get_services_expanded(self, name): + _expanded = [] + for line in self._pebble_services: + expr = r'^\s*({}(@\S*)?)\.service'.format(name) + ret = re.compile(expr).match(line) + if ret: + _expanded.append(ret.group(1)) + + if not _expanded: + _expanded = [name] + + return _expanded + + @cached_property + def processes(self): + """ + Identify running processes from ps that are associated with resolved + pebble services. The same search pattern used for identifying pebble + services is to match the process binary name. + + Returns a dictionary of process names along with the number of each. + """ + _proc_info = {} + for line in CLIHelper().ps(): + for expr in self._service_exprs: + """ + look for running process with this name. + We need to account for different types of process binary e.g. + + /snap//1830/ + /usr/bin/ + + and filter e.g. + + /var/lib/ and /var/log/ + """ + cmd = self.get_process_cmd_from_line(line, expr) + if cmd: + if cmd in _proc_info: + _proc_info[cmd] += 1 + else: + _proc_info[cmd] = 1 + + return _proc_info + + @property + def _service_info(self): + """Return a dictionary of pebble services grouped by state. """ + info = {} + for svc, obj in sorted_dict(self.services).items(): + state = obj.state + if state not in info: + info[state] = [] + + info[state].append(svc) + + return info + + @property + def _process_info(self): + """Return a list of processes associated with services. """ + return ["{} ({})".format(name, count) + for name, count in sorted_dict(self.processes).items()] + + @property + def summary(self): + """ + Output a dict summary of this class i.e. services, their state and any + processes run by them. + """ + return {'pebble': self._service_info, + 'ps': self._process_info} + + +class ServiceFactory(FactoryBase): + """ + Factory to dynamically create PebbleService objects for given services. + + Service objects are returned when a getattr() is done on this object using + the name of the service as the attr name. + """ + + def __getattr__(self, svc): + log.debug("creating service object for %s", svc) + return PebbleHelper([svc]).services.get(svc)