]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
mgr/orch: resurrect ServiceDescription, 'orch ls'
authorSage Weil <sage@redhat.com>
Sun, 16 Feb 2020 16:17:27 +0000 (10:17 -0600)
committerSage Weil <sage@redhat.com>
Wed, 19 Feb 2020 17:01:13 +0000 (11:01 -0600)
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 <sage@redhat.com>
src/pybind/mgr/cephadm/module.py
src/pybind/mgr/orchestrator/_interface.py
src/pybind/mgr/orchestrator/module.py

index 9b694bfe2eca519430e0aed586d98b48c3a36a7a..f812fa7c2cb0e17ad253a933d24806768293ea13 100644 (file)
@@ -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):
index 0e6064304a794d40481a5dea836128582bff95d5..ef51e03104f21464770efb05ddd73e44b42369ee 100644 (file)
@@ -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 "<DaemonDescription>({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 <foo> in mds.<foo>, 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://<pool>/[<namespace/>]<object>"
@@ -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 "<ServiceDescription>({n_name}:{s_type})".format(n_name=self.nodename,
-                                                                s_type=self.name())
+        return "<ServiceDescription>({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)
 
 
index 5be00c64f9fa397f197cee6b7fa992a9abfd95f6..68722709a2d0bbee0273c4cfbbe52c827d58b554 100644 (file)
@@ -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 '<unknown>' 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 "