]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
orch/cephadm: implement `ceph orch hardware` command
authorGuillaume Abrioux <gabrioux@ibm.com>
Thu, 26 Oct 2023 14:34:10 +0000 (14:34 +0000)
committerGuillaume Abrioux <gabrioux@ibm.com>
Thu, 25 Jan 2024 15:07:20 +0000 (15:07 +0000)
This adds a first implementation of the `ceph orch hardware` CLI.

Usage:

```
ceph orch hardware status [<hostname>] [--category <value>]
```

Omitting the `[<hostname>]` argument will generate a report for all hosts.
The default for argument `[--category]` is `summary`.

Example with `--category` :

```
+------------+-------------+-------+--------+---------+
|    HOST    |     NAME    | SPEED | STATUS |  STATE  |
+------------+-------------+-------+--------+---------+
| ceph-00001 |   eno8303   |   0   |   OK   | Enabled |
| ceph-00001 |   eno8403   |   0   |   OK   | Enabled |
| ceph-00001 | eno12399np0 | 10000 |   OK   | Enabled |
| ceph-00001 | eno12409np1 | 10000 |   OK   | Enabled |
| ceph-00001 |    bond0    | 10000 |   OK   | Enabled |
+------------+-------------+-------+--------+---------+
```

Signed-off-by: Guillaume Abrioux <gabrioux@ibm.com>
src/pybind/mgr/cephadm/module.py
src/pybind/mgr/orchestrator/_interface.py
src/pybind/mgr/orchestrator/module.py

index 8e54381b18cfdad959c08ba39714f14e4848df52..ed142adfc2a4a00a1fa22f7188f61a3fefc2f722 100644 (file)
@@ -1652,6 +1652,96 @@ Then run the following:
     def add_host(self, spec: HostSpec) -> str:
         return self._add_host(spec)
 
+    @handle_orch_error
+    def hardware_status(self,
+                        hostname: Optional[str] = None,
+                        category: str = 'summary',
+                        format: Format = Format.plain) -> str:
+        """
+        Display hardware status summary
+
+        :param hostname: hostname
+        """
+
+        table_heading_mapping = {
+            'summary': ['HOST', 'STORAGE', 'CPU', 'NET', 'MEMORY', 'POWER', 'FANS'],
+            'firmwares': ['HOST', 'COMPONENT', 'NAME', 'DATE', 'VERSION', 'STATUS'],
+            'criticals': ['HOST', 'COMPONENT', 'NAME', 'STATUS', 'STATE'],
+            'memory': ['HOST', 'NAME', 'STATUS', 'STATE'],
+            'storage': ['HOST', 'NAME', 'MODEL', 'SIZE', 'PROTOCOL', 'SN', 'STATUS', 'STATE'],
+            'processors': ['HOST', 'NAME', 'MODEL', 'CORES', 'THREADS', 'STATUS', 'STATE'],
+            'network': ['HOST', 'NAME', 'SPEED', 'STATUS', 'STATE'],
+            'power': ['HOST', 'ID', 'NAME', 'MODEL', 'MANUFACTURER', 'STATUS', 'STATE'],
+            'fans': ['HOST', 'ID', 'NAME', 'STATUS', 'STATE']
+        }
+        table_headings = table_heading_mapping.get(category, [])
+        table = PrettyTable(
+            table_headings,
+            border=True)
+        if category not in table_heading_mapping.keys():
+            return f"'{category}' is not a valid category."
+        if category == 'summary':
+            data = self.node_proxy.summary(hostname=hostname)
+            for k, v in data.items():
+                row = [k]
+                row.extend([v[key] for key in ['storage', 'processors', 'network', 'memory', 'power', 'fans']])
+                table.add_row(row)
+            output = table.get_string()
+        elif category == 'firmwares':
+            output = "Missing host name" if hostname is None else self._firmwares_table(hostname, table)
+        elif category == 'criticals':
+            output = self._criticals_table(hostname, table)
+        else:
+            output = self._common_table(category, hostname, table)
+
+        return output if 'output' in locals() else table.get_string()
+
+    def _firmwares_table(self, hostname, table):
+        data = self.node_proxy.firmwares(hostname=hostname)
+        for host, details in data.items():
+            for k, v in details.items():
+                table.add_row((host, k, v['name'], v['release_date'], v['version'], v['status']['health']))
+        return table.get_string()
+
+    def _criticals_table(self, hostname, table):
+        data = self.node_proxy.criticals(hostname=hostname)
+        for host, host_details in data.items():
+            for component, component_details in host_details.items():
+                for member, member_details in component_details.items():
+                    description = member_details.get('description') or member_details.get('name')
+                    table.add_row((host, component, description, member_details['status']['health'], member_details['status']['state']))
+        return table.get_string()
+
+    def _common_table(self, category, hostname, table):
+        data = self.node_proxy.common(endpoint=category, hostname=hostname)
+        mapping = {
+            'memory': ('description', 'health', 'state'),
+            'storage': ('description', 'model', 'capacity_bytes', 'protocol', 'serial_number', 'health', 'state'),
+            'processors': ('model', 'total_cores', 'total_threads', 'health', 'state'),
+            'network': ('name', 'speed_mbps', 'health', 'state'),
+            'power': ('name', 'model', 'manufacturer', 'health', 'state'),
+            'fans': ('name', 'health', 'state')
+        }
+
+        fields = mapping.get(category, ())
+        for host, details in data.items():
+            for k, v in details.items():
+                row = []
+                for field in fields:
+                    if field in v:
+                        row.append(v[field])
+                    elif field in v.get('status', {}):
+                        row.append(v['status'][field])
+                    else:
+                        row.append('')
+                if category in ('power', 'fans', 'processors'):
+                    table.add_row((host,) + (k,) + tuple(row))
+                else:
+                    table.add_row((host,) + tuple(row))
+
+
+        return table.get_string()
+
     @handle_orch_error
     def remove_host(self, host: str, force: bool = False, offline: bool = False, rm_crush_entry: bool = False) -> str:
         """
index 5bde317d19e67dcd7fcae2177bf62a609527b700..b36ffd4f89a34b9429172bd3096e6e7228437d15 100644 (file)
@@ -359,6 +359,15 @@ class Orchestrator(object):
         """
         raise NotImplementedError()
 
+    def hardware_status(self, hostname: Optional[str] = None, category: Optional[str] = 'summary') -> OrchResult[str]:
+        """
+        Display hardware status.
+
+        :param category: category
+        :param hostname: hostname
+        """
+        raise NotImplementedError()
+
     def remove_host(self, host: str, force: bool, offline: bool, rm_crush_entry: bool) -> OrchResult[str]:
         """
         Remove a host from the orchestrator inventory.
index 0000d25dd49cdb9b4fd21a0a157cccc40e24d3ce..9c5e2aec7719eef30003e1fe6c861aa53376f5a0 100644 (file)
@@ -488,6 +488,13 @@ class OrchestratorCli(OrchestratorClientMixin, MgrModule,
 
         return self._apply_misc([s], False, Format.plain)
 
+    @_cli_write_command('orch hardware status')
+    def _hardware_status(self, hostname: Optional[str] = None, _end_positional_: int = 0, category: str = 'summary') -> HandleCommandResult:
+        """Display hardware status"""
+        completion = self.hardware_status(hostname, category)
+        raise_if_exception(completion)
+        return HandleCommandResult(stdout=completion.result_str())
+
     @_cli_write_command('orch host rm')
     def _remove_host(self, hostname: str, force: bool = False, offline: bool = False, rm_crush_entry: bool = False) -> HandleCommandResult:
         """Remove a host"""