ctx_checker 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +19 -0
- data/README-zh.md +7 -1
- data/README.md +7 -1
- data/lib/ctx_checker/checks/aggregation.rb +7 -3
- data/lib/ctx_checker/checks/base.rb +8 -2
- data/lib/ctx_checker/cisco_ios/parsers/show_clock_detail.rb +1 -1
- data/lib/ctx_checker/cisco_ios/parsers/show_environment_all.rb +1 -1
- data/lib/ctx_checker/cisco_ios/parsers/show_etherchannel_summary.rb +1 -1
- data/lib/ctx_checker/cisco_ios/parsers/show_facility_alarm_status.rb +1 -1
- data/lib/ctx_checker/cisco_ios/parsers/show_interfaces.rb +1 -1
- data/lib/ctx_checker/cisco_ios/parsers/show_interfaces_transceiver_detail.rb +1 -1
- data/lib/ctx_checker/cisco_ios/parsers/show_ip_bgp_summary.rb +1 -1
- data/lib/ctx_checker/cisco_ios/parsers/show_ip_ospf_neighbor.rb +1 -1
- data/lib/ctx_checker/cisco_ios/parsers/show_ip_traffic.rb +1 -1
- data/lib/ctx_checker/cisco_ios/parsers/show_memory_statistics.rb +1 -1
- data/lib/ctx_checker/cisco_ios/parsers/show_ntp_status.rb +1 -1
- data/lib/ctx_checker/cisco_ios/parsers/show_processes_cpu.rb +1 -1
- data/lib/ctx_checker/cisco_nxos/parsers/show_environment.rb +1 -1
- data/lib/ctx_checker/cisco_nxos/parsers/show_interface_transceiver_details.rb +1 -1
- data/lib/ctx_checker/cisco_nxos/parsers/show_ip_traffic.rb +1 -1
- data/lib/ctx_checker/cisco_nxos/parsers/show_ntp_peer_status.rb +1 -1
- data/lib/ctx_checker/cisco_nxos/parsers/show_system_resources.rb +1 -1
- data/lib/ctx_checker/collector.rb +10 -3
- data/lib/ctx_checker/h3c/parsers/counters_table.rb +1 -1
- data/lib/ctx_checker/h3c/parsers/display_alarm_active.rb +1 -1
- data/lib/ctx_checker/h3c/parsers/display_bgp_peer.rb +1 -1
- data/lib/ctx_checker/h3c/parsers/display_clock.rb +1 -1
- data/lib/ctx_checker/h3c/parsers/display_cpu_usage.rb +1 -1
- data/lib/ctx_checker/h3c/parsers/display_environment.rb +1 -1
- data/lib/ctx_checker/h3c/parsers/display_fan.rb +1 -1
- data/lib/ctx_checker/h3c/parsers/display_interface.rb +2 -2
- data/lib/ctx_checker/h3c/parsers/display_link_aggregation_verbose.rb +1 -1
- data/lib/ctx_checker/h3c/parsers/display_memory.rb +2 -2
- data/lib/ctx_checker/h3c/parsers/display_ntp_service_status.rb +1 -1
- data/lib/ctx_checker/h3c/parsers/display_ospf_peer.rb +1 -1
- data/lib/ctx_checker/h3c/parsers/display_power.rb +1 -1
- data/lib/ctx_checker/h3c/parsers/display_transceiver_diagnosis.rb +1 -1
- data/lib/ctx_checker/hillstone/parsers/show_clock.rb +1 -1
- data/lib/ctx_checker/hillstone/parsers/show_cpu.rb +1 -1
- data/lib/ctx_checker/hillstone/parsers/show_cpu_cntr.rb +1 -1
- data/lib/ctx_checker/hillstone/parsers/show_environment.rb +1 -1
- data/lib/ctx_checker/hillstone/parsers/show_interface.rb +1 -1
- data/lib/ctx_checker/hillstone/parsers/show_interface_counter.rb +1 -1
- data/lib/ctx_checker/hillstone/parsers/show_ip_bgp_summary.rb +1 -1
- data/lib/ctx_checker/hillstone/parsers/show_memory.rb +1 -1
- data/lib/ctx_checker/hillstone/parsers/show_ntp_status.rb +1 -1
- data/lib/ctx_checker/hillstone/parsers/show_transceiver.rb +1 -1
- data/lib/ctx_checker/observation.rb +1 -2
- data/lib/ctx_checker/observation_merger.rb +4 -2
- data/lib/ctx_checker/palo_alto/parsers/show_clock.rb +1 -1
- data/lib/ctx_checker/palo_alto/parsers/show_counter_global.rb +1 -1
- data/lib/ctx_checker/palo_alto/parsers/show_counter_interface_all.rb +2 -2
- data/lib/ctx_checker/palo_alto/parsers/show_interface_all.rb +1 -1
- data/lib/ctx_checker/palo_alto/parsers/show_lacp_aggregate_ethernet.rb +1 -1
- data/lib/ctx_checker/palo_alto/parsers/show_ntp.rb +1 -1
- data/lib/ctx_checker/palo_alto/parsers/show_routing_protocol_bgp_peer.rb +1 -1
- data/lib/ctx_checker/palo_alto/parsers/show_routing_protocol_ospf_neighbor.rb +2 -2
- data/lib/ctx_checker/palo_alto/parsers/show_system_environmentals.rb +1 -1
- data/lib/ctx_checker/palo_alto/parsers/show_system_resources.rb +2 -2
- data/lib/ctx_checker/palo_alto/parsers/show_system_state_alarms.rb +1 -1
- data/lib/ctx_checker/palo_alto/parsers/show_transceiver_all.rb +1 -1
- data/lib/ctx_checker/parsers/base.rb +1 -1
- data/lib/ctx_checker/radware_alteon/parsers/info_l2_lacp_dump.rb +1 -1
- data/lib/ctx_checker/radware_alteon/parsers/info_l2_trunk.rb +1 -1
- data/lib/ctx_checker/radware_alteon/parsers/info_l3_bgp_summary.rb +1 -1
- data/lib/ctx_checker/radware_alteon/parsers/info_l3_ip.rb +1 -1
- data/lib/ctx_checker/radware_alteon/parsers/info_l3_ospf_nbr.rb +1 -1
- data/lib/ctx_checker/radware_alteon/parsers/info_link.rb +1 -1
- data/lib/ctx_checker/radware_alteon/parsers/info_sys_fan.rb +1 -1
- data/lib/ctx_checker/radware_alteon/parsers/info_sys_general.rb +1 -1
- data/lib/ctx_checker/radware_alteon/parsers/info_sys_ps.rb +1 -1
- data/lib/ctx_checker/radware_alteon/parsers/info_sys_temp.rb +1 -1
- data/lib/ctx_checker/radware_alteon/parsers/info_transceiver.rb +1 -1
- data/lib/ctx_checker/radware_alteon/parsers/stats_mp_cpu.rb +1 -1
- data/lib/ctx_checker/radware_alteon/parsers/stats_mp_mem.rb +1 -1
- data/lib/ctx_checker/radware_alteon/parsers/stats_port_ether.rb +1 -1
- data/lib/ctx_checker/radware_alteon/parsers/stats_port_if.rb +1 -1
- data/lib/ctx_checker/radware_alteon/parsers/stats_sp_allutils.rb +1 -1
- data/lib/ctx_checker/radware_alteon/parsers/stats_sys_ntp.rb +1 -1
- data/lib/ctx_checker/report.rb +6 -1
- data/lib/ctx_checker/snapshot.rb +3 -1
- data/lib/ctx_checker/system_policy.rb +1 -1
- data/lib/ctx_checker/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cfe9d7699f554a26eef174cf473835a6354cae53a94e35b4b010b9521c1702fb
|
|
4
|
+
data.tar.gz: 111dfc67e4e7acb4d77ce2e39d59962caf0a95a55dda4dfc111a1b8d9bef9df0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 329e9a0df103da02238c0c15fd1ca251254e9147821f14da142d946a530b40b7f506ea176f61eded7034099f2512248d83228136a506f19ebb384087ee740f55
|
|
7
|
+
data.tar.gz: ef5cf8c751dccfa83e6abe7af545e50d86a01c9934b9a42fd57503960f78770c0e38ce37559fe1df94b271ad6e84469bf11b02d43c7a6619ee85b5ebaafe188c
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
Changes to the public contract are recorded here. Versions follow Semantic Versioning.
|
|
4
4
|
|
|
5
|
+
## 0.2.2 (2026-10-18)
|
|
6
|
+
|
|
7
|
+
- Snapshot validated collection targets and dimensions before caller callbacks can alter them.
|
|
8
|
+
- Require lifecycle connectors to provide exactly one session, preventing silent empty collections and repeated queries.
|
|
9
|
+
- Reject unrequested observations as command fragments rather than upgrading them to complete evidence.
|
|
10
|
+
|
|
11
|
+
- Exercise the current optional net-connector API and share protocol receipts across custom-session tests without tying generic fixtures to connector result internals.
|
|
12
|
+
|
|
13
|
+
- Preserve proven aggregation capacity lower bounds in partial observations while retaining unknown overall coverage.
|
|
14
|
+
- Parse vendor lines and reconstruct H3C counters independently of Ruby's global record and output separators.
|
|
15
|
+
- Require valid dates and explicit UTC offsets in snapshot and command evidence timestamps, consistent with record timestamps.
|
|
16
|
+
- Count only complete member sources when proving aggregation capacity; incomplete sources cannot prove a capacity shortage.
|
|
17
|
+
- Index command diagnostics once per observation when constructing reports, avoiding repeated scans for expanded port queries.
|
|
18
|
+
- Add a synthetic report construction benchmark for 64, 256, and 512 failed queries.
|
|
19
|
+
|
|
20
|
+
## 0.2.1 (2026-10-17)
|
|
21
|
+
|
|
22
|
+
- Validate time-limit snapshot compatibility during policy construction; reject unsupported Numeric objects before evaluation while preserving exact Rational values.
|
|
23
|
+
|
|
5
24
|
## 0.2.0 (2026-10-17)
|
|
6
25
|
|
|
7
26
|
- Require ctx_parser and ctx_driver 0.2.x for the renamed public contracts.
|
data/README-zh.md
CHANGED
|
@@ -59,10 +59,14 @@ puts report.to_text
|
|
|
59
59
|
|
|
60
60
|
观察完整性 `complete/partial/failed/unsupported/not_requested/not_applicable` 与业务结论 `pass/fail/unknown/not_applicable` 分开。空回显不能独自证明健康或功能未配置。可信配置可证明可选 BGP、OSPF 或聚合检查不适用,必检策略仍优先。
|
|
61
61
|
|
|
62
|
+
快照字符串、命令证据和记录中的采样时间必须使用有效日期及明确的 `Z` 或 UTC 偏移;快照也接受 `Time` 对象。聚合容量仅计入有完整来源证据的成员,不完整成员不能证明容量充足,也不能证明容量不足。
|
|
63
|
+
|
|
62
64
|
error、CRC、discard 保持独立,不相加;单次累计值不能当作当前错误速率。增量要求身份一致、时间递增及可靠连续性证据。光功率需要设备或策略阈值;已有格式支持不代表所有型号/版本都已验证。
|
|
63
65
|
|
|
64
66
|
`Collector#collect` 接收负责会话生命周期的 connector;`collect_session` 借用已有会话。本 gem 不包含 Rails、库存数据库、调度或凭据。指标语义见[计数契约](COUNTER_CONTRACT.md)。
|
|
65
67
|
|
|
68
|
+
采集会在调用时钟或连接器前固定已校验的目标与所选维度。生命周期连接器的 `open` 必须恰好提供一次会话;未提供或提供两次均抛出 `ArgumentError`,第二次回调不会重复执行查询。合并命令结果时不接受 `not_requested` 观察片段。
|
|
69
|
+
|
|
66
70
|
详细厂商语义、历史验证记录和 Motor 专用集成说明保留在[架构参考](docs/architecture-zh.md)。其中宿主集成代码需要 Motor,不属于独立 gem API。适配层位于 [Motor 仓库](https://github.com/gatework/motor/tree/main/backend/lib) 的 `backend/lib/baseline`、`backend/lib/vxlan` 和 `backend/lib/wireless`。
|
|
67
71
|
|
|
68
72
|
生命周期 connector 是宿主适配器,提供 `open { |session| }`,VXLAN 和 Wireless 还要求 `platform`;原生 `Net::Connector::Base` 实例并不提供这一宿主生命周期协议。已连接的原生会话可传入 `CtxDriver::Reader` 或 `CtxChecker::Collector#collect_session`,不会隐式连接或重试。
|
|
@@ -75,6 +79,8 @@ error、CRC、discard 保持独立,不相加;单次累计值不能当作当
|
|
|
75
79
|
|
|
76
80
|
## 开发与测试
|
|
77
81
|
|
|
82
|
+
原生 net-connector 0.13 集成测试要求 ctx_driver 0.2.2 或更新版本。通用借用会话夹具直接实现回执协议,不依赖连接器的结果类。
|
|
83
|
+
|
|
78
84
|
共享类按实际职责命名,平台信息放在命名空间中。直接对应设备命令的解析器保留文件名和类名。
|
|
79
85
|
|
|
80
86
|
```sh
|
|
@@ -86,7 +92,7 @@ bundle exec rake build
|
|
|
86
92
|
|
|
87
93
|
`bundle exec rake` 执行 RSpec、RuboCop 及离线示例。测试放在 `spec/`,沿用项目 RSpec 约定;样例采用合成或脱敏证据。新增解析格式时补充成功、不完整、不支持及非法输入回归。离线测试不代表所有设备版本均通过实机验证。
|
|
88
94
|
|
|
89
|
-
运行 `ruby script/verify_package.rb /path/to/ctx_checker.gem`,验证使用已发布依赖的隔离安装。联动开发时可追加明确构建的依赖包路径,例如 `/path/to/ctx_parser.gem`,先安装这些候选包;候选包验证通过不代表注册表中的依赖已兼容。`bundle exec ruby -Ilib benchmark/runtime.rb`
|
|
95
|
+
运行 `ruby script/verify_package.rb /path/to/ctx_checker.gem`,验证使用已发布依赖的隔离安装。联动开发时可追加明确构建的依赖包路径,例如 `/path/to/ctx_parser.gem`,先安装这些候选包;候选包验证通过不代表注册表中的依赖已兼容。`bundle exec ruby -Ilib benchmark/runtime.rb` 用合成输入测量本地命令查找和诊断开销。`bundle exec ruby benchmark/report.rb` 测量 64、256、512 条失败查询的报告构建开销,不含网络和解析时间。
|
|
90
96
|
|
|
91
97
|
使用声明的最低 Ruby 版本检查兼容性。方法用途统一使用简明中文注释;复杂实现还需说明业务规则、实现思路及关键分支。保持公开返回值与错误契约。真实回显和凭据不可进入示例、fixture、日志或发布包。
|
|
92
98
|
|
data/README.md
CHANGED
|
@@ -59,10 +59,14 @@ puts report.to_text
|
|
|
59
59
|
|
|
60
60
|
Observation completeness (`complete`, `partial`, `failed`, `unsupported`, `not_requested`, `not_applicable`) is separate from findings (`pass`, `fail`, `unknown`, `not_applicable`). Empty output alone cannot prove a healthy or unconfigured feature. Confirmed configuration can establish that optional BGP, OSPF, or aggregation checks do not apply; required policy still takes precedence.
|
|
61
61
|
|
|
62
|
+
Sampling timestamps in snapshot strings, command evidence, and records require a valid ISO8601 date with an explicit `Z` or UTC offset. Snapshots also accept `Time` objects. Aggregation capacity counts only members supported by complete source evidence; incomplete members cannot establish either sufficient capacity or a shortage.
|
|
63
|
+
|
|
62
64
|
Errors, CRC, and discards are distinct counters and must not be summed. A single cumulative value is not a current error rate. Delta analysis needs matching identities, increasing timestamps, and reliable continuity evidence. Optics need device or policy thresholds; supported command formats do not guarantee every model/version is covered.
|
|
63
65
|
|
|
64
66
|
`Collector#collect` accepts an injected connector that owns its session lifecycle; `collect_session` borrows an existing session. No Rails, inventory database, scheduler, or credentials are included. See the [counter contract](COUNTER_CONTRACT.md) for metric semantics.
|
|
65
67
|
|
|
68
|
+
Collection snapshots the validated target and selected dimensions before calling the clock or connector. A lifecycle connector must yield exactly one session from `open`; yielding none or twice raises `ArgumentError`, and a second yield never repeats queries. Unrequested observations are not accepted as command fragments during combination.
|
|
69
|
+
|
|
66
70
|
Detailed vendor behavior, historical validation notes, and Motor-only integration are preserved in [the architecture reference](docs/architecture-zh.md). Host integration examples in that reference require Motor; they are not standalone gem APIs. Motor adapters live under `backend/lib/baseline`, `backend/lib/vxlan`, and `backend/lib/wireless` in the [Motor repository](https://github.com/gatework/motor/tree/main/backend/lib).
|
|
67
71
|
|
|
68
72
|
The lifecycle connector is a host adapter exposing `open { |session| }` (and `platform` for VXLAN and Wireless). It is distinct from a native `Net::Connector::Base` device instance. Borrowed native sessions are accepted by `CtxDriver::Reader` and `CtxChecker::Collector#collect_session`; they are never implicitly connected or retried.
|
|
@@ -75,6 +79,8 @@ Integer OSPF process IDs are normalized to strings in policies, observations, an
|
|
|
75
79
|
|
|
76
80
|
## Development and tests
|
|
77
81
|
|
|
82
|
+
Native net-connector 0.13 integration tests require ctx_driver 0.2.2 or newer. Generic borrowed-session fixtures implement the response protocol directly and remain independent of connector result classes.
|
|
83
|
+
|
|
78
84
|
Name shared classes by their responsibility and use the namespace for platform context. Preserve filenames and class names that directly match device commands.
|
|
79
85
|
|
|
80
86
|
```sh
|
|
@@ -86,7 +92,7 @@ bundle exec rake build
|
|
|
86
92
|
|
|
87
93
|
`bundle exec rake` runs RSpec, RuboCop, and the offline examples. Tests live in `spec/`, following the existing project's RSpec convention; fixtures use synthetic or sanitized evidence. Add regression cases for successful, incomplete, unsupported, and malformed inputs when extending a parser. Offline tests do not certify all device versions or replace live integration checks.
|
|
88
94
|
|
|
89
|
-
Run `ruby script/verify_package.rb /path/to/ctx_checker.gem` to verify an isolated installation with published dependencies. During coordinated development, append explicitly built dependency artifacts, such as `/path/to/ctx_parser.gem`, to install those candidates first. Candidate verification does not establish that the published dependencies are compatible. `bundle exec ruby -Ilib benchmark/runtime.rb` measures synthetic command lookup and diagnostic overhead locally.
|
|
95
|
+
Run `ruby script/verify_package.rb /path/to/ctx_checker.gem` to verify an isolated installation with published dependencies. During coordinated development, append explicitly built dependency artifacts, such as `/path/to/ctx_parser.gem`, to install those candidates first. Candidate verification does not establish that the published dependencies are compatible. `bundle exec ruby -Ilib benchmark/runtime.rb` measures synthetic command lookup and diagnostic overhead locally. `bundle exec ruby benchmark/report.rb` measures report construction for 64, 256, and 512 failed queries, excluding network and parsing time.
|
|
90
96
|
|
|
91
97
|
Use the declared minimum Ruby version for compatibility checks. Document source methods and non-obvious business logic in concise Chinese, preserve public return/error contracts, and never add raw device captures or credentials to examples, fixtures, logs, or release archives.
|
|
92
98
|
|
|
@@ -38,8 +38,12 @@ module CtxChecker
|
|
|
38
38
|
@policy.minimum_active_members.each do |group, minimum|
|
|
39
39
|
group_rows = rows.fetch(group, [])
|
|
40
40
|
members = group_rows.reject { |row| row[:member].nil? }
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
# 容量下界只计入完整来源;不完整成员也不能证明容量不足。
|
|
42
|
+
active_members = members.select { |row| record_complete?(row, @observation) && row[:member_status] == :active && row[:oper_status] == :up && !member_down?(row) }
|
|
43
|
+
active = active_members.size
|
|
44
|
+
known = group_rows.none? { |row| row[:member].nil? } && members.all? do |row|
|
|
45
|
+
record_complete?(row, @observation) && row[:member_status] && row[:member_status] != :unknown && %i[up down].include?(row[:oper_status])
|
|
46
|
+
end
|
|
43
47
|
status = if active >= minimum
|
|
44
48
|
:pass
|
|
45
49
|
elsif @observation.complete? && known
|
|
@@ -47,7 +51,7 @@ module CtxChecker
|
|
|
47
51
|
else
|
|
48
52
|
:unknown
|
|
49
53
|
end
|
|
50
|
-
results << finding(status, :aggregation_capacity, entity: group, actual: active, expected: minimum, severity: :critical)
|
|
54
|
+
results << finding(status, :aggregation_capacity, entity: group, actual: active, expected: minimum, severity: :critical, sources: active_members)
|
|
51
55
|
end
|
|
52
56
|
results
|
|
53
57
|
end
|
|
@@ -37,8 +37,14 @@ module CtxChecker
|
|
|
37
37
|
private
|
|
38
38
|
|
|
39
39
|
# 局部采集仍保留已知故障;判定成功则要求完整的支持证据。
|
|
40
|
-
def finding(status, reason, entity: nil, actual: nil, expected: nil, severity: :info, suffix: nil)
|
|
41
|
-
|
|
40
|
+
def finding(status, reason, entity: nil, actual: nil, expected: nil, severity: :info, suffix: nil, sources: nil)
|
|
41
|
+
# 跨记录结论必须由非空且全部完整的来源共同支持,不能只依赖维度状态。
|
|
42
|
+
complete = if sources
|
|
43
|
+
sources.any? && sources.all? { |record| record_complete?(record, @observation) }
|
|
44
|
+
else
|
|
45
|
+
record_complete?(@current_record, @observation)
|
|
46
|
+
end
|
|
47
|
+
if status == :pass && !complete
|
|
42
48
|
status, reason = :unknown, :incomplete_evidence
|
|
43
49
|
end
|
|
44
50
|
Finding.new(id: [ @observation.name, suffix || reason ].join("."), dimension: @observation.name,
|
|
@@ -16,7 +16,7 @@ module CtxChecker
|
|
|
16
16
|
def parse_output(text, complete:)
|
|
17
17
|
records, issues = [], diagnostics
|
|
18
18
|
time_source = nil
|
|
19
|
-
clean(text).each_line.with_index(1) do |source, number|
|
|
19
|
+
clean(text).each_line("\n").with_index(1) do |source, number|
|
|
20
20
|
line = source.strip
|
|
21
21
|
next if line.empty?
|
|
22
22
|
|
|
@@ -21,7 +21,7 @@ module CtxChecker
|
|
|
21
21
|
@temperature = nil
|
|
22
22
|
@sensor_table = false
|
|
23
23
|
@component_table = nil
|
|
24
|
-
clean(text).each_line.with_index(1) do |source, number|
|
|
24
|
+
clean(text).each_line("\n").with_index(1) do |source, number|
|
|
25
25
|
line = source.strip
|
|
26
26
|
next if line.empty? || /\A[- ]+\z/.match?(line)
|
|
27
27
|
next if catalyst_line(line) || stack_component_line(line) || sensor_line(line)
|
|
@@ -20,7 +20,7 @@ module CtxChecker
|
|
|
20
20
|
text = clean(text)
|
|
21
21
|
rows, issues, groups, totals = [], diagnostics, [], []
|
|
22
22
|
current, header, empty = nil, false, false
|
|
23
|
-
text.each_line.with_index(1) do |source, number|
|
|
23
|
+
text.each_line("\n").with_index(1) do |source, number|
|
|
24
24
|
line = source.strip
|
|
25
25
|
next if line.empty? || line.match?(/\A[-=+ ]+\z/)
|
|
26
26
|
|
|
@@ -12,7 +12,7 @@ module CtxChecker
|
|
|
12
12
|
# 解析 show facility alarm status,保留不完整或不支持的证据。
|
|
13
13
|
def parse_output(text, complete:)
|
|
14
14
|
records, issues, totals, severities = [], diagnostics, [], []
|
|
15
|
-
clean(text).each_line.with_index(1) do |source, number|
|
|
15
|
+
clean(text).each_line("\n").with_index(1) do |source, number|
|
|
16
16
|
line = source.strip
|
|
17
17
|
next if line.empty? || /\A[- ]+\z/.match?(line) || /\ASource\s+(?:Time\s+)?Severity\s+/.match?(line)
|
|
18
18
|
|
|
@@ -26,7 +26,7 @@ module CtxChecker
|
|
|
26
26
|
interfaces, counters, state_issues, counter_issues = [], [], diagnostics, diagnostics
|
|
27
27
|
current = nil
|
|
28
28
|
states = {}
|
|
29
|
-
text.each_line.with_index(1) do |source, number|
|
|
29
|
+
text.each_line("\n").with_index(1) do |source, number|
|
|
30
30
|
line = source.strip
|
|
31
31
|
next if line.empty?
|
|
32
32
|
|
|
@@ -18,7 +18,7 @@ module CtxChecker
|
|
|
18
18
|
text = clean(text)
|
|
19
19
|
rows, issues, ports = [], diagnostics, []
|
|
20
20
|
metric, header, lane_column = nil, false, false
|
|
21
|
-
text.each_line.with_index(1) do |source, number|
|
|
21
|
+
text.each_line("\n").with_index(1) do |source, number|
|
|
22
22
|
line = source.strip
|
|
23
23
|
next if line.empty? || line.match?(/\A[-= ]+\z/)
|
|
24
24
|
|
|
@@ -25,7 +25,7 @@ module CtxChecker
|
|
|
25
25
|
section = { header: false, empty: false, count: 0, scope_fields: [], scope: scope }
|
|
26
26
|
sections << section
|
|
27
27
|
pending = nil
|
|
28
|
-
text.each_line.with_index(1) do |source, number|
|
|
28
|
+
text.each_line("\n").with_index(1) do |source, number|
|
|
29
29
|
line = source.strip
|
|
30
30
|
next if line.empty? || line.match?(/\A[-= ]+\z/)
|
|
31
31
|
|
|
@@ -24,7 +24,7 @@ module CtxChecker
|
|
|
24
24
|
sections = []
|
|
25
25
|
section = { header: false, empty: false, count: 0, totals: [], declared: false, scope: scope }
|
|
26
26
|
sections << section
|
|
27
|
-
text.each_line.with_index(1) do |source, number|
|
|
27
|
+
text.each_line("\n").with_index(1) do |source, number|
|
|
28
28
|
line = source.strip
|
|
29
29
|
next if line.empty? || line.match?(/\A[-= ]+\z/)
|
|
30
30
|
|
|
@@ -31,7 +31,7 @@ module CtxChecker
|
|
|
31
31
|
records, issues = [], diagnostics
|
|
32
32
|
active = false
|
|
33
33
|
recognized = false
|
|
34
|
-
clean(text).each_line.with_index(1) do |source, number|
|
|
34
|
+
clean(text).each_line("\n").with_index(1) do |source, number|
|
|
35
35
|
line = source.strip
|
|
36
36
|
next if line.empty?
|
|
37
37
|
if line == "IP statistics:"
|
|
@@ -12,7 +12,7 @@ module CtxChecker
|
|
|
12
12
|
# 解析 show memory statistics,保留不完整或不支持的证据。
|
|
13
13
|
def parse_output(text, complete:)
|
|
14
14
|
records, issues = [], diagnostics
|
|
15
|
-
clean(text).each_line.with_index(1) do |source, number|
|
|
15
|
+
clean(text).each_line("\n").with_index(1) do |source, number|
|
|
16
16
|
line = source.strip
|
|
17
17
|
next if line.empty? || /\AHead\s+Total\(b\)/.match?(line) || /\ATracekey\s*:\s*[\da-f#]+\z/i.match?(line)
|
|
18
18
|
|
|
@@ -16,7 +16,7 @@ module CtxChecker
|
|
|
16
16
|
# 解析 show ntp status,保留不完整或不支持的证据。
|
|
17
17
|
def parse_output(text, complete:)
|
|
18
18
|
records, issues = [], diagnostics
|
|
19
|
-
clean(text).each_line.with_index(1) do |source, number|
|
|
19
|
+
clean(text).each_line("\n").with_index(1) do |source, number|
|
|
20
20
|
line = source.strip
|
|
21
21
|
next if line.empty?
|
|
22
22
|
|
|
@@ -16,7 +16,7 @@ module CtxChecker
|
|
|
16
16
|
def parse_output(text, complete:)
|
|
17
17
|
records, issues = [], diagnostics
|
|
18
18
|
recognized = false
|
|
19
|
-
clean(text).each_line.with_index(1) do |source, number|
|
|
19
|
+
clean(text).each_line("\n").with_index(1) do |source, number|
|
|
20
20
|
line = source.strip
|
|
21
21
|
next if line.empty?
|
|
22
22
|
|
|
@@ -22,7 +22,7 @@ module CtxChecker
|
|
|
22
22
|
@section = nil
|
|
23
23
|
@section_size = 0
|
|
24
24
|
@power_columns = nil
|
|
25
|
-
clean(text).each_line.with_index(1) do |source, number|
|
|
25
|
+
clean(text).each_line("\n").with_index(1) do |source, number|
|
|
26
26
|
line = source.strip
|
|
27
27
|
next if line.empty?
|
|
28
28
|
next if section_line(line) || header_line(line)
|
|
@@ -18,7 +18,7 @@ module CtxChecker
|
|
|
18
18
|
text = clean(text)
|
|
19
19
|
rows, issues, devices = [], diagnostics, []
|
|
20
20
|
current, lane, header, lane_valid = nil, nil, false, true
|
|
21
|
-
text.each_line.with_index(1) do |source, number|
|
|
21
|
+
text.each_line("\n").with_index(1) do |source, number|
|
|
22
22
|
line = source.strip
|
|
23
23
|
next if line.empty? || line.match?(/\A[-= ]+\z/)
|
|
24
24
|
|
|
@@ -57,7 +57,7 @@ module CtxChecker
|
|
|
57
57
|
section = nil
|
|
58
58
|
fragment_sections, acl_sections = 0, 0
|
|
59
59
|
ipv4 = false
|
|
60
|
-
clean(text).each_line.with_index(1) do |source, number|
|
|
60
|
+
clean(text).each_line("\n").with_index(1) do |source, number|
|
|
61
61
|
line = source.strip
|
|
62
62
|
next if line.empty?
|
|
63
63
|
if /\AIP Software Processed Traffic Statistics\z/i.match?(line)
|
|
@@ -19,7 +19,7 @@ module CtxChecker
|
|
|
19
19
|
caches, statuses = [], []
|
|
20
20
|
kernel_cache = {}
|
|
21
21
|
duplicate_cache = false
|
|
22
|
-
clean(text).each_line.with_index(1) do |source, number|
|
|
22
|
+
clean(text).each_line("\n").with_index(1) do |source, number|
|
|
23
23
|
line = source.strip
|
|
24
24
|
next if line.empty?
|
|
25
25
|
|
|
@@ -19,7 +19,7 @@ module CtxChecker
|
|
|
19
19
|
|
|
20
20
|
# 连接的建立与清理由传入连接器的块生命周期管理。
|
|
21
21
|
def collect(target:, connector:, dimensions: DEFAULT_DIMENSIONS, applicability: nil)
|
|
22
|
-
parser, identity = prepare(target, dimensions)
|
|
22
|
+
parser, identity, dimensions = prepare(target, dimensions)
|
|
23
23
|
if connector.respond_to?(:platform) && ParserRegistry.canonical(connector.platform) != parser.platform
|
|
24
24
|
raise ArgumentError, "Connector platform differs from the target"
|
|
25
25
|
end
|
|
@@ -29,12 +29,17 @@ module CtxChecker
|
|
|
29
29
|
|
|
30
30
|
issues = []
|
|
31
31
|
stage = :connect
|
|
32
|
+
yielded = false
|
|
32
33
|
begin
|
|
33
34
|
connector.open do |session|
|
|
35
|
+
raise ArgumentError, "Connector must provide one session" if yielded
|
|
36
|
+
|
|
37
|
+
yielded = true
|
|
34
38
|
stage = :collect
|
|
35
39
|
collect_into(session, parser, dimensions, observations)
|
|
36
40
|
stage = :close
|
|
37
41
|
end
|
|
42
|
+
raise ArgumentError, "Connector must provide one session" unless yielded
|
|
38
43
|
rescue StandardError => error
|
|
39
44
|
raise unless CtxDriver::Failure.connection_error?(error, connector)
|
|
40
45
|
|
|
@@ -48,7 +53,7 @@ module CtxChecker
|
|
|
48
53
|
|
|
49
54
|
# 借用已打开的 net-connector 会话,不负责建立或关闭连接。
|
|
50
55
|
def collect_session(target:, session:, dimensions: DEFAULT_DIMENSIONS, applicability: nil)
|
|
51
|
-
parser, identity = prepare(target, dimensions)
|
|
56
|
+
parser, identity, dimensions = prepare(target, dimensions)
|
|
52
57
|
observations = inapplicable_observations(parser, dimensions, applicability)
|
|
53
58
|
dimensions = dimensions - observations.keys
|
|
54
59
|
collect_into(session, parser, dimensions, observations) if dimensions.any?
|
|
@@ -87,13 +92,15 @@ module CtxChecker
|
|
|
87
92
|
# 发送任何设备命令前,先校验维度选择与目标身份。
|
|
88
93
|
def prepare(target, dimensions)
|
|
89
94
|
RuntimeParser.validate_dimensions!(dimensions)
|
|
95
|
+
dimensions = dimensions.dup.freeze
|
|
90
96
|
raise ArgumentError, "Target must be a hash" unless target.is_a?(Hash)
|
|
97
|
+
target = CtxParser::Immutable.copy(target)
|
|
91
98
|
|
|
92
99
|
parser = RuntimeParser.new(platform: target.fetch(:platform), model: target[:model], software_version: target[:software_version])
|
|
93
100
|
parser.discovery_dimensions(dimensions)
|
|
94
101
|
# 打开连接器前校验身份,避免将输入错误误报为连接故障。
|
|
95
102
|
snapshot = Snapshot.new(target: target.merge(platform: parser.platform), observed_at: @clock.call, observations: {})
|
|
96
|
-
[ parser, snapshot.target ]
|
|
103
|
+
[ parser, snapshot.target, dimensions ]
|
|
97
104
|
end
|
|
98
105
|
|
|
99
106
|
# 采集发现命令及其依赖命令,保留各自独立的失败结果。
|
|
@@ -14,7 +14,7 @@ module CtxChecker
|
|
|
14
14
|
def parse_counters(text, complete:, metric:)
|
|
15
15
|
source = clean(text)
|
|
16
16
|
rows, sections, section = [], [], nil
|
|
17
|
-
source.each_line do |raw|
|
|
17
|
+
source.each_line("\n") do |raw|
|
|
18
18
|
line = raw.strip
|
|
19
19
|
if /\AInterface\s+Total\s*\(pkts?\)\s+Broadcast\s*\(pkts?\)\s+Multicast\s*\(pkts?\)\s+Err\s*\(pkts?\)\z/i.match?(line)
|
|
20
20
|
sections << (section = { count: 0 })
|
|
@@ -16,7 +16,7 @@ module CtxChecker
|
|
|
16
16
|
def parse_output(text, complete:)
|
|
17
17
|
source = clean(text)
|
|
18
18
|
rows, header, empty = [], false, false
|
|
19
|
-
source.each_line do |raw|
|
|
19
|
+
source.each_line("\n") do |raw|
|
|
20
20
|
line = raw.strip
|
|
21
21
|
if /\ASeq\s+Level\s+Time\s+Info\z/i.match?(line)
|
|
22
22
|
header = true
|
|
@@ -15,7 +15,7 @@ module CtxChecker
|
|
|
15
15
|
def parse_output(text, complete:)
|
|
16
16
|
text = clean(text)
|
|
17
17
|
rows, issues, total, header = [], diagnostics, nil, false
|
|
18
|
-
text.each_line do |raw|
|
|
18
|
+
text.each_line("\n") do |raw|
|
|
19
19
|
line = raw.strip
|
|
20
20
|
if (match = /\ATotal number of peers:\s*(\d+)\z/i.match(line))
|
|
21
21
|
issues << { code: :duplicate_peer_count } unless total.nil?
|
|
@@ -15,7 +15,7 @@ module CtxChecker
|
|
|
15
15
|
def parse_output(text, complete:)
|
|
16
16
|
source = clean(text)
|
|
17
17
|
timestamps, zones, summer = [], [], false
|
|
18
|
-
source.each_line do |raw|
|
|
18
|
+
source.each_line("\n") do |raw|
|
|
19
19
|
line = raw.strip
|
|
20
20
|
if (match = /\A(\d{1,2}:\d{2}:\d{2}(?:\.\d+)?)\s+(\S+)\s+[A-Za-z]{3}\s+(\d{2})\/(\d{2})\/(\d{4})\z/.match(line))
|
|
21
21
|
timestamps << match.captures
|
|
@@ -16,7 +16,7 @@ module CtxChecker
|
|
|
16
16
|
def parse_output(text, complete:)
|
|
17
17
|
source = clean(text)
|
|
18
18
|
rows, components, current, table = [], [], nil, nil
|
|
19
|
-
source.each_line do |raw|
|
|
19
|
+
source.each_line("\n") do |raw|
|
|
20
20
|
line = raw.strip
|
|
21
21
|
if (match = /\A((?:Chassis \d+ )?Slot \d+(?: CPU \d+(?: Core \d+)?)?) CPU usage:\z/i.match(line))
|
|
22
22
|
current = scope_name(match[1])
|
|
@@ -18,7 +18,7 @@ module CtxChecker
|
|
|
18
18
|
def parse_output(text, complete:)
|
|
19
19
|
source = clean(text)
|
|
20
20
|
rows, chassis, header, auxiliary, contexts, context = [], nil, nil, nil, [], nil
|
|
21
|
-
source.each_line do |raw|
|
|
21
|
+
source.each_line("\n") do |raw|
|
|
22
22
|
line = raw.strip
|
|
23
23
|
if (match = /\AChassis (\d+):\z/i.match(line))
|
|
24
24
|
chassis = "chassis #{match[1]} "
|
|
@@ -16,7 +16,7 @@ module CtxChecker
|
|
|
16
16
|
def parse_output(text, complete:)
|
|
17
17
|
source = clean(text)
|
|
18
18
|
rows, location, current, table, seen, contexts, context, fanless = [], "system", nil, false, {}, [], nil, {}
|
|
19
|
-
source.each_line do |raw|
|
|
19
|
+
source.each_line("\n") do |raw|
|
|
20
20
|
line = raw.strip
|
|
21
21
|
if (match = /\A((?:Chassis \d+ )?Slot \d+|Chassis \d+)\s*:\z/i.match(line))
|
|
22
22
|
location, current, table = scope_name(match[1]), nil, false
|
|
@@ -52,7 +52,7 @@ module CtxChecker
|
|
|
52
52
|
# Never 或相对时间都不能证明跨重启的计数连续性。
|
|
53
53
|
discontinuity = nil
|
|
54
54
|
section_counters = []
|
|
55
|
-
counter_source = section.lines.reject { |line| /\A\s*Description:/i.match?(line) }.join
|
|
55
|
+
counter_source = section.lines("\n").reject { |line| /\A\s*Description:/i.match?(line) }.join("")
|
|
56
56
|
COUNTERS.each do |metric, pattern|
|
|
57
57
|
values = counter_source.scan(pattern).flatten
|
|
58
58
|
if DISCARD_LABELS.key?(metric) && values.size != counter_source.scan(DISCARD_LABELS.fetch(metric)).size
|
|
@@ -82,7 +82,7 @@ module CtxChecker
|
|
|
82
82
|
# 换行 VLAN 列表仅在紧接前一列表时作为元数据处理。
|
|
83
83
|
def known_details?(section)
|
|
84
84
|
vlan_list = false
|
|
85
|
-
section.lines.drop(1).all? do |raw|
|
|
85
|
+
section.lines("\n").drop(1).all? do |raw|
|
|
86
86
|
line = raw.strip
|
|
87
87
|
if (match = /\A(?:VLAN (?:Passing|permitted)|(?:Tagged|Untagged)\s+VLAN ID)\s*:\s*(.+)\z/i.match(line))
|
|
88
88
|
vlan_list = valid_vlan_list?(match[1])
|
|
@@ -22,7 +22,7 @@ module CtxChecker
|
|
|
22
22
|
return build_result([], complete: complete, recognized: true, issues: [ { code: :aggregation_not_configured } ])
|
|
23
23
|
end
|
|
24
24
|
rows, issues, group, remote, groups, columns, tables = [], diagnostics, nil, false, {}, nil, []
|
|
25
|
-
text.each_line do |raw|
|
|
25
|
+
text.each_line("\n") do |raw|
|
|
26
26
|
line = raw.strip
|
|
27
27
|
if (match = /\AAggregate Interface:\s*(.*)\z/i.match(line))
|
|
28
28
|
group, remote, columns = nil, false, nil
|
|
@@ -20,7 +20,7 @@ module CtxChecker
|
|
|
20
20
|
|
|
21
21
|
rows, contexts, sections = [], [], []
|
|
22
22
|
context, scope, unit, columns, current, section = nil, "physical", nil, nil, nil, nil
|
|
23
|
-
source.each_line do |raw|
|
|
23
|
+
source.each_line("\n") do |raw|
|
|
24
24
|
line = raw.strip
|
|
25
25
|
if /\A(?:Container memory statistics|Memory statistics|The statistics about memory).*measured in (KB|bytes)(?:\(Available\))?:\z/i.match?(line)
|
|
26
26
|
unit = line.match(/measured in (KB|bytes)/i)[1].casecmp?("KB") ? 1024 : 1
|
|
@@ -64,7 +64,7 @@ module CtxChecker
|
|
|
64
64
|
# 旧交换机仅报告物理内存使用量,不能证明缓存调整后的应用使用量。
|
|
65
65
|
def legacy_memory(source, complete:)
|
|
66
66
|
values, seen = {}, {}
|
|
67
|
-
source.each_line do |raw|
|
|
67
|
+
source.each_line("\n") do |raw|
|
|
68
68
|
line = raw.strip
|
|
69
69
|
next if line.empty?
|
|
70
70
|
|
|
@@ -20,7 +20,7 @@ module CtxChecker
|
|
|
20
20
|
def parse_output(text, complete:)
|
|
21
21
|
source = clean(text)
|
|
22
22
|
row, seen, local_sync = { component: "system" }, {}, []
|
|
23
|
-
source.each_line do |raw|
|
|
23
|
+
source.each_line("\n") do |raw|
|
|
24
24
|
line = raw.strip
|
|
25
25
|
next if line.empty?
|
|
26
26
|
key, value = line.split(/:\s*/, 2)
|
|
@@ -17,7 +17,7 @@ module CtxChecker
|
|
|
17
17
|
return build_result([], complete: complete, recognized: true, issues: [ { code: :protocol_not_configured } ])
|
|
18
18
|
end
|
|
19
19
|
rows, issues, process, area, header = [], diagnostics, nil, nil, false
|
|
20
|
-
text.each_line do |raw|
|
|
20
|
+
text.each_line("\n") do |raw|
|
|
21
21
|
line = raw.strip
|
|
22
22
|
if (match = /\AOSPF Process (\d+) with Router ID ([\d.]+)\z/i.match(line))
|
|
23
23
|
process, area = match[1], nil
|
|
@@ -16,7 +16,7 @@ module CtxChecker
|
|
|
16
16
|
def parse_output(text, complete:)
|
|
17
17
|
source = clean(text)
|
|
18
18
|
rows, location, columns, contexts, context = [], "system", nil, [], nil
|
|
19
|
-
source.each_line do |raw|
|
|
19
|
+
source.each_line("\n") do |raw|
|
|
20
20
|
line = raw.strip
|
|
21
21
|
if (match = /\A((?:Chassis \d+ )?Slot \d+|Chassis \d+)\s*:\z/i.match(line))
|
|
22
22
|
location, columns = scope_name(match[1]), nil
|
|
@@ -27,7 +27,7 @@ module CtxChecker
|
|
|
27
27
|
next
|
|
28
28
|
end
|
|
29
29
|
name = CtxChecker::Parsers::InterfaceNames.normalize(match[1], platform: :h3c)
|
|
30
|
-
lines = block.lines.drop(1).map(&:strip).reject(&:empty?)
|
|
30
|
+
lines = block.lines("\n").drop(1).map(&:strip).reject(&:empty?)
|
|
31
31
|
if (state = availability(lines, name))
|
|
32
32
|
rows.concat(measurements(name, nil, nil, nil, **state))
|
|
33
33
|
next
|
|
@@ -14,7 +14,7 @@ module CtxChecker
|
|
|
14
14
|
# 要求当前采样及回显声明的各标准采样时间窗均有证据。
|
|
15
15
|
def parse_output(text, complete:)
|
|
16
16
|
rows = []
|
|
17
|
-
clean(text).each_line.with_index(1) do |raw, number|
|
|
17
|
+
clean(text).each_line("\n").with_index(1) do |raw, number|
|
|
18
18
|
line = raw.strip
|
|
19
19
|
if (match = /\A(Average|Current) cpu utilization\s*:\s*(\S+)\s*\z/i.match(line))
|
|
20
20
|
rows << sample(match[2], scope: match[1].casecmp?("average") ? "system_average" : "system", seconds: nil)
|
|
@@ -22,7 +22,7 @@ module CtxChecker
|
|
|
22
22
|
def parse_output(text, complete:)
|
|
23
23
|
records, contexts = [], []
|
|
24
24
|
context, queue, summary = nil, nil, false
|
|
25
|
-
clean(text).each_line.with_index(1) do |raw, number|
|
|
25
|
+
clean(text).each_line("\n").with_index(1) do |raw, number|
|
|
26
26
|
line = raw.strip
|
|
27
27
|
if /\Acpu Port Send\s+Rev Counter:\z/i.match?(line)
|
|
28
28
|
summary = true
|
|
@@ -22,7 +22,7 @@ module CtxChecker
|
|
|
22
22
|
rows = { temperature: [], environment: [], power: [] }
|
|
23
23
|
issues = rows.transform_values { diagnostics }
|
|
24
24
|
contexts, section, header = [], nil, false
|
|
25
|
-
clean(text).each_line.with_index(1) do |raw, number|
|
|
25
|
+
clean(text).each_line("\n").with_index(1) do |raw, number|
|
|
26
26
|
line = raw.strip
|
|
27
27
|
if (match = /\A(.+ information(?:\(C\))?):\z/.match(line))
|
|
28
28
|
section, header = SECTIONS[match[1]], false
|
|
@@ -18,7 +18,7 @@ module CtxChecker
|
|
|
18
18
|
# 识别无地址链路时,仍保留 IP 协议 down 状态。
|
|
19
19
|
def parse_output(text, complete:)
|
|
20
20
|
rows, issues, header = [], diagnostics, false
|
|
21
|
-
clean(text).each_line do |raw|
|
|
21
|
+
clean(text).each_line("\n") do |raw|
|
|
22
22
|
line = raw.strip
|
|
23
23
|
next if line.empty? || /\A[-=]+\z/.match?(line) || LEGEND.match?(line)
|
|
24
24
|
|
|
@@ -15,7 +15,7 @@ module CtxChecker
|
|
|
15
15
|
# 解释计数器前先校验列顺序;缺少采样时保留 nil。
|
|
16
16
|
def parse_output(text, complete:)
|
|
17
17
|
rows, issues, header, count = [], diagnostics, false, 0
|
|
18
|
-
clean(text).each_line do |raw|
|
|
18
|
+
clean(text).each_line("\n") do |raw|
|
|
19
19
|
line = raw.strip
|
|
20
20
|
next if line.empty? || /\A[-=]+\z/.match?(line)
|
|
21
21
|
|
|
@@ -11,7 +11,7 @@ module CtxChecker
|
|
|
11
11
|
|
|
12
12
|
# 仅支持已观察到的 StoneOS 格式,不假设使用 Cisco 表格语法。
|
|
13
13
|
def parse_output(text, complete:)
|
|
14
|
-
lines = clean(text).lines.map(&:strip).reject(&:empty?)
|
|
14
|
+
lines = clean(text).lines("\n").map(&:strip).reject(&:empty?)
|
|
15
15
|
recognized = lines == [ "No IPv4 neighbor is configured" ]
|
|
16
16
|
issues = [ { code: recognized ? :protocol_not_configured : :unrecognized_bgp_output } ]
|
|
17
17
|
build_result([], complete: complete, recognized: recognized, issues: issues)
|
|
@@ -15,7 +15,7 @@ module CtxChecker
|
|
|
15
15
|
def parse_output(text, complete:)
|
|
16
16
|
percentages, totals, histories = [], [], []
|
|
17
17
|
header_count, history = 0, nil
|
|
18
|
-
clean(text).each_line.with_index(1) do |raw, number|
|
|
18
|
+
clean(text).each_line("\n").with_index(1) do |raw, number|
|
|
19
19
|
line = raw.strip
|
|
20
20
|
if (match = /\AThe percentage of memory utilization:\s*(\S+)\s*%\z/i.match(line))
|
|
21
21
|
percentages << percentage(match[1])
|
|
@@ -15,7 +15,7 @@ module CtxChecker
|
|
|
15
15
|
def parse_output(text, complete:)
|
|
16
16
|
source = clean(text)
|
|
17
17
|
rows, clients, versions, settings = [], [], [], []
|
|
18
|
-
source.each_line.with_index(1) do |raw, number|
|
|
18
|
+
source.each_line("\n").with_index(1) do |raw, number|
|
|
19
19
|
line = raw.strip
|
|
20
20
|
next if line.empty?
|
|
21
21
|
|
|
@@ -17,7 +17,7 @@ module CtxChecker
|
|
|
17
17
|
# 逐模块隔离,避免未知块将测量值错误关联到前一模块。
|
|
18
18
|
def parse_output(text, complete:)
|
|
19
19
|
rows, issues, blocks, current, header = [], diagnostics, [], nil, false
|
|
20
|
-
clean(text).each_line do |raw|
|
|
20
|
+
clean(text).each_line("\n") do |raw|
|
|
21
21
|
line = raw.strip
|
|
22
22
|
next if line.empty? || /\A=+\z/.match?(line)
|
|
23
23
|
|
|
@@ -68,8 +68,7 @@ module CtxChecker
|
|
|
68
68
|
raise ArgumentError, "Invalid evidence byte count" if entry.key?(:bytes) && !(entry[:bytes].is_a?(Integer) && entry[:bytes] >= 0)
|
|
69
69
|
raise ArgumentError, "Invalid evidence reference" if entry.key?(:reference) && !text?(entry[:reference])
|
|
70
70
|
if entry.key?(:observed_at)
|
|
71
|
-
raise ArgumentError, "Invalid evidence time" unless entry[:observed_at]
|
|
72
|
-
Time.iso8601(entry[:observed_at])
|
|
71
|
+
raise ArgumentError, "Invalid evidence time" unless SystemFields.timestamp?(entry[:observed_at])
|
|
73
72
|
end
|
|
74
73
|
end
|
|
75
74
|
@records, @issues, @evidence = [ normalized, issues, evidence ].map { |value| CtxParser::Immutable.copy(value) }
|
|
@@ -8,8 +8,10 @@ module CtxChecker
|
|
|
8
8
|
class ObservationMerger
|
|
9
9
|
# 归并已声明的命令证据,不丢失来源失败或身份歧义。
|
|
10
10
|
def self.call(name, observations, commands:, required_metrics: [])
|
|
11
|
-
unless observations.is_a?(Array) && observations.all?
|
|
12
|
-
|
|
11
|
+
unless observations.is_a?(Array) && observations.all? do |observation|
|
|
12
|
+
observation.is_a?(Observation) && observation.name == name && observation.status != :not_requested
|
|
13
|
+
end
|
|
14
|
+
raise ArgumentError, "Command observations must share a requested dimension"
|
|
13
15
|
end
|
|
14
16
|
sources = observations.map do |observation|
|
|
15
17
|
source = observation.evidence.map { |entry| entry[:command] }.compact.uniq
|
|
@@ -15,7 +15,7 @@ module CtxChecker
|
|
|
15
15
|
def parse_output(text, complete:)
|
|
16
16
|
source = clean(text)
|
|
17
17
|
rows, issues = [], diagnostics
|
|
18
|
-
source.each_line.with_index(1) do |raw, number|
|
|
18
|
+
source.each_line("\n").with_index(1) do |raw, number|
|
|
19
19
|
next if raw.strip.empty?
|
|
20
20
|
match = /\A(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+([A-Z][a-z]{2})\s+(\d{1,2})\s+(\d{2}:\d{2}:\d{2})\s+(\S+)\s+(\d{4})\z/.match(raw.strip)
|
|
21
21
|
unless match
|
|
@@ -18,7 +18,7 @@ module CtxChecker
|
|
|
18
18
|
source = clean(text)
|
|
19
19
|
rows, totals, issues = [], [], diagnostics
|
|
20
20
|
title, header = false, false
|
|
21
|
-
source.each_line.with_index(1) do |raw, number|
|
|
21
|
+
source.each_line("\n").with_index(1) do |raw, number|
|
|
22
22
|
line = raw.strip
|
|
23
23
|
if line == "Global counters:"
|
|
24
24
|
title = true
|
|
@@ -24,7 +24,7 @@ module CtxChecker
|
|
|
24
24
|
next if block.strip.empty?
|
|
25
25
|
match = /\A\s*Interface:\s*([^\n]+)\n/i.match(block)
|
|
26
26
|
unless match
|
|
27
|
-
lines = block.lines.map(&:strip).reject { |line| line.empty? || line.match?(/\A-+\z/) }
|
|
27
|
+
lines = block.lines("\n").map(&:strip).reject { |line| line.empty? || line.match?(/\A-+\z/) }
|
|
28
28
|
issues << { code: :unrecognized_counter_block } unless lines == [ "Hardware interface counters:" ]
|
|
29
29
|
next
|
|
30
30
|
end
|
|
@@ -33,7 +33,7 @@ module CtxChecker
|
|
|
33
33
|
kind = CtxChecker::Parsers::InterfaceNames.kind(name)
|
|
34
34
|
wanted = kind == :physical ? :hardware : :logical
|
|
35
35
|
section, values = nil, []
|
|
36
|
-
block.lines.drop(1).each do |raw|
|
|
36
|
+
block.lines("\n").drop(1).each do |raw|
|
|
37
37
|
line = raw.strip
|
|
38
38
|
if (heading = /\A(Hardware|Logical) interface counters(?: read from CPU)?:\z/i.match(line))
|
|
39
39
|
section = heading[1].downcase.to_sym
|
|
@@ -15,7 +15,7 @@ module CtxChecker
|
|
|
15
15
|
text = clean(text)
|
|
16
16
|
rows, issues, section, totals, counts, duplicates = {}, diagnostics, nil, {}, Hash.new(0), []
|
|
17
17
|
logical_names = {}
|
|
18
|
-
text.each_line do |raw|
|
|
18
|
+
text.each_line("\n") do |raw|
|
|
19
19
|
line = raw.strip
|
|
20
20
|
if (match = /\Atotal configured (hardware|logical) interfaces:\s*(\d+)\z/i.match(line))
|
|
21
21
|
section = match[1].downcase.to_sym
|
|
@@ -14,7 +14,7 @@ module CtxChecker
|
|
|
14
14
|
def parse_output(text, complete:)
|
|
15
15
|
text = clean(text)
|
|
16
16
|
rows, issues, group, header, groups = [], diagnostics, nil, false, {}
|
|
17
|
-
text.each_line do |raw|
|
|
17
|
+
text.each_line("\n") do |raw|
|
|
18
18
|
line = raw.strip
|
|
19
19
|
if (match = /\AAE group:\s*(ae\d+)\z/i.match(line))
|
|
20
20
|
group = match[1].downcase
|
|
@@ -14,7 +14,7 @@ module CtxChecker
|
|
|
14
14
|
source = clean(text)
|
|
15
15
|
rows, globals, issues = [], [], diagnostics
|
|
16
16
|
current, fields, header, peer_fields = nil, {}, false, {}
|
|
17
|
-
source.each_line.with_index(1) do |raw, number|
|
|
17
|
+
source.each_line("\n").with_index(1) do |raw, number|
|
|
18
18
|
line = raw.strip
|
|
19
19
|
next if line.empty?
|
|
20
20
|
if line == "NTP state:"
|
|
@@ -22,7 +22,7 @@ module CtxChecker
|
|
|
22
22
|
issues << { code: :unrecognized_bgp_block }
|
|
23
23
|
next
|
|
24
24
|
end
|
|
25
|
-
if block.lines.any? { |line| !line.strip.empty? && !FIELDS.match?(line.strip) }
|
|
25
|
+
if block.lines("\n").any? { |line| !line.strip.empty? && !FIELDS.match?(line.strip) }
|
|
26
26
|
issues << { code: :unrecognized_bgp_field }
|
|
27
27
|
end
|
|
28
28
|
if [ "Remote Address", "Peer status", "Remote AS", "virtual router" ].any? { |field| block.scan(/^\s*#{Regexp.escape(field)}:/i).size > 1 }
|
|
@@ -21,13 +21,13 @@ module CtxChecker
|
|
|
21
21
|
text.split(/(?=^\s*virtual router:)/i).each do |block|
|
|
22
22
|
unless block.match?(/\A\s*virtual router:/i)
|
|
23
23
|
# 文档规定的图例位于首个邻居前,任意前导文本不构成证据。
|
|
24
|
-
lines = block.lines.map(&:strip).reject(&:empty?)
|
|
24
|
+
lines = block.lines("\n").map(&:strip).reject(&:empty?)
|
|
25
25
|
unless lines.all? { |line| line.match?(/\A(?:=+|Options: 0x80:reserved,.*|N\/P:NSSA option,.*)\z/i) }
|
|
26
26
|
issues << { code: :unrecognized_ospf_block }
|
|
27
27
|
end
|
|
28
28
|
next
|
|
29
29
|
end
|
|
30
|
-
issues << { code: :unrecognized_ospf_field } if block.lines.any? { |line| !line.strip.empty? && !FIELDS.match?(line.strip) }
|
|
30
|
+
issues << { code: :unrecognized_ospf_field } if block.lines("\n").any? { |line| !line.strip.empty? && !FIELDS.match?(line.strip) }
|
|
31
31
|
if [ "neighbor address", "neighbor router ID", "status" ].any? { |field| block.scan(/^\s*#{Regexp.escape(field)}:/i).size > 1 }
|
|
32
32
|
issues << { code: :duplicate_ospf_field }
|
|
33
33
|
next
|
|
@@ -19,7 +19,7 @@ module CtxChecker
|
|
|
19
19
|
rows = { temperature: [], environment: [], power: [] }
|
|
20
20
|
issues = rows.transform_values { diagnostics }
|
|
21
21
|
section, header, sections, section_rows = nil, nil, [], 0
|
|
22
|
-
source.each_line.with_index(1) do |raw, number|
|
|
22
|
+
source.each_line("\n").with_index(1) do |raw, number|
|
|
23
23
|
line = raw.strip
|
|
24
24
|
next if line.empty?
|
|
25
25
|
if (match = /\A-+\s*([^-]+?)\s*-+\z/.match(line))
|
|
@@ -17,9 +17,9 @@ module CtxChecker
|
|
|
17
17
|
def parse_output(text, complete:)
|
|
18
18
|
source = clean(text)
|
|
19
19
|
cpus, memories, cpu_issues, memory_issues, shared_issues = [], [], diagnostics, diagnostics, diagnostics
|
|
20
|
-
swap_lines = source.lines.grep(/\A\s*(?:(?:KiB|MiB|GiB|TiB)\s+)?Swap\s*:/)
|
|
20
|
+
swap_lines = source.lines("\n").grep(/\A\s*(?:(?:KiB|MiB|GiB|TiB)\s+)?Swap\s*:/)
|
|
21
21
|
process_table = false
|
|
22
|
-
source.each_line.with_index(1) do |raw, number|
|
|
22
|
+
source.each_line("\n").with_index(1) do |raw, number|
|
|
23
23
|
line = raw.strip
|
|
24
24
|
if (match = /\A%?Cpu(?:\(s\)|(\d+))\s*:\s*(.*)\z/i.match(line))
|
|
25
25
|
cpus << cpu(match, cpu_issues)
|
|
@@ -13,7 +13,7 @@ module CtxChecker
|
|
|
13
13
|
def parse_output(text, complete:)
|
|
14
14
|
source = clean(text)
|
|
15
15
|
rows, issues = [], diagnostics
|
|
16
|
-
source.each_line.with_index(1) do |raw, number|
|
|
16
|
+
source.each_line("\n").with_index(1) do |raw, number|
|
|
17
17
|
line = raw.strip
|
|
18
18
|
# 配置阈值和复制的 HA 对端状态不属于本地当前告警。
|
|
19
19
|
next if line.empty? || line.match?(/\A(?:cfg\.|peer\.)/)
|
|
@@ -15,7 +15,7 @@ module CtxChecker
|
|
|
15
15
|
def parse_output(text, complete:)
|
|
16
16
|
text = clean(text)
|
|
17
17
|
rows, issues, header = [], diagnostics, false
|
|
18
|
-
text.each_line do |raw|
|
|
18
|
+
text.each_line("\n") do |raw|
|
|
19
19
|
line = raw.strip
|
|
20
20
|
if /\APort\s+Temperature\s+Voltage\s+Current\s+Tx Power\s+Rx Power\z/i.match?(line)
|
|
21
21
|
header = true
|
|
@@ -23,7 +23,7 @@ module CtxChecker
|
|
|
23
23
|
@unsupported = false
|
|
24
24
|
@command_failed = false
|
|
25
25
|
source = CtxParser::Input.text(text, encoding: :transcode, newlines: :lf, max_bytes: MAX_BYTES)
|
|
26
|
-
source.each_line.with_index(1) do |line, number|
|
|
26
|
+
source.each_line("\n").with_index(1) do |line, number|
|
|
27
27
|
if /\A\s*(?:(?:%\s*)?(?:Error:\s*)?(?:Invalid (?:input|command|syntax)|Unrecognized command|Unknown command|command not (?:found|supported)|not supported on this|syntax error)|\^-+\s*unrecognized keyword\b)/i.match?(line)
|
|
28
28
|
@unsupported = true
|
|
29
29
|
@issues << { code: :unsupported_command, line: number }
|
|
@@ -15,7 +15,7 @@ module CtxChecker
|
|
|
15
15
|
# 活跃模式需要额外运行证据,不能由重复的 trunk 记录替代。
|
|
16
16
|
def parse_output(text, complete:)
|
|
17
17
|
issues, ports, header = diagnostics, {}, false
|
|
18
|
-
clean(text).each_line do |raw|
|
|
18
|
+
clean(text).each_line("\n") do |raw|
|
|
19
19
|
line = raw.strip
|
|
20
20
|
next if line.empty? || /\A-+\z/.match?(line)
|
|
21
21
|
|
|
@@ -15,7 +15,7 @@ module CtxChecker
|
|
|
15
15
|
# 将成员限定在其所属 trunk 中,明确保留不完整聚合组。
|
|
16
16
|
def parse_output(text, complete:)
|
|
17
17
|
rows, issues, groups, current = [], diagnostics, {}, nil
|
|
18
|
-
clean(text).each_line do |raw|
|
|
18
|
+
clean(text).each_line("\n") do |raw|
|
|
19
19
|
line = raw.strip
|
|
20
20
|
next if line.empty? || /\A-+\z/.match?(line)
|
|
21
21
|
|
|
@@ -11,7 +11,7 @@ module CtxChecker
|
|
|
11
11
|
|
|
12
12
|
# 协议明确禁用不代表策略要求的对等体健康。
|
|
13
13
|
def parse_output(text, complete:)
|
|
14
|
-
lines = clean(text).lines.map(&:strip).reject(&:empty?)
|
|
14
|
+
lines = clean(text).lines("\n").map(&:strip).reject(&:empty?)
|
|
15
15
|
disabled = lines == [ "BGP is disabled." ]
|
|
16
16
|
issues = [ { code: disabled ? :protocol_disabled : :unrecognized_bgp_output } ]
|
|
17
17
|
build_result([], complete: complete, recognized: disabled, issues: issues)
|
|
@@ -16,7 +16,7 @@ module CtxChecker
|
|
|
16
16
|
def parse_output(text, complete:)
|
|
17
17
|
rows, issues, section = [], diagnostics, :preamble
|
|
18
18
|
header, interfaces, ipv6, closed = false, false, false, false
|
|
19
|
-
clean(text).each_line do |raw|
|
|
19
|
+
clean(text).each_line("\n") do |raw|
|
|
20
20
|
line = raw.strip
|
|
21
21
|
next if line.empty? || /\A-+\z/.match?(line)
|
|
22
22
|
|
|
@@ -11,7 +11,7 @@ module CtxChecker
|
|
|
11
11
|
|
|
12
12
|
# 在验证活跃 Alteon 邻居表前,保留明确禁用状态。
|
|
13
13
|
def parse_output(text, complete:)
|
|
14
|
-
lines = clean(text).lines.map(&:strip).reject(&:empty?)
|
|
14
|
+
lines = clean(text).lines("\n").map(&:strip).reject(&:empty?)
|
|
15
15
|
disabled = lines == [ "OSPF is disabled." ]
|
|
16
16
|
issues = [ { code: disabled ? :protocol_disabled : :unrecognized_ospf_output } ]
|
|
17
17
|
build_result([], complete: complete, recognized: disabled, issues: issues)
|
|
@@ -15,7 +15,7 @@ module CtxChecker
|
|
|
15
15
|
# 链路行缺失不能证明端口已禁用或健康。
|
|
16
16
|
def parse_output(text, complete:)
|
|
17
17
|
rows, issues, header = [], diagnostics, false
|
|
18
|
-
clean(text).each_line do |raw|
|
|
18
|
+
clean(text).each_line("\n") do |raw|
|
|
19
19
|
line = raw.strip
|
|
20
20
|
next if line.empty? || /\A[-\s]+\z/.match?(line)
|
|
21
21
|
next if /\A-+\s+-+\s+-+\s+-+\s+--TX---RX--\s+-+\z/.match?(line)
|
|
@@ -12,7 +12,7 @@ module CtxChecker
|
|
|
12
12
|
# 校验每条风扇记录,并按厂商关键性定义确定故障严重级别。
|
|
13
13
|
def parse_output(text, complete:)
|
|
14
14
|
rows, tables, headers = [], 0, 0
|
|
15
|
-
clean(text).each_line.with_index(1) do |raw, number|
|
|
15
|
+
clean(text).each_line("\n").with_index(1) do |raw, number|
|
|
16
16
|
line = raw.strip
|
|
17
17
|
if line == "Fans table:"
|
|
18
18
|
tables += 1
|
|
@@ -13,7 +13,7 @@ module CtxChecker
|
|
|
13
13
|
|
|
14
14
|
# 仅投影当前时钟,不使用启动、应用或保存时间戳。
|
|
15
15
|
def parse_output(text, complete:)
|
|
16
|
-
lines = clean(text).lines.map(&:strip)
|
|
16
|
+
lines = clean(text).lines("\n").map(&:strip)
|
|
17
17
|
clocks = lines.grep(/\ASystem Information at\b/)
|
|
18
18
|
zones = lines.grep(/\ATime zone:/)
|
|
19
19
|
@issues << { code: :ambiguous_clock_time, count: clocks.size } unless clocks.one?
|
|
@@ -14,7 +14,7 @@ module CtxChecker
|
|
|
14
14
|
# 未识别的汇总状态保持不可用,不推定冗余供电正常。
|
|
15
15
|
def parse_output(text, complete:)
|
|
16
16
|
rows = []
|
|
17
|
-
clean(text).each_line.with_index(1) do |raw, number|
|
|
17
|
+
clean(text).each_line("\n").with_index(1) do |raw, number|
|
|
18
18
|
line = raw.strip
|
|
19
19
|
if (match = /\A(Single|Dual) Power Supply Status:\s*(.+)\z/i.match(line))
|
|
20
20
|
state = STATES.fetch(match[2].downcase, :unknown)
|
|
@@ -15,7 +15,7 @@ module CtxChecker
|
|
|
15
15
|
# 阈值缺失时保留未知,拒绝矛盾的汇总或范围证据。
|
|
16
16
|
def parse_output(text, complete:)
|
|
17
17
|
rows, highs, criticals, summaries = [], [], [], []
|
|
18
|
-
clean(text).each_line.with_index(1) do |raw, number|
|
|
18
|
+
clean(text).each_line("\n").with_index(1) do |raw, number|
|
|
19
19
|
line = raw.strip
|
|
20
20
|
if (match = /\ASensor (\d+) has (\S+) degree Celsius\z/.match(line))
|
|
21
21
|
value = /\A#{NUMBER}\z/.match?(match[2]) ? Float(match[2], exception: false) : nil
|
|
@@ -18,7 +18,7 @@ module CtxChecker
|
|
|
18
18
|
# 指定光学方向或单位前,先校验多行表头。
|
|
19
19
|
def parse_output(text, complete:)
|
|
20
20
|
rows, issues, headings = [], diagnostics, []
|
|
21
|
-
clean(text).each_line do |raw|
|
|
21
|
+
clean(text).each_line("\n") do |raw|
|
|
22
22
|
line = raw.strip
|
|
23
23
|
next if line.empty? || /\A[-\s]+\z/.match?(line)
|
|
24
24
|
|
|
@@ -14,7 +14,7 @@ module CtxChecker
|
|
|
14
14
|
# 要求各文档规定的 MP 时间窗完整,无效读数保持未知。
|
|
15
15
|
def parse_output(text, complete:)
|
|
16
16
|
rows, headers = [], 0
|
|
17
|
-
clean(text).each_line.with_index(1) do |raw, number|
|
|
17
|
+
clean(text).each_line("\n").with_index(1) do |raw, number|
|
|
18
18
|
line = raw.strip
|
|
19
19
|
if line == "CPU utilization:"
|
|
20
20
|
headers += 1
|
|
@@ -16,7 +16,7 @@ module CtxChecker
|
|
|
16
16
|
# 扣除明确报告的 Linux 缓存;原始进程数值仅作为来源证据保留。
|
|
17
17
|
def parse_output(text, complete:)
|
|
18
18
|
values, headers = Hash.new { |hash, key| hash[key] = [] }, 0
|
|
19
|
-
clean(text).each_line.with_index(1) do |raw, number|
|
|
19
|
+
clean(text).each_line("\n").with_index(1) do |raw, number|
|
|
20
20
|
line = raw.strip
|
|
21
21
|
if line == "Memory stats:"
|
|
22
22
|
headers += 1
|
|
@@ -18,7 +18,7 @@ module CtxChecker
|
|
|
18
18
|
# 将各 IEEE 计数器绑定到其明确的数据端口标题。
|
|
19
19
|
def parse_output(text, complete:)
|
|
20
20
|
blocks, issues, current = [], diagnostics, nil
|
|
21
|
-
clean(text).each_line do |raw|
|
|
21
|
+
clean(text).each_line("\n") do |raw|
|
|
22
22
|
line = raw.strip
|
|
23
23
|
next if line.empty? || /\A-+\z/.match?(line)
|
|
24
24
|
|
|
@@ -17,7 +17,7 @@ module CtxChecker
|
|
|
17
17
|
# 解释方向性测量前,要求存在带标签的累计计数列。
|
|
18
18
|
def parse_output(text, complete:)
|
|
19
19
|
blocks, issues, current, section = [], diagnostics, nil, nil
|
|
20
|
-
clean(text).each_line do |raw|
|
|
20
|
+
clean(text).each_line("\n") do |raw|
|
|
21
21
|
line = raw.strip
|
|
22
22
|
next if line.empty? || /\A-+\z/.match?(line)
|
|
23
23
|
|
|
@@ -15,7 +15,7 @@ module CtxChecker
|
|
|
15
15
|
rows = { cpu: [], memory: [] }
|
|
16
16
|
issues = rows.transform_values { diagnostics }
|
|
17
17
|
sections, current = [], nil
|
|
18
|
-
clean(text).each_line.with_index(1) do |raw, number|
|
|
18
|
+
clean(text).each_line("\n").with_index(1) do |raw, number|
|
|
19
19
|
line = raw.strip
|
|
20
20
|
if (match = /\ASPs CPU utilization percentage for (\d+) sec sampling\s*:\z/.match(line))
|
|
21
21
|
current = section(:cpu, match[1].to_i)
|
|
@@ -18,7 +18,7 @@ module CtxChecker
|
|
|
18
18
|
source = clean(text)
|
|
19
19
|
rows, headers, sections, time_fields = [], 0, [], []
|
|
20
20
|
counter_issues, ntp_issues, section = diagnostics, diagnostics, nil
|
|
21
|
-
source.each_line.with_index(1) do |raw, number|
|
|
21
|
+
source.each_line("\n").with_index(1) do |raw, number|
|
|
22
22
|
line = raw.strip
|
|
23
23
|
next if line.empty? || /\A-+\z/.match?(line)
|
|
24
24
|
|
data/lib/ctx_checker/report.rb
CHANGED
|
@@ -65,11 +65,16 @@ module CtxChecker
|
|
|
65
65
|
grouped = {}
|
|
66
66
|
snapshot.observations.each_value do |observation|
|
|
67
67
|
sources = observation.evidence.filter_map { |entry| entry[:command] }.uniq
|
|
68
|
+
# 逐命令索引诊断,避免发现数百个端口时反复扫描整个列表。
|
|
69
|
+
issues_by_command = observation.issues.group_by { |issue| issue[:command] }
|
|
70
|
+
if sources.one?
|
|
71
|
+
issues_by_command[sources.first] = observation.issues.select { |issue| !issue[:command] || issue[:command] == sources.first }
|
|
72
|
+
end
|
|
68
73
|
observation.evidence.each do |evidence|
|
|
69
74
|
command = evidence[:command]
|
|
70
75
|
next unless command
|
|
71
76
|
|
|
72
|
-
issues =
|
|
77
|
+
issues = issues_by_command.fetch(command, [])
|
|
73
78
|
state = issues.any? { |issue| issue[:skipped] } ? :skipped : evidence.fetch(:status, observation.status)
|
|
74
79
|
entry = grouped[command] ||= { command: command, dimensions: {}, issues: [] }
|
|
75
80
|
entry[:dimensions][observation.name] = command_status([ entry[:dimensions][observation.name], state ].compact)
|
data/lib/ctx_checker/snapshot.rb
CHANGED
|
@@ -23,7 +23,9 @@ module CtxChecker
|
|
|
23
23
|
end
|
|
24
24
|
@observed_at = case observed_at
|
|
25
25
|
when Time then observed_at.getutc.freeze
|
|
26
|
-
when String
|
|
26
|
+
when String
|
|
27
|
+
raise ArgumentError, "Observation time requires a valid date and explicit UTC offset" unless SystemFields.timestamp?(observed_at)
|
|
28
|
+
Time.iso8601(observed_at).getutc.freeze
|
|
27
29
|
else raise ArgumentError, "Observation time must be Time or ISO8601"
|
|
28
30
|
end
|
|
29
31
|
@target = CtxParser::Immutable.copy(target)
|
|
@@ -27,7 +27,7 @@ module CtxChecker
|
|
|
27
27
|
@resource_thresholds = CtxParser::Immutable.copy(DEFAULT_RESOURCES.merge(resource_thresholds))
|
|
28
28
|
@temperature_thresholds, @required_components, @required_dimensions, @device_counter_limits =
|
|
29
29
|
[ temperature_thresholds, required_components, required_dimensions, device_counter_limits ].map { |value| CtxParser::Immutable.copy(value) }
|
|
30
|
-
@max_clock_skew_seconds, @max_ntp_offset_ms = max_clock_skew_seconds, max_ntp_offset_ms
|
|
30
|
+
@max_clock_skew_seconds, @max_ntp_offset_ms = CtxParser::Immutable.copy([ max_clock_skew_seconds, max_ntp_offset_ms ])
|
|
31
31
|
freeze
|
|
32
32
|
end
|
|
33
33
|
|
data/lib/ctx_checker/version.rb
CHANGED