forked from canonical/hotsos
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for Pebble service manager
Resolves: canonical#584
- Loading branch information
Showing
2 changed files
with
180 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |