ctx_checker 0.1.0 → 0.2.1

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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +27 -0
  3. data/README-zh.md +10 -0
  4. data/README.md +10 -0
  5. data/docs/architecture-zh.md +2 -2
  6. data/lib/ctx_checker/checks/base.rb +4 -4
  7. data/lib/ctx_checker/checks/clock.rb +3 -3
  8. data/lib/ctx_checker/checks/counters.rb +7 -7
  9. data/lib/ctx_checker/checks/device_counters.rb +1 -1
  10. data/lib/ctx_checker/checks/interfaces.rb +1 -1
  11. data/lib/ctx_checker/checks/optics.rb +1 -1
  12. data/lib/ctx_checker/checks/temperature.rb +2 -0
  13. data/lib/ctx_checker/cisco_ios/parsers/show_clock_detail.rb +1 -1
  14. data/lib/ctx_checker/cisco_ios/parsers/show_environment_all.rb +3 -3
  15. data/lib/ctx_checker/cisco_ios/parsers/show_etherchannel_summary.rb +6 -3
  16. data/lib/ctx_checker/cisco_ios/parsers/show_facility_alarm_status.rb +1 -1
  17. data/lib/ctx_checker/cisco_ios/parsers/show_interfaces.rb +19 -5
  18. data/lib/ctx_checker/cisco_ios/parsers/show_interfaces_transceiver_detail.rb +3 -2
  19. data/lib/ctx_checker/cisco_ios/parsers/show_ip_bgp_summary.rb +5 -5
  20. data/lib/ctx_checker/cisco_ios/parsers/show_ip_ospf_neighbor.rb +3 -3
  21. data/lib/ctx_checker/cisco_ios/parsers/show_ip_traffic.rb +1 -1
  22. data/lib/ctx_checker/cisco_ios/parsers/show_memory_statistics.rb +1 -1
  23. data/lib/ctx_checker/cisco_ios/parsers/show_ntp_status.rb +1 -1
  24. data/lib/ctx_checker/cisco_ios/parsers/show_processes_cpu.rb +1 -1
  25. data/lib/ctx_checker/cisco_ios/profile.rb +3 -3
  26. data/lib/ctx_checker/cisco_nxos/parsers/show_environment.rb +3 -3
  27. data/lib/ctx_checker/cisco_nxos/parsers/show_interface_transceiver_details.rb +13 -8
  28. data/lib/ctx_checker/cisco_nxos/parsers/show_ip_traffic.rb +1 -1
  29. data/lib/ctx_checker/cisco_nxos/parsers/show_ntp_peer_status.rb +1 -1
  30. data/lib/ctx_checker/cisco_nxos/parsers/show_port_channel_summary.rb +2 -2
  31. data/lib/ctx_checker/cisco_nxos/parsers/show_system_resources.rb +8 -5
  32. data/lib/ctx_checker/cisco_nxos/profile.rb +3 -3
  33. data/lib/ctx_checker/h3c/parsers/display_bgp_peer.rb +2 -2
  34. data/lib/ctx_checker/h3c/parsers/display_cpu_usage.rb +3 -2
  35. data/lib/ctx_checker/h3c/parsers/display_interface.rb +2 -2
  36. data/lib/ctx_checker/h3c/parsers/display_link_aggregation_verbose.rb +1 -1
  37. data/lib/ctx_checker/h3c/parsers/display_ospf_peer.rb +2 -2
  38. data/lib/ctx_checker/h3c/parsers/display_transceiver_diagnosis.rb +1 -1
  39. data/lib/ctx_checker/h3c/profile.rb +3 -3
  40. data/lib/ctx_checker/hillstone/parsers/show_environment.rb +3 -3
  41. data/lib/ctx_checker/hillstone/parsers/show_interface.rb +2 -2
  42. data/lib/ctx_checker/hillstone/parsers/show_interface_counter.rb +1 -1
  43. data/lib/ctx_checker/hillstone/parsers/show_ntp_status.rb +2 -2
  44. data/lib/ctx_checker/hillstone/parsers/show_transceiver.rb +1 -1
  45. data/lib/ctx_checker/hillstone/profile.rb +3 -3
  46. data/lib/ctx_checker/observation.rb +13 -8
  47. data/lib/ctx_checker/palo_alto/parsers/show_clock.rb +1 -1
  48. data/lib/ctx_checker/palo_alto/parsers/show_counter_global.rb +1 -1
  49. data/lib/ctx_checker/palo_alto/parsers/show_counter_interface_all.rb +1 -1
  50. data/lib/ctx_checker/palo_alto/parsers/show_interface_all.rb +1 -1
  51. data/lib/ctx_checker/palo_alto/parsers/show_lacp_aggregate_ethernet.rb +1 -1
  52. data/lib/ctx_checker/palo_alto/parsers/show_ntp.rb +1 -1
  53. data/lib/ctx_checker/palo_alto/parsers/show_routing_protocol_bgp_peer.rb +3 -3
  54. data/lib/ctx_checker/palo_alto/parsers/show_routing_protocol_ospf_neighbor.rb +4 -4
  55. data/lib/ctx_checker/palo_alto/parsers/show_system_environmentals.rb +3 -3
  56. data/lib/ctx_checker/palo_alto/parsers/show_system_resources.rb +3 -3
  57. data/lib/ctx_checker/palo_alto/parsers/show_system_state_alarms.rb +1 -1
  58. data/lib/ctx_checker/palo_alto/parsers/show_transceiver_all.rb +1 -1
  59. data/lib/ctx_checker/palo_alto/profile.rb +3 -3
  60. data/lib/ctx_checker/parsers/base.rb +11 -4
  61. data/lib/ctx_checker/parsers/diagnostics.rb +47 -0
  62. data/lib/ctx_checker/policy.rb +20 -12
  63. data/lib/ctx_checker/radware_alteon/parsers/info_l2_lacp_dump.rb +3 -3
  64. data/lib/ctx_checker/radware_alteon/parsers/info_l2_trunk.rb +4 -4
  65. data/lib/ctx_checker/radware_alteon/parsers/info_l3_ip.rb +1 -1
  66. data/lib/ctx_checker/radware_alteon/parsers/info_link.rb +1 -1
  67. data/lib/ctx_checker/radware_alteon/parsers/info_transceiver.rb +1 -1
  68. data/lib/ctx_checker/radware_alteon/parsers/stats_port_ether.rb +1 -1
  69. data/lib/ctx_checker/radware_alteon/parsers/stats_port_if.rb +1 -1
  70. data/lib/ctx_checker/radware_alteon/parsers/stats_sp_allutils.rb +3 -3
  71. data/lib/ctx_checker/radware_alteon/parsers/stats_sys_ntp.rb +3 -3
  72. data/lib/ctx_checker/radware_alteon/profile.rb +9 -9
  73. data/lib/ctx_checker/runtime_parser.rb +16 -10
  74. data/lib/ctx_checker/{system_record.rb → system_fields.rb} +3 -3
  75. data/lib/ctx_checker/system_policy.rb +14 -14
  76. data/lib/ctx_checker/version.rb +1 -1
  77. data/lib/ctx_checker.rb +1 -1
  78. metadata +11 -10
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6bd23ca0a7767d79f02f7b0103f0c2f524534ab677d368f995484981d93fbbfb
4
- data.tar.gz: 9c324f79c104b917338007d105c2a55405a09a036e66a3c9349ef57e3f9664de
3
+ metadata.gz: 71962914e6b52e83b27e6c79345530ef9e92fb4168175a43e6cc46f464abc8f4
4
+ data.tar.gz: e1e3adf05ec7d615920b49d65734e44cd3df35ba5efff584f11b5d0b5273df22
5
5
  SHA512:
6
- metadata.gz: 15cf93bf62713e215071bc2d5d0081820cdad50d67b38ddbbc3874e6df30b8f256417997b33e2383ea849bc15b879537b7f9013f393f01dd74f9c943db0a9aff
7
- data.tar.gz: 0ac66781f4a9795b92bc872704633d72ab12a88a19b26ebe6b3af7e8b8a4239c013bbec61c28c2182e859043fc6b2ee2726eff3f3339e52e648d950769634137
6
+ metadata.gz: 270d9b5bf20181cfc30f89dbe9b069ae5ac3499ab57efc726c991be5e2fe3f633156de1e8e51d612b7719be1981d74e89c26334fb546fbe19262f6290d759871
7
+ data.tar.gz: 124f1e9f0609e3a46e382305ffd23768096b272868e852ffd1a25da317ae58defefac2ec1bfb6200fc94b381f1bb0bf594e69baf4d7db286731043c722ddb49e
data/CHANGELOG.md CHANGED
@@ -2,6 +2,33 @@
2
2
 
3
3
  Changes to the public contract are recorded here. Versions follow Semantic Versioning.
4
4
 
5
+ ## 0.2.1 (2026-10-17)
6
+
7
+ - Validate time-limit snapshot compatibility during policy construction; reject unsupported Numeric objects before evaluation while preserving exact Rational values.
8
+
9
+ ## 0.2.0 (2026-10-17)
10
+
11
+ - Require ctx_parser and ctx_driver 0.2.x for the renamed public contracts.
12
+
13
+ - Allow isolated package verification to install explicitly supplied dependency candidate artifacts without changing gemspec dependency constraints.
14
+ - Require a measured temperature when an exact sensor policy supplies numeric thresholds.
15
+ - Preserve conflicting Cisco interface states as incomplete evidence and retain observed protocol failures.
16
+ - Normalize integer OSPF process identities in policies, observations, and coverage scopes; reject normalized duplicates.
17
+ - Bound parser diagnostic samples to 128 plus an omitted-count summary, and skip vendor parsing for explicit command failures.
18
+ - Index validated command implementations for response parsing, rebuilding the index after discovery expansion.
19
+
20
+ - Use the shared `CtxParser::Configuration`, `Results`, and `Commands` names and dimension indexes where applicable.
21
+
22
+ - Name system field definitions and validation `SystemFields`; exercise installed BGP parsing during package verification.
23
+
24
+ - Rename domain APIs directly; callers must use the new names (no compatibility aliases).
25
+
26
+ - Name required-dimension predicates and policy validators explicitly; expose sampling interval units in counter evaluation.
27
+
28
+ ### Changed
29
+
30
+ - Index CPU windows, optical measurements, aggregation membership, and required interfaces once, avoiding repeated full scans during parsing and health evaluation.
31
+
5
32
  ## 0.1.0 (2026-10-16)
6
33
 
7
34
  ### Added
data/README-zh.md CHANGED
@@ -67,8 +67,16 @@ error、CRC、discard 保持独立,不相加;单次累计值不能当作当
67
67
 
68
68
  生命周期 connector 是宿主适配器,提供 `open { |session| }`,VXLAN 和 Wireless 还要求 `platform`;原生 `Net::Connector::Base` 实例并不提供这一宿主生命周期协议。已连接的原生会话可传入 `CtxDriver::Reader` 或 `CtxChecker::Collector#collect_session`,不会隐式连接或重试。
69
69
 
70
+ ## 领域术语
71
+
72
+ `Observation` 表示某维度的采集证据,`Finding` 表示健康判定。`Policy#required_dimension?`、`SystemPolicy#required_dimension?` 查询必需观测维度。接口与光学测量仍按其完整身份匹配要求。计数器阈值比较经过连续性验证的采样区间内增量,区间以秒计;阈值不是累计总量或每秒速率。
73
+
74
+ 整数 OSPF 进程 ID 在策略、观测记录和覆盖作用域中统一转换成字符串,再检查身份重复。明确配置温度阈值时必须有数值读数,只有 `ok` 状态不能满足阈值策略。每个解析结果最多保留 128 条诊断样本,额外的 `diagnostics_truncated` 摘要记录省略数量;截断不能证明证据完整。
75
+
70
76
  ## 开发与测试
71
77
 
78
+ 共享类按实际职责命名,平台信息放在命名空间中。直接对应设备命令的解析器保留文件名和类名。
79
+
72
80
  ```sh
73
81
  bundle install
74
82
  bundle exec rake
@@ -78,6 +86,8 @@ bundle exec rake build
78
86
 
79
87
  `bundle exec rake` 执行 RSpec、RuboCop 及离线示例。测试放在 `spec/`,沿用项目 RSpec 约定;样例采用合成或脱敏证据。新增解析格式时补充成功、不完整、不支持及非法输入回归。离线测试不代表所有设备版本均通过实机验证。
80
88
 
89
+ 运行 `ruby script/verify_package.rb /path/to/ctx_checker.gem`,验证使用已发布依赖的隔离安装。联动开发时可追加明确构建的依赖包路径,例如 `/path/to/ctx_parser.gem`,先安装这些候选包;候选包验证通过不代表注册表中的依赖已兼容。`bundle exec ruby -Ilib benchmark/runtime.rb` 用合成输入测量本地命令查找和诊断开销。
90
+
81
91
  使用声明的最低 Ruby 版本检查兼容性。方法用途统一使用简明中文注释;复杂实现还需说明业务规则、实现思路及关键分支。保持公开返回值与错误契约。真实回显和凭据不可进入示例、fixture、日志或发布包。
82
92
 
83
93
  ## 发布
data/README.md CHANGED
@@ -67,8 +67,16 @@ Detailed vendor behavior, historical validation notes, and Motor-only integratio
67
67
 
68
68
  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.
69
69
 
70
+ ## Domain vocabulary
71
+
72
+ An `Observation` describes collected evidence for a dimension; a `Finding` describes its health assessment. `Policy#required_dimension?` and `SystemPolicy#required_dimension?` query required observation dimensions. Interface and optical requirements retain their scoped identity checks. Counter limits compare increments over a validated sampling interval in seconds; they are not cumulative totals or rates.
73
+
74
+ Integer OSPF process IDs are normalized to strings in policies, observations, and covered scopes before checking identities. An explicit temperature threshold requires a numeric reading; an `ok` sensor state alone cannot satisfy it. Parsers retain at most 128 diagnostic samples per result, plus a `diagnostics_truncated` entry counting omitted diagnostics; truncation never proves complete evidence.
75
+
70
76
  ## Development and tests
71
77
 
78
+ Name shared classes by their responsibility and use the namespace for platform context. Preserve filenames and class names that directly match device commands.
79
+
72
80
  ```sh
73
81
  bundle install
74
82
  bundle exec rake
@@ -78,6 +86,8 @@ bundle exec rake build
78
86
 
79
87
  `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.
80
88
 
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.
90
+
81
91
  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.
82
92
 
83
93
  ## Release
@@ -23,12 +23,12 @@ net-connector session -> Collector / CtxDriver::Reader -> 逐命令 Parser
23
23
  ### 文件组织
24
24
 
25
25
  * `lib/ctx_checker/{observation,snapshot,policy,finding,report}.rb`:不可变领域对象和公共契约。
26
- * `lib/ctx_checker/{system_record,system_policy,observation_merger}.rb`:系统指标字段、运行阈值及多命令证据组合。
26
+ * `lib/ctx_checker/{system_fields,system_policy,observation_merger}.rb`:系统指标字段、运行阈值及多命令证据组合。
27
27
  * `lib/ctx_checker/{evaluator,collector,runtime_parser,parser_registry}.rb`:判定组合、采集和离线入口。
28
28
  * `lib/ctx_checker/checks/*.rb`:按指标组织的厂商无关检查。
29
29
  * `lib/ctx_checker/parsers/{base,interface_names,scoped_result}.rb`:文本边界、完整性、接口名称及协议表范围证明。
30
30
  * `lib/ctx_checker/{h3c,cisco_ios,cisco_nxos,palo_alto,hillstone,radware_alteon}/profile.rb`:只读查询目录及厂商发现策略。
31
- * `lib/ctx_checker/<platform>/parsers/<command>.rb`:一个命令一个解析器;一份回显包含多个指标时返回 ResultSet。
31
+ * `lib/ctx_checker/<platform>/parsers/<command>.rb`:一个命令一个解析器;一份回显包含多个指标时返回 Results。
32
32
 
33
33
  不创建每厂商一个巨型解析模板。共享同一语法才共享解析机制;健康规则集中在 checks。
34
34
  型号变体通过 Profile 策略或 MODEL_PARSERS 显式扩展,不按未知型号猜测命令。
@@ -15,11 +15,11 @@ module CtxChecker
15
15
  def call(observation)
16
16
  @observation = observation
17
17
  if observation.status == :not_applicable
18
- return [ finding(:fail, :required_feature_not_configured, severity: :critical) ] if @policy.required?(observation.name)
18
+ return [ finding(:fail, :required_feature_not_configured, severity: :critical) ] if @policy.required_dimension?(observation.name)
19
19
 
20
20
  return [ finding(:not_applicable, :feature_not_configured) ]
21
21
  end
22
- if observation.status == :not_requested && !@policy.required?(observation.name)
22
+ if observation.status == :not_requested && !@policy.required_dimension?(observation.name)
23
23
  return [ finding(:not_applicable, :not_requested) ]
24
24
  end
25
25
  results = observation.records.flat_map do |record|
@@ -51,8 +51,8 @@ module CtxChecker
51
51
  def missing_status = @observation.complete? ? :fail : :unknown
52
52
  # 仅在需要关联接口状态的检查中建立接口索引。
53
53
  def interface(name)
54
- @interfaces ||= @snapshot.observation(:interfaces).records.to_h { |row| [ row[:name], row ] }
55
- @interfaces[name]
54
+ @interfaces_by_name ||= @snapshot.observation(:interfaces).records.to_h { |row| [ row[:name], row ] }
55
+ @interfaces_by_name[name]
56
56
  end
57
57
 
58
58
  # 同级查询失败不能使其他来源的完整测量失效。
@@ -19,10 +19,10 @@ module CtxChecker
19
19
  return results + [ finding(:unknown, :clock_time_unavailable, entity: entity) ]
20
20
  end
21
21
  observed_at = @observation.observed_at || @snapshot.observed_at
22
- skew = (Time.iso8601(record[:time]) - observed_at).abs
22
+ skew_seconds = (Time.iso8601(record[:time]) - observed_at).abs
23
23
  limit = @policy.system.max_clock_skew_seconds
24
- results << finding(skew > limit ? :fail : :pass, :clock_skew, entity: entity,
25
- actual: { time: record[:time], skew_seconds: skew }, expected: { maximum_seconds: limit }, severity: skew > limit ? :warning : :info)
24
+ results << finding(skew_seconds > limit ? :fail : :pass, :clock_skew, entity: entity,
25
+ actual: { time: record[:time], skew_seconds: skew_seconds }, expected: { maximum_seconds: limit }, severity: skew_seconds > limit ? :warning : :info)
26
26
  results
27
27
  end
28
28
  end
@@ -10,7 +10,7 @@ module CtxChecker
10
10
  def initialize(**options)
11
11
  super
12
12
  @previous_counters = @previous&.observation(dimension)
13
- @previous_records = @previous_counters&.records&.to_h { |row| [ row.values_at(*identity_fields), row ] } || {}
13
+ @previous_records_by_identity = @previous_counters&.records&.to_h { |row| [ row.values_at(*identity_fields), row ] } || {}
14
14
  end
15
15
 
16
16
  private
@@ -21,7 +21,7 @@ module CtxChecker
21
21
  value = record[:value]
22
22
  return finding(:unknown, :counter_unavailable, entity: entity) if value.nil?
23
23
  previous = @previous_counters
24
- prior = @previous_records[record.values_at(*identity_fields)]
24
+ prior = @previous_records_by_identity[record.values_at(*identity_fields)]
25
25
  if (reason = packet_semantics_reason(record, prior))
26
26
  return finding(:unknown, reason, entity: entity, actual: record.slice(:value, :unit, :scope))
27
27
  end
@@ -30,9 +30,9 @@ module CtxChecker
30
30
  return finding(value.zero? ? :pass : :unknown, value.zero? ? :zero_cumulative_counter : :cumulative_errors,
31
31
  entity: entity, actual: value, severity: value.zero? ? :info : :warning)
32
32
  end
33
- interval = sample_time(record, @observation, @snapshot) - sample_time(prior, previous, @previous)
33
+ interval_seconds = sample_time(record, @observation, @snapshot) - sample_time(prior, previous, @previous)
34
34
  # 先验证时间与重置标记;数值下降可能是清零或重启,不能按负增量计算。
35
- reason = if interval <= 0
35
+ reason = if interval_seconds <= 0
36
36
  :counter_sample_time_invalid
37
37
  elsif record[:discontinuity].nil? || prior[:discontinuity].nil?
38
38
  :counter_continuity_unconfirmed
@@ -45,10 +45,10 @@ module CtxChecker
45
45
  limit = limit_for(record)
46
46
  # 超限是已观察故障;未超限仍需前后来源完整才能证明通过。
47
47
  if delta <= limit && (!record_complete?(prior, previous) || !record_complete?(record, @observation))
