Skip to content

Commit 09a58bc

Browse files
Improved snmp uptime parsing with new snmp parsing function while maintaining old behavior
Removed if64 workaround and moved tests to uptime tests SUP-28325 Change-Id: I476f6432101f668667fbc595eae9dc4ab9b1a7a4
1 parent 40d4570 commit 09a58bc

4 files changed

Lines changed: 197 additions & 109 deletions

File tree

cmk/plugins/collection/agent_based/if64.py

Lines changed: 3 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import dataclasses
77
import time
88
from collections.abc import Mapping, MutableMapping, Sequence
9-
from typing import Any, NamedTuple
9+
from typing import Any
1010

1111
from cmk.agent_based.v2 import (
1212
CheckPlugin,
@@ -35,43 +35,13 @@ def parse_if64adm(string_table: StringTable) -> If64AdmSection:
3535
}
3636

3737

38-
class UptimePair(NamedTuple):
39-
sys_uptime_sec: float | None
40-
hr_sys_uptime_sec: float | None
41-
42-
def __bool__(self) -> bool:
43-
return self.hr_sys_uptime_sec is not None or self.sys_uptime_sec is not None
44-
45-
def to_list(self) -> list[float]:
46-
return [
47-
value for value in [self.hr_sys_uptime_sec, self.sys_uptime_sec] if value is not None
48-
]
49-
50-
51-
# parse_snmp_uptime currently decides on an uptime
52-
# based on whether it is defined, thus 0 would be preferred over a real uptime
53-
# in case of bad data. This is a workaround to force parsing of each value.
54-
def parse_snmp_uptime_pair(string_table: StringTable) -> UptimePair | None:
55-
try:
56-
sys_uptime_sec = uptime.parse_snmp_uptime([[string_table[0][0], ""]])
57-
hr_sys_uptime_sec = uptime.parse_snmp_uptime([["", string_table[0][1]]])
58-
return UptimePair(
59-
sys_uptime_sec=sys_uptime_sec.uptime_sec if sys_uptime_sec else None,
60-
hr_sys_uptime_sec=hr_sys_uptime_sec.uptime_sec if hr_sys_uptime_sec else None,
61-
)
62-
except TypeError:
63-
return None
64-
except IndexError:
65-
return None
66-
67-
6838
def parse_if64_with_uptime(
6939
string_table: Sequence[StringByteTable],
7040
) -> interfaces.Section[interfaces.InterfaceWithCounters]:
7141
raw_uptime, raw_if64 = string_table
72-
parsed_uptimes = parse_snmp_uptime_pair([[str(w) for w in l] for l in raw_uptime])
42+
parsed_uptimes = uptime.parse_snmp_uptime_pair([[str(w) for w in l] for l in raw_uptime])
7343

74-
timestamp = max(parsed_uptimes.to_list()) if parsed_uptimes else time.time()
44+
timestamp = parsed_uptimes.max_uptime_sec if parsed_uptimes else time.time()
7545
return if64.parse_if64(
7646
raw_if64,
7747
timestamp,

packages/cmk-plugins/cmk/plugins/lib/uptime.py

Lines changed: 55 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,50 @@ def check(params: Mapping[str, Any], section: Section) -> CheckResult:
7575
)
7676

7777

78+
class UptimePair(NamedTuple):
79+
sys_uptime_sec: float | None
80+
hr_sys_uptime_sec: float | None
81+
82+
@property
83+
def max_uptime_sec(self) -> float:
84+
return max(
85+
[value for value in [self.hr_sys_uptime_sec, self.sys_uptime_sec] if value is not None]
86+
)
87+
88+
def __bool__(self) -> bool:
89+
return self.hr_sys_uptime_sec is not None or self.sys_uptime_sec is not None
90+
91+
92+
def parse_snmp_uptime_pair(string_table: StringTable) -> UptimePair | None:
93+
def parse(raw_value: str) -> int | None:
94+
if ":" in raw_value:
95+
try:
96+
days, h, m, s = raw_value.split(":")
97+
return (int(days) * 86400) + (int(h) * 3600) + (int(m) * 60) + int(float(s))
98+
except Exception:
99+
return None
100+
101+
if raw_value == "0":
102+
return 0
103+
104+
if len(raw_value) < 3:
105+
return None
106+
107+
try:
108+
return int(raw_value[:-2])
109+
except Exception:
110+
return None
111+
112+
try:
113+
return UptimePair(
114+
sys_uptime_sec=parse(string_table[0][0]), hr_sys_uptime_sec=parse(string_table[0][1])
115+
)
116+
except TypeError:
117+
return None
118+
except IndexError:
119+
return None
120+
121+
78122
def parse_snmp_uptime(string_table: StringTable) -> Section | None:
79123
"""
80124
>>> parse_snmp_uptime([['2297331594', '']])
@@ -87,29 +131,17 @@ def parse_snmp_uptime(string_table: StringTable) -> Section | None:
87131
Section(uptime_sec=0, message=None)
88132
89133
"""
90-
if not string_table:
91-
return None
92-
93-
ticks = string_table[0][1] or string_table[0][0]
94-
95-
if ticks == "0":
96-
return Section(0, None)
97134

98-
if len(ticks) < 3:
135+
# internally uses parse_snmp_uptime_pair to keep parsing logic
136+
# in one place.
137+
uptime_pair = parse_snmp_uptime_pair(string_table)
138+
if not uptime_pair:
99139
return None
100140

101-
try:
102-
return Section(int(ticks[:-2]), None)
103-
except Exception:
104-
pass
105-
106-
try:
107-
days, h, m, s = ticks.split(":")
108-
return Section(
109-
(int(days) * 86400) + (int(h) * 3600) + (int(m) * 60) + int(float(s)),
110-
None,
111-
)
112-
except Exception:
113-
pass
114-
115-
return None
141+
# prefer hr_sys_uptime_sec if possible, even if it may have weird values
142+
result = (
143+
uptime_pair.hr_sys_uptime_sec
144+
if uptime_pair.hr_sys_uptime_sec is not None
145+
else uptime_pair.sys_uptime_sec
146+
)
147+
return Section(result, None)
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
#!/usr/bin/env python3
2+
# Copyright (C) 2026 Checkmk GmbH - License: GNU General Public License v2
3+
# This file is part of Checkmk (https://checkmk.com). It is subject to the terms and
4+
# conditions defined in the file COPYING, which is part of this source code package.
5+
import pytest
6+
7+
from cmk.plugins.lib.uptime import parse_snmp_uptime, parse_snmp_uptime_pair, Section
8+
9+
10+
def test_parse_snmp_uptime_pair_with_empty_section_is_none() -> None:
11+
parsed_uptime = parse_snmp_uptime_pair([[]])
12+
assert parsed_uptime is None
13+
14+
15+
def test_parse_snmp_uptime_pair_with_no_values_is_not_valid() -> None:
16+
parsed_uptime = parse_snmp_uptime_pair([["", ""]])
17+
18+
assert parsed_uptime is not None
19+
assert not parsed_uptime
20+
with pytest.raises(ValueError):
21+
_ = parsed_uptime.max_uptime_sec
22+
assert parsed_uptime.sys_uptime_sec is None
23+
assert parsed_uptime.hr_sys_uptime_sec is None
24+
25+
26+
def test_parse_snmp_uptime_pair_with_sys_uptime_only_has_parsed_value() -> None:
27+
parsed_uptime = parse_snmp_uptime_pair([["500000", ""]])
28+
29+
assert parsed_uptime
30+
assert parsed_uptime.max_uptime_sec == 5000
31+
assert parsed_uptime.sys_uptime_sec == 5000
32+
assert parsed_uptime.hr_sys_uptime_sec is None
33+
34+
35+
def test_parse_snmp_uptime_pair_with_hr_sys_uptime_only_has_parsed_value() -> None:
36+
parsed_uptime = parse_snmp_uptime_pair([["", "500000"]])
37+
38+
assert parsed_uptime
39+
assert parsed_uptime.max_uptime_sec == 5000
40+
assert parsed_uptime.sys_uptime_sec is None
41+
assert parsed_uptime.hr_sys_uptime_sec == 5000
42+
43+
44+
def test_parse_snmp_uptime_pair_with_both_uptime_values_has_parsed_values() -> None:
45+
parsed_uptime = parse_snmp_uptime_pair([["500000", "500500"]])
46+
47+
assert parsed_uptime
48+
assert parsed_uptime.max_uptime_sec == 5005
49+
assert parsed_uptime.sys_uptime_sec == 5000
50+
assert parsed_uptime.hr_sys_uptime_sec == 5005
51+
52+
53+
def test_parse_snmp_uptime_pair_with_both_values_but_hr_uptime_is_zero_second_value_is_not_none() -> (
54+
None
55+
):
56+
parsed_uptime = parse_snmp_uptime_pair([["500000", "0"]])
57+
58+
assert parsed_uptime
59+
assert parsed_uptime.max_uptime_sec == 5000
60+
assert parsed_uptime.sys_uptime_sec == 5000
61+
assert parsed_uptime.hr_sys_uptime_sec == 0
62+
63+
64+
def test_parse_snmp_uptime_parses_with_zero_values() -> None:
65+
assert parse_snmp_uptime([["0", ""]]) == Section(uptime_sec=0, message=None)
66+
assert parse_snmp_uptime([["", "0"]]) == Section(uptime_sec=0, message=None)
67+
assert parse_snmp_uptime([["0", "0"]]) == Section(uptime_sec=0, message=None)
68+
69+
70+
def test_parse_snmp_uptime_is_none_with_empty_data() -> None:
71+
assert parse_snmp_uptime([["", ""]]) is None
72+
73+
74+
def test_parse_snmp_uptime_is_none_with_nonsense_data() -> None:
75+
assert parse_snmp_uptime([["", "Fortigate 80C"]]) is None
76+
assert parse_snmp_uptime([["Fortigate 80C", ""]]) is None
77+
78+
79+
def test_parse_snmp_uptime_parses_with_only_sys_up_time() -> None:
80+
assert parse_snmp_uptime([["2297331594", ""]]) == Section(uptime_sec=22973315, message=None)
81+
82+
83+
def test_parse_snmp_uptime_parses_with_only_hr_system_up_time() -> None:
84+
assert parse_snmp_uptime([["", "2297331594"]]) == Section(uptime_sec=22973315, message=None)
85+
86+
87+
def test_parse_snmp_uptime_prefers_sys_up_time_with_unexpected_hr_system_up_time() -> None:
88+
# would have been None with the previous logic
89+
assert parse_snmp_uptime([["2297331594", "9"]]) == Section(uptime_sec=22973315, message=None)
90+
assert parse_snmp_uptime([["2297331594", "99"]]) == Section(uptime_sec=22973315, message=None)
91+
92+
93+
def test_parse_snmp_uptime_prefers_hr_system_up_time_with_with_valid_hr_value() -> None:
94+
# keeping the old behavior and not trying to guess the correct value
95+
assert parse_snmp_uptime([["2297331594", "0"]]) == Section(uptime_sec=0, message=None)
96+
assert parse_snmp_uptime([["2297331594", "999"]]) == Section(uptime_sec=9, message=None)
97+
assert parse_snmp_uptime([["2297331594", "2297331"]]) == Section(uptime_sec=22973, message=None)
98+
99+
assert parse_snmp_uptime([["0", "2297331694"]]) == Section(uptime_sec=22973316, message=None)
100+
assert parse_snmp_uptime([["9", "2297331694"]]) == Section(uptime_sec=22973316, message=None)
101+
assert parse_snmp_uptime([["99", "2297331694"]]) == Section(uptime_sec=22973316, message=None)
102+
assert parse_snmp_uptime([["999", "2297331694"]]) == Section(uptime_sec=22973316, message=None)
103+
assert parse_snmp_uptime([["2297331", "2297331694"]]) == Section(
104+
uptime_sec=22973316, message=None
105+
)
106+
107+
108+
def test_parse_snmp_uptime_prefers_hr_system_up_time() -> None:
109+
assert parse_snmp_uptime([["2267331694", "2297331694"]]) == Section(
110+
uptime_sec=22973316, message=None
111+
)
112+
113+
114+
def test_parse_snmp_uptime_parses_with_sys_up_timestamp_and_hr_system_up_timestamp() -> None:
115+
assert parse_snmp_uptime([["124:21:26:42.03", "124:21:29:01.14"]]) == Section(
116+
uptime_sec=10790941, message=None
117+
)
118+
119+
120+
def test_parse_snmp_uptime_parses_mixed_formats() -> None:
121+
assert parse_snmp_uptime([["124:21:26:42.03", "1079094124"]]) == Section(
122+
uptime_sec=10790941, message=None
123+
)
124+
125+
assert parse_snmp_uptime([["1079094124", "124:21:26:42.03"]]) == Section(
126+
uptime_sec=10790802, message=None
127+
)
128+
129+
130+
def test_parse_snmp_uptime_parses_with_only_hr_system_up_timestamp() -> None:
131+
assert parse_snmp_uptime([["", "124:21:29:01.14"]]) == Section(
132+
uptime_sec=10790941, message=None
133+
)
134+
135+
136+
def test_parse_snmp_uptime_parses_with_only_sys_up_timestamp() -> None:
137+
assert parse_snmp_uptime([["124:21:26:42.03", ""]]) == Section(
138+
uptime_sec=10790802, message=None
139+
)

tests/unit/cmk/plugins/collection/agent_based/test_if64.py

Lines changed: 0 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -75,56 +75,3 @@ def test_check_timestamps_valid() -> None:
7575
value_store: dict[str, object] = {}
7676
assert not list(if64._check_timestamps({"a": 1, "b": 2}, value_store))
7777
assert not list(if64._check_timestamps({"a": 61, "b": 62}, value_store))
78-
79-
80-
def test_parse_snmp_uptime_pair_with_empty_section_is_none() -> None:
81-
parsed_uptime = if64.parse_snmp_uptime_pair([[]])
82-
assert parsed_uptime is None
83-
84-
85-
def test_parse_snmp_uptime_pair_with_no_values_is_not_valid() -> None:
86-
parsed_uptime = if64.parse_snmp_uptime_pair([["", ""]])
87-
88-
assert parsed_uptime is not None
89-
assert not parsed_uptime
90-
assert len(parsed_uptime.to_list()) == 0
91-
assert parsed_uptime.sys_uptime_sec is None
92-
assert parsed_uptime.hr_sys_uptime_sec is None
93-
94-
95-
def test_parse_snmp_uptime_pair_with_sys_uptime_only_has_parsed_value() -> None:
96-
parsed_uptime = if64.parse_snmp_uptime_pair([["500000", ""]])
97-
98-
assert parsed_uptime
99-
assert len(parsed_uptime.to_list()) == 1
100-
assert parsed_uptime.sys_uptime_sec == 5000
101-
assert parsed_uptime.hr_sys_uptime_sec is None
102-
103-
104-
def test_parse_snmp_uptime_pair_with_hr_sys_uptime_only_has_parsed_value() -> None:
105-
parsed_uptime = if64.parse_snmp_uptime_pair([["", "500000"]])
106-
107-
assert parsed_uptime
108-
assert len(parsed_uptime.to_list()) == 1
109-
assert parsed_uptime.sys_uptime_sec is None
110-
assert parsed_uptime.hr_sys_uptime_sec == 5000
111-
112-
113-
def test_parse_snmp_uptime_pair_with_both_uptime_values_has_parsed_values() -> None:
114-
parsed_uptime = if64.parse_snmp_uptime_pair([["500000", "500500"]])
115-
116-
assert parsed_uptime
117-
assert len(parsed_uptime.to_list()) == 2
118-
assert parsed_uptime.sys_uptime_sec == 5000
119-
assert parsed_uptime.hr_sys_uptime_sec == 5005
120-
121-
122-
def test_parse_snmp_uptime_pair_with_both_values_but_hr_uptime_is_zero_second_value_is_not_none() -> (
123-
None
124-
):
125-
parsed_uptime = if64.parse_snmp_uptime_pair([["500000", "0"]])
126-
127-
assert parsed_uptime
128-
assert len(parsed_uptime.to_list()) == 2
129-
assert parsed_uptime.sys_uptime_sec == 5000
130-
assert parsed_uptime.hr_sys_uptime_sec == 0

0 commit comments

Comments
 (0)