From 6708387357a803f7a30e4496b31181db967c17fe Mon Sep 17 00:00:00 2001 From: Sage Weil Date: Sun, 16 Feb 2020 10:17:27 -0600 Subject: [PATCH] mgr/orch: resurrect ServiceDescription, 'orch ls' This is a meta-service description that maps to multiple daemons. Remove the old useless properties, and add in size and running counts. Signed-off-by: Sage Weil --- src/pybind/mgr/cephadm/module.py | 41 +++++++--- src/pybind/mgr/orchestrator/_interface.py | 96 ++++++++++------------- src/pybind/mgr/orchestrator/module.py | 52 ++++++++++++ 3 files changed, 126 insertions(+), 63 deletions(-) diff --git a/src/pybind/mgr/cephadm/module.py b/src/pybind/mgr/cephadm/module.py index 9b694bfe2ec..f812fa7c2cb 100644 --- a/src/pybind/mgr/cephadm/module.py +++ b/src/pybind/mgr/cephadm/module.py @@ -1423,16 +1423,37 @@ class CephadmOrchestrator(MgrModule, orchestrator.OrchestratorClientMixin): return None -# def describe_service(self, service_type=None, service_id=None, -# node_name=None, refresh=False): -# if service_type not in ("mds", "osd", "mgr", "mon", 'rgw', "nfs", None): -# raise orchestrator.OrchestratorValidationError( -# service_type + " unsupported") -# result = self._get_daemons(service_type, -# service_id=service_id, -# node_name=node_name, -# refresh=refresh) -# return result + def describe_service(self, service_type=None, service_name=None, + refresh=False): + if refresh: + # ugly sync path, FIXME someday perhaps? + for host, hi in self.inventory.items(): + self._refresh_host_daemons(host) + sm = {} # type: Dict[str, orchestrator.ServiceDescription] + for h, dm in self.daemon_cache.data.items(): + for name, dd in dm.items(): + if service_type and service_type != dd.daemon_type: + continue + n = dd.service_name() + if service_name and service_name != n: + continue + if n not in sm: + sm[n] = orchestrator.ServiceDescription( + service_name=n, + last_refresh=dd.last_refresh, + container_image_id=dd.container_image_id, + container_image_name=dd.container_image_name, + ) + sm[n].size += 1 + if dd.status == 1: + sm[n].running += 1 + if not sm[n].last_refresh or not dd.last_refresh or dd.last_refresh < sm[n].last_refresh: # type: ignore + sm[n].last_refresh = dd.last_refresh + if sm[n].container_image_id != dd.container_image_id: + sm[n].container_image_id = 'mix' + if sm[n].container_image_name != dd.container_image_name: + sm[n].container_image_name = 'mix' + return trivial_result([s for n, s in sm.items()]) def list_daemons(self, daemon_type=None, daemon_id=None, host=None, refresh=False): diff --git a/src/pybind/mgr/orchestrator/_interface.py b/src/pybind/mgr/orchestrator/_interface.py index 0e6064304a7..ef51e03104f 100644 --- a/src/pybind/mgr/orchestrator/_interface.py +++ b/src/pybind/mgr/orchestrator/_interface.py @@ -890,8 +890,8 @@ class Orchestrator(object): """ raise NotImplementedError() - def describe_service(self, service_type=None, service_id=None, node_name=None, refresh=False): - # type: (Optional[str], Optional[str], Optional[str], bool) -> Completion + def describe_service(self, service_type=None, service_name=None, refresh=False): + # type: (Optional[str], Optional[str], bool) -> Completion """ Describe a service (of any kind) that is already configured in the orchestrator. For example, when viewing an OSD in the dashboard @@ -1225,6 +1225,14 @@ class DaemonDescription(object): return self.name().startswith(service_name + '.') return False + def service_name(self): + if self.daemon_type == 'rgw': + v = self.daemon_id.split('.') + return 'rgw.%s' % ('.'.join(v[0:2])) + if self.daemon_type in ['mds', 'nfs']: + return 'mds.%s' % (self.daemon_id.split('.')[0]) + return self.daemon_type + def __repr__(self): return "({type}.{id})".format(type=self.daemon_type, id=self.daemon_id) @@ -1267,44 +1275,24 @@ class ServiceDescription(object): has decided the service should run. """ - def __init__(self, nodename=None, - container_id=None, container_image_id=None, + def __init__(self, + container_image_id=None, container_image_name=None, - service=None, service_instance=None, - service_type=None, version=None, rados_config_location=None, - service_url=None, status=None, status_desc=None): - # Node is at the same granularity as InventoryNode - self.nodename = nodename # type: Optional[str] - + service_name=None, + rados_config_location=None, + service_url=None, + last_refresh=None, + size=0, + running=0): # Not everyone runs in containers, but enough people do to - # justify having the container_id (runtime id) and container_image + # justify having the container_image_id (image hash) and container_image # (image name) - self.container_id = container_id # runtime id self.container_image_id = container_image_id # image hash self.container_image_name = container_image_name # image friendly name - # Some services can be deployed in groups. For example, mds's can - # have an active and standby daemons, and nfs-ganesha can run daemons - # in parallel. This tag refers to a group of daemons as a whole. - # - # For instance, a cluster of mds' all service the same fs, and they - # will all have the same service value (which may be the - # Filesystem name in the FSMap). - # - # Single-instance services should leave this set to None - self.service = service - - # The orchestrator will have picked some names for daemons, - # typically either based on hostnames or on pod names. - # This is the in mds., the ID that will appear - # in the FSMap/ServiceMap. - self.service_instance = service_instance - - # The type of service (osd, mon, mgr, etc.) - self.service_type = service_type - - # Service version that was deployed - self.version = version + # The service_name is either a bare type (e.g., 'mgr') or + # type.id combination (e.g., 'mds.fsname' or 'rgw.realm.zone'). + self.service_name = service_name # Location of the service configuration when stored in rados # object. Format: "rados:///[]" @@ -1314,42 +1302,44 @@ class ServiceDescription(object): # the URL. self.service_url = service_url - # Service status: -1 error, 0 stopped, 1 running - self.status = status + # Number of daemons + self.size = size - # Service status description when status == -1. - self.status_desc = status_desc + # Number of daemons up + self.running = running # datetime when this info was last refreshed - self.last_refresh = None # type: Optional[datetime.datetime] + self.last_refresh = last_refresh # type: Optional[datetime.datetime] - def name(self): - if self.service_instance: - return '%s.%s' % (self.service_type, self.service_instance) - return self.service_type + def service_type(self): + if self.service_name: + return self.service_name.split('.')[0] + return None def __repr__(self): - return "({n_name}:{s_type})".format(n_name=self.nodename, - s_type=self.name()) + return "({name})".format(name=self.service_name) def to_json(self): out = { - 'nodename': self.nodename, - 'container_id': self.container_id, - 'service': self.service, - 'service_instance': self.service_instance, - 'service_type': self.service_type, - 'version': self.version, + 'container_image_id': self.container_image_id, + 'container_image_name': self.container_image_name, + 'service_name': self.service_name, 'rados_config_location': self.rados_config_location, 'service_url': self.service_url, - 'status': self.status, - 'status_desc': self.status_desc, + 'size': self.size, + 'running': self.running, } + if self.last_refresh: + out['last_refresh'] = self.last_refresh.strftime(DATEFMT) return {k: v for (k, v) in out.items() if v is not None} @classmethod @handle_type_error def from_json(cls, data): + if 'last_refresh' in data: + data['last_refresh'] = datetime.datetime.strptime( + data['last_refresh'], + DATEFMT) return cls(**data) diff --git a/src/pybind/mgr/orchestrator/module.py b/src/pybind/mgr/orchestrator/module.py index 5be00c64f9f..68722709a2d 100644 --- a/src/pybind/mgr/orchestrator/module.py +++ b/src/pybind/mgr/orchestrator/module.py @@ -292,6 +292,58 @@ class OrchestratorCli(OrchestratorClientMixin, MgrModule): out.append(table.get_string()) return HandleCommandResult(stdout='\n'.join(out)) + @_cli_read_command( + 'orch ls', + "name=service_type,type=CephString,req=false " + "name=service_name,type=CephString,req=false " + "name=format,type=CephChoices,strings=json|plain,req=false " + "name=refresh,type=CephBool,req=false", + 'List services known to orchestrator') + def _list_services(self, host=None, service_type=None, service_name=None, format='plain', refresh=False): + completion = self.describe_service(service_type, + service_name, + refresh=refresh) + self._orchestrator_wait([completion]) + raise_if_exception(completion) + services = completion.result + + def ukn(s): + return '' if s is None else s + + # Sort the list for display + services.sort(key=lambda s: (ukn(s.service_name))) + + if len(services) == 0: + return HandleCommandResult(stdout="No services reported") + elif format == 'json': + data = [s.to_json() for s in services] + return HandleCommandResult(stdout=json.dumps(data)) + else: + now = datetime.datetime.utcnow() + table = PrettyTable( + ['NAME', 'RUNNING', 'REFRESHED', 'IMAGE NAME', 'IMAGE ID'], + border=False) + table.align['NAME'] = 'l' + table.align['RUNNING'] = 'r' + table.align['REFRESHED'] = 'l' + table.align['IMAGE NAME'] = 'l' + table.align['IMAGE ID'] = 'l' + table.left_padding_width = 0 + table.right_padding_width = 1 + for s in sorted(services, key=lambda s: s.service_name): + if s.last_refresh: + age = to_pretty_timedelta(now - s.last_refresh) + ' ago' + else: + age = '-' + table.add_row(( + s.service_name, + '%d/%d' % (s.running, s.size), + age, + ukn(s.container_image_name), + ukn(s.container_image_id)[0:12])) + + return HandleCommandResult(stdout=table.get_string()) + @_cli_read_command( 'orch ps', "name=host,type=CephString,req=false " -- 2.39.5