48
- return finding(:unknown, :counter_evidence_incomplete, entity: entity, actual: { delta: delta, seconds: interval })
48
+ return finding(:unknown, :counter_evidence_incomplete, entity: entity, actual: { delta: delta, seconds: interval_seconds })
49
49
  end
50
50
  finding(delta > limit ? :fail : :pass, :counter_delta, entity: entity,
51
- actual: delta_measurement(value, delta, interval), expected: { maximum_delta: limit }, severity: :warning)
51
+ actual: delta_measurement(value, delta, interval_seconds), expected: { maximum_delta: limit }, severity: :warning)
52
52
  end
53
53
 
54
54
  # 选择当前评估负责的观测维度。
@@ -58,7 +58,7 @@ module CtxChecker
58
58
  # 优先使用资源专用阈值,再回退到策略默认值。
59
59
  def limit_for(record) = @policy.counter_limits.fetch(record[:metric], 0)
60
60
  # 将已计算增量与采样间隔组织为报告测量值。
61
- def delta_measurement(_value, delta, interval) = { delta: delta, seconds: interval }
61
+ def delta_measurement(_value, delta, interval_seconds) = { delta: delta, seconds: interval_seconds }
62
62
  # 解析速率或增量分析所需的采样时间。
63
63
  def sample_time(record, observation, snapshot) = record[:sampled_at] ? Time.iso8601(record[:sampled_at]) : observation.observed_at || snapshot.observed_at
64
64
 
@@ -26,7 +26,7 @@ module CtxChecker
26
26
  # 优先使用资源专用阈值,再回退到策略默认值。
27
27
  def limit_for(record) = @policy.system.device_counter_limits.fetch(record[:metric], 0)
28
28
  # 组织累计量、已验证增量及采样间隔,并计算每秒速率。
29
- def delta_measurement(value, delta, interval) = { cumulative: value, delta: delta, seconds: interval, per_second: delta / interval }
29
+ def delta_measurement(value, delta, interval_seconds) = { cumulative: value, delta: delta, seconds: interval_seconds, per_second: delta / interval_seconds }
30
30
  end
31
31
  end
32
32
  end
@@ -11,7 +11,7 @@ module CtxChecker
11
11
  # 按策略评估资源,同时保留测量值缺失的状态。
12
12
  def assess(record)
13
13
  name = record[:name]
14
- required = @policy.required_interfaces.include?(name)
14
+ required = @policy.required_interface?(name)
15
15
  status, reason = if record[:admin_status] == :down && !required
16
16
  [ :not_applicable, :administratively_down ]
17
17
  elsif record[:oper_status] == :down || record[:protocol_status] == :down && record[:protocol_applicable] != false || required && record[:admin_status] == :down
@@ -20,7 +20,7 @@ module CtxChecker
20
20
  end
21
21
  context = interface(record[:interface])
22
22
  if context && record_complete?(context, @snapshot.observation(:interfaces)) &&
23
- context[:admin_status] == :down && !@policy.required_interfaces.include?(record[:interface]) && !@policy.required_optics?(record)
23
+ context[:admin_status] == :down && !@policy.required_interface?(record[:interface]) && !@policy.required_optics?(record)
24
24
  return finding(:not_applicable, :administratively_down, entity: entity)
25
25
  end
26
26
  if (record[:present] == false || record[:dom_supported] == false) && !ALARMS.include?(record[:alarm])
@@ -33,6 +33,8 @@ module CtxChecker
33
33
  if !override && (state == :ok && severity || explicit_fault && !severity)
34
34
  results << finding(:unknown, :temperature_state_disagreement, entity: entity)
35
35
  end
36
+ elsif override && value.nil?
37
+ results << finding(:unknown, :temperature_value_unavailable, entity: entity, expected: bounds)
36
38
  elsif !explicit_fault
37
39
  results << finding(state == :ok ? :pass : :unknown, :temperature_state, entity: entity, actual: record)
38
40
  end
@@ -14,7 +14,7 @@ module CtxChecker
14
14
 
15
15
  # 解析 show clock detail,保留不完整或不支持的证据。
16
16
  def parse_output(text, complete:)
17
- records, issues = [], []
17
+ records, issues = [], diagnostics
18
18
  time_source = nil
19
19
  clean(text).each_line.with_index(1) do |source, number|
20
20
  line = source.strip
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative "../../parsers/base"
4
4
  require_relative "system_values"
5
- require "ctx_parser/result_set"
5
+ require "ctx_parser/results"
6
6
 
7
7
  module CtxChecker
8
8
  module CiscoIos
@@ -16,7 +16,7 @@ module CtxChecker
16
16
  # 解析 show environment all,保留不完整或不支持的证据。
17
17
  def parse_output(text, complete:)
18
18
  @rows = { temperature: [], environment: [], power: [] }
19
- @problems = @rows.transform_values { [] }
19
+ @problems = @rows.transform_values { diagnostics }
20
20
  @switch = "active"
21
21
  @temperature = nil
22
22
  @sensor_table = false
@@ -33,7 +33,7 @@ module CtxChecker
33
33
  identity = dimension == :temperature ? [ :component ] : %i[component kind]
34
34
  [ dimension, build_result(rows, complete: complete, recognized: rows.any?, issues: @problems[dimension], identity: identity) ]
35
35
  end
36
- CtxParser::ResultSet.new(results: results)
36
+ CtxParser::Results.new(by_dimension: results)
37
37
  end
38
38
 
