Skip to content

Commit e3dfec7

Browse files
committed
19324 FIX HW/SW inventory: Fix flaky links to inventory tree or history
SUP-28176 Change-Id: I966e435ad548b4ab565c410abf84f4f4b68e440e
1 parent 059a5a9 commit e3dfec7

3 files changed

Lines changed: 202 additions & 5 deletions

File tree

.werks/19324.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[//]: # (werk v2)
2+
# HW/SW inventory: Fix flaky links to inventory tree or history
3+
4+
key | value
5+
---------- | ---
6+
date | 2026-04-21T10:35:13+00:00
7+
version | 2.4.0p27
8+
class | fix
9+
edition | cre
10+
component | multisite
11+
level | 1
12+
compatible | yes
13+
14+
Different conditions were implemented for linking inventory views in
15+
`Services of host > Host > HW/SW inventory`,
16+
`Inventory of host > Host > HW/SW inventory` and
17+
`Inventory history of host > Host > HW/SW inventory`,
18+
so that the links could be present in one view but not in another. This has been fixed now.

cmk/gui/views/visual_type.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,13 +158,30 @@ def _compute_link_from_result(
158158
base_link_from: Callable[[SingleInfos, Rows, Visual, HTTPVariables], bool],
159159
has_inventory_tree: Callable[[HostName, SiteId, SDPath | None, bool], bool],
160160
) -> bool:
161-
if not base_link_from(linking_view_single_infos, linking_view_rows, visual, context_vars):
162-
return False
163-
164161
link_from = visual["link_from"]
165162
if not link_from:
166163
return True # No link from filtering: Always display this.
167164

165+
if "has_inventory_tree" not in link_from and "has_inventory_tree_history" not in link_from:
166+
# No inventory checks — delegate fully to base class.
167+
return base_link_from(linking_view_single_infos, linking_view_rows, visual, context_vars)
168+
169+
# Inventory checks use context_vars directly and do not need row data. The base class
170+
# returns False for empty rows, which would wrongly suppress inventory links on views
171+
# whose datasource yields no rows (e.g. "Services of host" for a host with no services).
172+
# Only the single_infos guard from the base class is still relevant here.
173+
single_info_condition = link_from.get("single_infos")
174+
if single_info_condition and not set(single_info_condition).issubset(linking_view_single_infos):
175+
return False
176+
177+
# Non-inventory conditions (e.g. host_labels) must still be evaluated via the base class.
178+
# We call it after the single_infos guard so a mismatch short-circuits before the base
179+
# class can raise NotImplementedError for unsupported single_infos combinations.
180+
_inventory_keys = {"has_inventory_tree", "has_inventory_tree_history", "single_infos"}
181+
if link_from.keys() - _inventory_keys:
182+
if not base_link_from(linking_view_single_infos, linking_view_rows, visual, context_vars):
183+
return False
184+
168185
context = dict(context_vars)
169186
if (hostname := context.get("host")) is None:
170187
# No host data? Keep old behaviour

tests/unit/cmk/gui/views/test_visual_type.py

Lines changed: 164 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,10 @@ def test_inventory_tree_link_empty_rows() -> None:
115115
[],
116116
visual,
117117
context_vars,
118-
base_link_from=_base_checks_rows,
118+
base_link_from=_base_returns_true,
119119
has_inventory_tree=_tree_found,
120120
)
121-
is False
121+
is True
122122
)
123123

124124

@@ -131,13 +131,175 @@ def test_inventory_tree_history_link_empty_rows() -> None:
131131
[],
132132
visual,
133133
context_vars,
134+
base_link_from=_base_returns_true,
135+
has_inventory_tree=_tree_found,
136+
)
137+
is True
138+
)
139+
140+
141+
def test_label_filter_suppresses_link_when_rows_empty() -> None:
142+
visual = _make_visual(
143+
{
144+
"single_infos": ["host"],
145+
"host_labels": {"cmk/os-family": "linux"},
146+
"has_inventory_tree": _path("hardware"),
147+
}
148+
)
149+
context_vars: HTTPVariables = [("host", "myhost"), ("site", "mysite")]
150+
assert (
151+
_compute_link_from_result(
152+
["host"],
153+
[],
154+
visual,
155+
context_vars,
134156
base_link_from=_base_checks_rows,
135157
has_inventory_tree=_tree_found,
136158
)
137159
is False
138160
)
139161

140162

