From: Guillaume Abrioux Date: Thu, 26 Oct 2023 14:34:10 +0000 (+0000) Subject: orch/cephadm: implement `ceph orch hardware` command X-Git-Tag: v18.2.4~314^2~34 X-Git-Url: http://git.apps.os.sepia.ceph.com/?a=commitdiff_plain;h=04605d8973f710766999eccfefcbf5cb901af036;p=ceph.git orch/cephadm: implement `ceph orch hardware` command This adds a first implementation of the `ceph orch hardware` CLI. Usage: ``` ceph orch hardware status [] [--category ] ``` Omitting the `[]` 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 (cherry picked from commit 1665156eea9e57e533a2ded26a8f7b37df68f5c5) --- diff --git a/src/pybind/mgr/cephadm/module.py b/src/pybind/mgr/cephadm/module.py index c0f04ecf1cbe2..0a8e06dfc2ea3 100644 --- a/src/pybind/mgr/cephadm/module.py +++ b/src/pybind/mgr/cephadm/module.py @@ -1636,6 +1636,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) -> str: """ diff --git a/src/pybind/mgr/orchestrator/_interface.py b/src/pybind/mgr/orchestrator/_interface.py index e9a6c3f07cb2f..6157e5eec3044 100644 --- a/src/pybind/mgr/orchestrator/_interface.py +++ b/src/pybind/mgr/orchestrator/_interface.py @@ -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) -> OrchResult[str]: """ Remove a host from the orchestrator inventory. diff --git a/src/pybind/mgr/orchestrator/module.py b/src/pybind/mgr/orchestrator/module.py index de4777e0defa4..43878221c9fdf 100644 --- a/src/pybind/mgr/orchestrator/module.py +++ b/src/pybind/mgr/orchestrator/module.py @@ -487,6 +487,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) -> HandleCommandResult: """Remove a host"""