39
39
  # Catalyst StackWise Virtual 在传感器后按交换机报告电源和风扇托盘。
@@ -18,7 +18,7 @@ module CtxChecker
18
18
  # 要求存在数据表或明确空表证据;标志图例不能作为成员记录。
19
19
  def parse_output(text, complete:)
20
20
  text = clean(text)
21
- rows, issues, groups, totals = [], [], [], []
21
+ rows, issues, groups, totals = [], diagnostics, [], []
22
22
  current, header, empty = nil, false, false
23
23
  text.each_line.with_index(1) do |source, number|
24
24
  line = source.strip
@@ -49,16 +49,19 @@ module CtxChecker
49
49
  totals.each do |total|
50
50
  issues << { code: :count_mismatch, expected: total, observed: groups.size } unless total == groups.size
51
51
  end
52
+ member_groups = rows.to_h { |row| [ row[:group], true ] }
52
53
  groups.each do |group|
53
- next if rows.any? { |row| row[:group] == group[:group] }
54
+ next if member_groups.key?(group[:group])
54
55
 
55
56
  rows << group.merge(member: nil, member_status: :unknown)
57
+ member_groups[group[:group]] = true
56
58
  issues << { code: :aggregation_members_missing, group: group[:group] }
57
59
  end
58
60
  repeated_groups = groups.group_by { |group| group[:group] }.select { |_, entries| entries.size > 1 }.keys
59
61
  if repeated_groups.any?
60
62
  issues << { code: :duplicate_aggregation_group, count: repeated_groups.size }
61
- rows.reject! { |row| repeated_groups.include?(row[:group]) }
63
+ repeated = repeated_groups.to_h { |name| [ name, true ] }
64
+ rows.reject! { |row| repeated.key?(row[:group]) }
62
65
  end
63
66
  issues << { code: :contradictory_empty } if empty && groups.any?
64
67
  build_result(rows, complete: complete, recognized: empty || (header && groups.any?), issues: issues, identity: %i[group member])
@@ -11,7 +11,7 @@ module CtxChecker
11
11
 
12
12
  # 解析 show facility alarm status,保留不完整或不支持的证据。
13
13
  def parse_output(text, complete:)
14
- records, issues, totals, severities = [], [], [], []
14
+ records, issues, totals, severities = [], diagnostics, [], []
15
15
  clean(text).each_line.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)
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative "../../parsers/base"
4
4
  require_relative "../../parsers/interface_names"
5
- require "ctx_parser/result_set"
5
+ require "ctx_parser/results"
6
6
 
7
7
  module CtxChecker
8
8
  module CiscoIos
@@ -23,22 +23,24 @@ module CtxChecker
23
23
  # 分别跟踪覆盖率;有效状态头不能证明计数器已采集。
24
24
  def parse_output(text, complete:)
25
25
  text = clean(text)
26
- interfaces, counters, state_issues, counter_issues = [], [], [], []
26
+ interfaces, counters, state_issues, counter_issues = [], [], diagnostics, diagnostics
27
27
  current = nil
28
+ states = {}
28
29
  text.each_line.with_index(1) do |source, number|
29
30
  line = source.strip
30
31
  next if line.empty?
31
32
 
32
33
  if (match = interface_header(line))
33
34
  current = interface_record(match)
35
+ states = current.slice(:admin_status, :protocol_status).transform_values { |value| value == :unknown ? [] : [ value ] }
34
36
  interfaces << current
35
37
  elsif (match = /\A(\S+) is /.match(line)) && CtxChecker::Parsers::InterfaceNames.kind(normalize_name(match[1])) != :unknown
36
38
  current = nil
37
39
  state_issues << { code: :unparsed_interface_header, line: number }
38
40
  elsif current && (match = /\Aadmin state is (up|down)\b/i.match(line))
39
- current[:admin_status] = match[1].downcase.to_sym
41
+ assign_state(current, :admin_status, match[1].downcase.to_sym, states, state_issues, number)
40
42
  elsif current && (match = /\ALine protocol is (up|down)\b/i.match(line))
41
- current[:protocol_status] = match[1].downcase.to_sym
43
+ assign_state(current, :protocol_status, match[1].downcase.to_sym, states, state_issues, number)
42
44
  elsif current && (match = /\ALast clearing of "show interfaces?" counters\s+(.+)\z/i.match(line))
43
45
  # 相对清零时长不能证明两个采样间的计数器连续性。
44
46
  current[:discontinuity] = nil
@@ -84,13 +86,25 @@ module CtxChecker
84
86
  interfaces.select { |record| record[:kind] == :physical }.each do |record|
85
87
  counters.concat(complete_counter_coverage(record, counters_by_interface.fetch(record[:name], []), counter_issues))
86
88
  end