163+
def test_label_filter_suppresses_link_when_label_mismatches() -> None:
164+
visual = _make_visual(
165+
{
166+
"single_infos": ["host"],
167+
"host_labels": {"cmk/os-family": "linux"},
168+
"has_inventory_tree": _path("hardware"),
169+
}
170+
)
171+
context_vars: HTTPVariables = [("host", "myhost"), ("site", "mysite")]
172+
row: dict[str, object] = {}
173+
174+
def _base_rejects(
175+
single_infos: SingleInfos, rows: Rows, visual: Visual, context_vars: HTTPVariables
176+
) -> bool:
177+
return False
178+
179+
assert (
180+
_compute_link_from_result(
181+
["host"],
182+
[row],
183+
visual,
184+
context_vars,
185+
base_link_from=_base_rejects,
186+
has_inventory_tree=_tree_found,
187+
)
188+
is False
189+
)
190+
191+
192+
def test_label_and_inventory_both_match_shows_link() -> None:
193+
visual = _make_visual(
194+
{
195+
"single_infos": ["host"],
196+
"host_labels": {"cmk/os-family": "linux"},
197+
"has_inventory_tree": _path("hardware"),
198+
}
199+
)
200+
context_vars: HTTPVariables = [("host", "myhost"), ("site", "mysite")]
201+
row: dict[str, object] = {}
202+
assert (
203+
_compute_link_from_result(
204+
["host"],
205+
[row],
206+
visual,
207+
context_vars,
208+
base_link_from=_base_checks_rows,
209+
has_inventory_tree=_tree_found,
210+
)
211+
is True
212+
)
213+
214+
215+
def test_label_matches_but_inventory_missing_suppresses_link() -> None:
216+
visual = _make_visual(
217+
{
218+
"single_infos": ["host"],
219+
"host_labels": {"cmk/os-family": "linux"},
220+
"has_inventory_tree": _path("hardware"),
221+
}
222+
)
223+
context_vars: HTTPVariables = [("host", "myhost"), ("site", "mysite")]
224+
row: dict[str, object] = {}
225+
assert (
226+
_compute_link_from_result(
227+
["host"],
228+
[row],
229+
visual,
230+
context_vars,
231+
base_link_from=_base_checks_rows,
232+
has_inventory_tree=_tree_not_found,
233+
)
234+
is False
235+
)
236+
237+
238+
def test_single_infos_not_matching_returns_false() -> None:
239+
visual = _make_visual({"single_infos": ["host"], "has_inventory_tree": _path("hardware")})
240+
context_vars: HTTPVariables = [("host", "myhost"), ("site", "s")]
241+
assert (
242+
_compute_link_from_result(
243+
[],
244+
[],
245+
visual,
246+
context_vars,
247+
base_link_from=_base_returns_true,
248+
has_inventory_tree=_tree_found,
249+
)
250+
is False
251+
)
252+
253+
254+
def test_single_infos_matching_proceeds() -> None:
255+
visual = _make_visual({"single_infos": ["host"], "has_inventory_tree": _path("hardware")})
256+
context_vars: HTTPVariables = [("host", "myhost"), ("site", "mysite")]
257+
assert (
258+
_compute_link_from_result(
259+
["host"],
260+
[],
261+
visual,
262+
context_vars,
263+
base_link_from=_base_returns_true,
264+
has_inventory_tree=_tree_found,
265+
)
266+
is True
267+
)
268+
269+
270+
def test_single_infos_partially_matching_returns_false() -> None:
271+
visual = _make_visual(
272+
{"single_infos": ["host", "service"], "has_inventory_tree": _path("hardware")}
273+
)
274+
assert (
275+
_compute_link_from_result(
276+
["host"],
277+
[],
278+
visual,
279+
[],
280+
base_link_from=_base_returns_true,
281+
has_inventory_tree=_tree_found,
282+
)
283+
is False
284+
)
285+
286+
287+
def test_no_single_infos_condition_proceeds() -> None:
288+
visual = _make_visual({"has_inventory_tree": _path("hardware")})
289+
context_vars: HTTPVariables = [("host", "myhost"), ("site", "mysite")]
290+
assert (
291+
_compute_link_from_result(
292+
[],
293+
[],
294+
visual,
295+
context_vars,
296+
base_link_from=_base_returns_true,
297+
has_inventory_tree=_tree_found,
298+
)
299+
is True
300+
)
301+
302+
141303
def test_no_host_in_context_returns_true() -> None:
142304
visual = _make_visual({"has_inventory_tree": _path("hardware")})
143305
assert (

0 commit comments

Comments
 (0)