diff --git a/zabbix_cli/commands/host.py b/zabbix_cli/commands/host.py index 63403fad..4a23881e 100644 --- a/zabbix_cli/commands/host.py +++ b/zabbix_cli/commands/host.py @@ -650,6 +650,7 @@ def show_host( ) -> None: """Show a specific host.""" from zabbix_cli.commands.results.host import HostFilterArgs + from zabbix_cli.pyzabbix.utils import get_proxy_map args = HostFilterArgs.from_command_args( filter_legacy, active, maintenance, monitored @@ -659,6 +660,7 @@ def show_host( hostname_or_id, select_groups=True, select_templates=True, + select_interfaces=True, sort_field="host", sort_order="ASC", search=True, # we allow wildcard patterns @@ -667,6 +669,10 @@ def show_host( active_interface=args.active, ) + # HACK: inject proxy map to host for rendering + proxy_map = get_proxy_map(app.state.client) + host.set_proxy(proxy_map) + render_result(host) @@ -733,6 +739,7 @@ def show_hosts( """ from zabbix_cli.commands.results.host import HostFilterArgs from zabbix_cli.models import AggregateResult + from zabbix_cli.pyzabbix.utils import get_proxy_map args = HostFilterArgs.from_command_args( filter_legacy, active, maintenance, monitored @@ -749,14 +756,18 @@ def show_hosts( maintenance=args.maintenance_status, monitored=args.status, active_interface=args.active, + limit=limit, ) - total_hosts = len(hosts) # store len before limiting - if limit: - hosts = hosts[: abs(limit)] + # HACK: inject proxy map for each host + proxy_map = get_proxy_map(app.state.client) + for host in hosts: + host.set_proxy(proxy_map) render_result(AggregateResult(result=hosts)) + # TODO: implement paging for large result sets + total_hosts = app.state.client.get_host_count() if total_hosts > len(hosts): # we limited the results info( f"Only showing first {limit} of {total_hosts} hosts. Use [option]--limit 0[/] to show all." diff --git a/zabbix_cli/pyzabbix/client.py b/zabbix_cli/pyzabbix/client.py index 2974984e..1fd46e27 100644 --- a/zabbix_cli/pyzabbix/client.py +++ b/zabbix_cli/pyzabbix/client.py @@ -745,6 +745,7 @@ def get_host( maintenance=maintenance, monitored=monitored, active_interface=active_interface, + limit=1, ) if not hosts: raise ZabbixNotFoundError( @@ -770,7 +771,7 @@ def get_hosts( sort_field: Optional[str] = None, sort_order: Optional[Literal["ASC", "DESC"]] = None, search: bool = True, # we generally always want to search when multiple hosts are requested - # **filter_kwargs, + limit: Optional[int] = None, ) -> List[Host]: """Fetches all hosts matching the given criteria(s). @@ -853,10 +854,26 @@ def get_hosts( params["sortfield"] = sort_field if sort_order: params["sortorder"] = sort_order + if limit: + params["limit"] = limit resp: List[Any] = self.host.get(**params) or [] # TODO add result to cache - return [Host(**resp) for resp in resp] + return [Host(**r) for r in resp] + + def get_host_count(self) -> int: + """Fetches the total number of hosts in the Zabbix server.""" + return self.count("host") + + def count(self, object_type: str, params: Optional[ParamsType] = None) -> int: + """Count the number of objects of a given type.""" + params = params or {} + params["countOutput"] = True + try: + resp = getattr(self, object_type).get(**params) + return int(resp) + except (ZabbixAPIException, TypeError, ValueError) as e: + raise ZabbixAPICallError(f"Failed to fetch {object_type} count") from e def create_host( self, diff --git a/zabbix_cli/pyzabbix/types.py b/zabbix_cli/pyzabbix/types.py index 214ff811..dcda0153 100644 --- a/zabbix_cli/pyzabbix/types.py +++ b/zabbix_cli/pyzabbix/types.py @@ -417,13 +417,14 @@ class Host(ZabbixAPIBaseModel): # Compat for <7.0.0 validation_alias=AliasChoices("proxyid", "proxy_hostid"), ) - proxy_address: Optional[str] = None proxy_groupid: Optional[str] = None # >= 7.0 maintenance_status: Optional[str] = None + # active_available is a new field in 7.0. + # Previous versions required checking the `available` field of its first interface. + # In zabbix-cli v2, this value was serialized as `zabbix_agent`. active_available: Optional[str] = Field( default=None, validation_alias=AliasChoices( - "available", # < 7.0 "active_available", # >= 7.0 "zabbix_agent", # Zabbix-cli V2 name of this field ), @@ -432,6 +433,9 @@ class Host(ZabbixAPIBaseModel): macros: List[Macro] = Field(default_factory=list) interfaces: List[HostInterface] = Field(default_factory=list) + # HACK: Add a field for the host's proxy that we can inject later + proxy: Optional[Proxy] = None + def __str__(self) -> str: return f"{self.host!r} ({self.hostid})" @@ -442,6 +446,27 @@ def model_simple_dump(self) -> Dict[str, Any]: "hostid": self.hostid, } + def set_proxy(self, proxy_map: Dict[str, Proxy]) -> None: + """Set proxy info for the host given a mapping of proxy IDs to proxies.""" + if not (proxy := proxy_map.get(str(self.proxyid))): + return + self.proxy = proxy + + def get_active_status(self, with_code: bool = False) -> str: + """Returns the active interface status as a formatted string.""" + if self.zabbix_version.release >= (7, 0, 0): + return ActiveInterface.string_from_value( + self.active_available, with_code=with_code + ) + # We are on pre-7.0.0, check the first interface + iface = self.interfaces[0] if self.interfaces else None + if iface: + return ActiveInterface.string_from_value( + iface.available, with_code=with_code + ) + else: + return ActiveInterface.UNKNOWN.as_status(with_code=with_code) + # Legacy V2 JSON format compatibility @field_serializer("maintenance_status", when_used="json") def _LEGACY_maintenance_status_serializer( @@ -455,14 +480,15 @@ def _LEGACY_maintenance_status_serializer( return v @computed_field - def zabbix_agent(self) -> Optional[Union[int, str]]: + @property + def zabbix_agent(self) -> str: """LEGACY: Serializes the zabbix agent status as a formatted string in legacy mode, and as-is in new mode. """ # NOTE: use `self.active_available` instead of `self.zabbix_agent` if self.legacy_json_format: - return ActiveInterface.string_from_value(self.active_available) - return self.active_available + return self.get_active_status(with_code=True) + return self.get_active_status() @field_serializer("status", when_used="json") def _LEGACY_status_serializer( @@ -512,10 +538,10 @@ def __cols_rows__(self) -> ColsRowsType: self.host, "\n".join([group.name for group in self.groups]), "\n".join([template.host for template in self.templates]), - ActiveInterface.string_from_value(self.active_available), + self.zabbix_agent, MaintenanceStatus.string_from_value(self.maintenance_status), MonitoringStatus.string_from_value(self.status), - self.proxy_address or "", + self.proxy.name if self.proxy else "", ] ] return cols, rows diff --git a/zabbix_cli/pyzabbix/utils.py b/zabbix_cli/pyzabbix/utils.py index 1f1dde8c..ff4a3279 100644 --- a/zabbix_cli/pyzabbix/utils.py +++ b/zabbix_cli/pyzabbix/utils.py @@ -3,6 +3,7 @@ import random import re from typing import TYPE_CHECKING +from typing import Dict from typing import Optional from zabbix_cli.exceptions import ZabbixAPICallError @@ -14,7 +15,7 @@ def get_random_proxy(client: ZabbixAPI, pattern: Optional[str] = None) -> Proxy: - """Fetches a random proxy, optionally matching a regex pattern.""" + """Fetch a random proxy, optionally matching a regex pattern.""" proxies = client.get_proxies() if not proxies: raise ZabbixNotFoundError("No proxies found") @@ -27,3 +28,9 @@ def get_random_proxy(client: ZabbixAPI, pattern: Optional[str] = None) -> Proxy: if not proxies: raise ZabbixNotFoundError(f"No proxies matching pattern {pattern!r}") return random.choice(proxies) + + +def get_proxy_map(client: ZabbixAPI) -> Dict[str, Proxy]: + """Fetch all proxies and return a mapping of proxy IDs to Proxy objects.""" + proxies = client.get_proxies() + return {proxy.proxyid: proxy for proxy in proxies}