87
- CtxParser::ResultSet.new(results: {
89
+ CtxParser::Results.new(by_dimension: {
88
90
  interfaces: build_result(interfaces, complete: complete, recognized: interfaces.any?, issues: state_issues, identity: [ :name ]),
89
91
  counters: build_result(counters, complete: complete, recognized: counters.any?, issues: state_issues + counter_issues,
90
92
  identity: %i[interface metric])
91
93
  })
92
94
  end
93
95
 
96
+ # 冲突协议状态保留 down;冲突管理状态不能据此豁免接口检查。
97
+ def assign_state(record, field, value, states, issues, number)
98
+ values = states.fetch(field)
99
+ values << value unless values.include?(value)
100
+ record[field] = if values.size > 1
101
+ issues << { code: :conflicting_interface_state, field: field, interface: record[:name], line: number }
102
+ field == :protocol_status ? :down : :unknown
103
+ else
104
+ value
105
+ end
106
+ end
107
+
94
108
  # 按计数器标签匹配,不误取总包数,并保留格式错误的指标行。
95
109
  def counter_line?(line)
96
110
  line.match?(/\b(?:input errors?|output errors?|CRC(?:\/FCS)?|input discards?|output discards?|Total output drops)\b/i) ||
@@ -16,7 +16,7 @@ module CtxChecker
16
16
  # 解析 show interfaces transceiver detail,保留不完整或不支持的证据。
17
17
  def parse_output(text, complete:)
18
18
  text = clean(text)
19
- rows, issues, ports = [], [], []
19
+ rows, issues, ports = [], diagnostics, []
20
20
  metric, header, lane_column = nil, false, false
21
21
  text.each_line.with_index(1) do |source, number|
22
22
  line = source.strip
@@ -48,11 +48,12 @@ module CtxChecker
48
48
  issues << { code: :unparsed_row, line: number }
49
49
  end
50
50
  end
51
+ measurements = rows.to_h { |row| [ row.values_at(:interface, :lane, :metric), true ] }
51
52
  identities = rows.map { |row| row.values_at(:interface, :lane) }
52
53
  identities.concat((ports.uniq - rows.map { |row| row[:interface] }).map { |name| [ name, nil ] })
53
54
  identities.uniq.each do |name, lane|
54
55
  %i[tx_power rx_power].each do |expected|
55
- next if rows.any? { |row| row.values_at(:interface, :lane, :metric) == [ name, lane, expected ] }
56
+ next if measurements.key?([ name, lane, expected ])
56
57
 
57
58
  issues << { code: :optical_measurement_missing, interface: name, lane: lane, metric: expected }
58
59
  end
@@ -19,7 +19,7 @@ module CtxChecker
19
19
  # 使用作用域与行身份,避免不同地址族或 VRF 相互覆盖。
20
20
  def parse_output(text, complete:)
21
21
  text = clean(text).gsub(/State\/\s*\n\s*PfxRcd/, "State/PfxRcd")
22
- rows, issues = [], []
22
+ rows, issues = [], diagnostics
23
23
  scope = { vrf: "default", afi_safi: self.class::ADDRESS_FAMILY }
24
24
  sections = []
25
25
  section = { header: false, empty: false, count: 0, scope_fields: [], scope: scope }
@@ -46,7 +46,7 @@ module CtxChecker
46
46
  section[:empty] = true
47
47
  elsif parse_statistics(line, section, scope, issues, number)
48
48
  next
49
- elsif CtxParser::Values.valid_host?(line)
49
+ elsif CtxParser::Values.ip_address?(line)
50
50
  issues << { code: :unparsed_row, line: number - 1 } if pending
51
51
  pending = line
52
52
  elsif peer_row?(line) || (pending && line.match?(/\A4\s/))
@@ -77,7 +77,7 @@ module CtxChecker
77
77
  end
78
78
 
79
79
  # 识别协议对等体记录,排除摘要和表头。
80
- def peer_row?(line) = CtxParser::Values.valid_host?(line.split.first.to_s)
80
+ def peer_row?(line) = CtxParser::Values.ip_address?(line.split.first.to_s)
81
81
 
82
82
  # 解析单个路由作用域,不合并无关的 VRF 证据。
83
83
  def parse_scope(line, scope, issues, number)
@@ -124,7 +124,7 @@ module CtxChecker
124
124
  # 归一化单个路由对等体及其协议状态。
125
125
  def parse_peer(line, scope)
126
126
  match = /\A(\S+)\s+4\s+(\d+(?:\.\d+)?)\s+\d+\s+\d+\s+\d+\s+\d+\s+\d+\s+\S+\s+(.+)\z/.match(line)
127
- return unless match && CtxParser::Values.valid_host?(match[1])
127
+ return unless match && CtxParser::Values.ip_address?(match[1])
128
128
 
129
129
  remote_as = as_number(match[2])
130
130
  return unless remote_as
@@ -132,7 +132,7 @@ module CtxChecker
132
132
  state, prefixes = peer_state(match[3])
133
133
  return unless state
134
134
 
135
- scope.merge(peer: CtxParser::Values.host(match[1]), state: state, received_prefixes: prefixes, remote_as: remote_as)
135
+ scope.merge(peer: CtxParser::Values.ip_address(match[1]), state: state, received_prefixes: prefixes, remote_as: remote_as)
136
136
  end
137
137
 
138
138
  # 仅在不存在平台附加标记时,末列数字才能证明会话已建立。
@@ -19,7 +19,7 @@ module CtxChecker
19
19
  # 解析 show ip ospf neighbor,保留不完整或不支持的证据。
20
20
  def parse_output(text, complete:)
21
21
  text = clean(text)
22
- rows, issues = [], []
22
+ rows, issues = [], diagnostics
23
23
  scope = { vrf: "default", process_id: nil, area: nil }
24
24
  sections = []
25
25
  section = { header: false, empty: false, count: 0, totals: [], declared: false, scope: scope }
@@ -81,14 +81,14 @@ module CtxChecker
81
81
  ipv4_address?(match[1]) && ipv4_address?(match[5])
82
82
 
83
83
  scope.merge(interface: CtxChecker::Parsers::InterfaceNames.normalize(match[6], platform: self.class::PLATFORM),
84
- router_id: CtxParser::Values.host(match[1]), neighbor_address: CtxParser::Values.host(match[5]),
84
+ router_id: CtxParser::Values.ip_address(match[1]), neighbor_address: CtxParser::Values.ip_address(match[5]),
85
85
  state: STATES.fetch(match[3].upcase), network_type: :unknown, local_role: :unknown,
86
86
  neighbor_role: match[4] == "-" ? :unknown : match[4].downcase.to_sym)
87
87
  end
88
88
 
89
89
  # OSPFv2 路由器 ID 与接口地址必须是 IPv4 主机地址。
90
90
  def ipv4_address?(text)
91
- text.match?(/\A\d+(?:\.\d+){3}\z/) && CtxParser::Values.valid_host?(text)
91
+ text.match?(/\A\d+(?:\.\d+){3}\z/) && CtxParser::Values.ip_address?(text)
92
92
  end
93
93
  end
94
94
  end
@@ -28,7 +28,7 @@ module CtxChecker
28
28
 
29
29
  # 解析 show ip traffic,保留不完整或不支持的证据。
30
30
  def parse_output(text, complete:)
31
- records, issues = [], []
31
+ records, issues = [], diagnostics
32
32
  active = false
33
33
  recognized = false
34
34
  clean(text).each_line.with_index(1) do |source, number|
@@ -11,7 +11,7 @@ module CtxChecker
11
11
 
12
12
  # 解析 show memory statistics,保留不完整或不支持的证据。
13
13
  def parse_output(text, complete:)
14
- records, issues = [], []
14
+ records, issues = [], diagnostics
15
15
  clean(text).each_line.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)
@@ -15,7 +15,7 @@ module CtxChecker
15
15
 
16
16
  # 解析 show ntp status,保留不完整或不支持的证据。
17
17
  def parse_output(text, complete:)
18
- records, issues = [], []
18
+ records, issues = [], diagnostics
19
19
  clean(text).each_line.with_index(1) do |source, number|
20
20
  line = source.strip
21
21
  next if line.empty?
@@ -14,7 +14,7 @@ module CtxChecker
14
14
 
15
15
  # 解析 show processes cpu,保留不完整或不支持的证据。
16
16
  def parse_output(text, complete:)
17
- records, issues = [], []
17
+ records, issues = [], diagnostics
18
18
  recognized = false
19
19
  clean(text).each_line.with_index(1) do |source, number|
20
20
  line = source.strip
@@ -18,7 +18,7 @@ module CtxChecker
18
18
  module CiscoIos
19
19
  # IOS 与 IOS-XE 共用命令契约;型号特有语法通过解析器覆盖。
20
20
  class Profile < CtxParser::Profile
21
- CATALOG = CtxParser::CommandCatalog.new([
21
+ DECLARATIONS = CtxParser::Commands.new([
22
22
  { dimensions: %i[interfaces counters], command: "show interfaces", parser: Parsers::ShowInterfaces },
23
23
  { dimensions: [ :aggregation ], command: "show etherchannel summary", parser: Parsers::ShowEtherchannelSummary },
24
24
  { dimensions: [ :optics ], command: "show interfaces transceiver detail", parser: Parsers::ShowInterfacesTransceiverDetail },
@@ -32,8 +32,8 @@ module CtxChecker
32
32
  { dimensions: [ :alarms ], command: "show facility-alarm status", parser: Parsers::ShowFacilityAlarmStatus },
33
33
  { dimensions: [ :device_counters ], command: "show ip traffic", parser: Parsers::ShowIpTraffic }
34
34
  ])
35
- COMMANDS = CATALOG.commands
36
- PARSERS = CATALOG.parsers
35
+ COMMANDS = DECLARATIONS.by_dimension
36
+ PARSERS = DECLARATIONS.parsers
37
37
  end
38
38
  end
39
39
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative "../../parsers/base"
4
4
  require_relative "../../cisco_ios/parsers/system_values"
5
- require "ctx_parser/result_set"
5
+ require "ctx_parser/results"
6
6
 
7
7
  module CtxChecker
8
8
  module CiscoNxos
@@ -18,7 +18,7 @@ module CtxChecker
18
18
  # 解析 show environment,保留不完整或不支持的证据。
19
19
  def parse_output(text, complete:)
20
20
  @rows = { temperature: [], environment: [], power: [] }
21
- @problems = @rows.transform_values { [] }
21
+ @problems = @rows.transform_values { diagnostics }
22
22
  @section = nil
23
23
  @section_size = 0
24
24
  @power_columns = nil
@@ -34,7 +34,7 @@ module CtxChecker
34
34
  (dimension ? [ @problems[dimension] ] : @problems.values).each { |issues| issues << unparsed(number) }
35
35
  end
36
36
  finish_section
37
- CtxParser::ResultSet.new(results: @rows.to_h do |dimension, rows|
37
+ CtxParser::Results.new(by_dimension: @rows.to_h do |dimension, rows|
38
38
  identity = dimension == :temperature ? [ :component ] : %i[component kind]
39
39
  [ dimension, build_result(rows, complete: complete, recognized: rows.any?, issues: @problems[dimension], identity: identity) ]
40
40
  end)