Skip to content

Commit

Permalink
Add support for Pebble service manager
Browse files Browse the repository at this point in the history
Resolves: canonical#584
  • Loading branch information
dosaboy committed Apr 26, 2023
1 parent e6f87a3 commit bed5170
Show file tree
Hide file tree
Showing 2 changed files with 180 additions and 0 deletions.
2 changes: 2 additions & 0 deletions hotsos/core/host_helpers/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')],
Expand Down
178 changes: 178 additions & 0 deletions hotsos/core/host_helpers/pebble.py
Original file line number Diff line number Diff line change
@@ -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/<name>/1830/<svc>
/usr/bin/<svc>
and filter e.g.
/var/lib/<svc> and /var/log/<svc>
"""
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)

0 comments on commit bed5170

Please sign in to comment.