ctx_checker 0.2.1 → 0.2.3

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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -0
  3. data/README-zh.md +7 -1
  4. data/README.md +7 -1
  5. data/lib/ctx_checker/checks/aggregation.rb +7 -3
  6. data/lib/ctx_checker/checks/base.rb +8 -2
  7. data/lib/ctx_checker/checks/routing.rb +10 -1
  8. data/lib/ctx_checker/cisco_ios/parsers/show_clock_detail.rb +1 -1
  9. data/lib/ctx_checker/cisco_ios/parsers/show_environment_all.rb +1 -1
  10. data/lib/ctx_checker/cisco_ios/parsers/show_etherchannel_summary.rb +1 -1
  11. data/lib/ctx_checker/cisco_ios/parsers/show_facility_alarm_status.rb +1 -1
  12. data/lib/ctx_checker/cisco_ios/parsers/show_interfaces.rb +1 -1
  13. data/lib/ctx_checker/cisco_ios/parsers/show_interfaces_transceiver_detail.rb +1 -1
  14. data/lib/ctx_checker/cisco_ios/parsers/show_ip_bgp_summary.rb +1 -1
  15. data/lib/ctx_checker/cisco_ios/parsers/show_ip_ospf_neighbor.rb +1 -1
  16. data/lib/ctx_checker/cisco_ios/parsers/show_ip_traffic.rb +1 -1
  17. data/lib/ctx_checker/cisco_ios/parsers/show_memory_statistics.rb +1 -1
  18. data/lib/ctx_checker/cisco_ios/parsers/show_ntp_status.rb +1 -1
  19. data/lib/ctx_checker/cisco_ios/parsers/show_processes_cpu.rb +1 -1
  20. data/lib/ctx_checker/cisco_nxos/parsers/show_environment.rb +1 -1
  21. data/lib/ctx_checker/cisco_nxos/parsers/show_interface_transceiver_details.rb +1 -1
  22. data/lib/ctx_checker/cisco_nxos/parsers/show_ip_traffic.rb +1 -1
  23. data/lib/ctx_checker/cisco_nxos/parsers/show_ntp_peer_status.rb +1 -1
  24. data/lib/ctx_checker/cisco_nxos/parsers/show_system_resources.rb +1 -1
  25. data/lib/ctx_checker/collector.rb +10 -3
  26. data/lib/ctx_checker/h3c/parsers/counters_table.rb +1 -1
  27. data/lib/ctx_checker/h3c/parsers/display_alarm_active.rb +1 -1
  28. data/lib/ctx_checker/h3c/parsers/display_bgp_peer.rb +1 -1
  29. data/lib/ctx_checker/h3c/parsers/display_clock.rb +1 -1
  30. data/lib/ctx_checker/h3c/parsers/display_cpu_usage.rb +1 -1
  31. data/lib/ctx_checker/h3c/parsers/display_environment.rb +1 -1
  32. data/lib/ctx_checker/h3c/parsers/display_fan.rb +1 -1
  33. data/lib/ctx_checker/h3c/parsers/display_interface.rb +2 -2
  34. data/lib/ctx_checker/h3c/parsers/display_link_aggregation_verbose.rb +1 -1
  35. data/lib/ctx_checker/h3c/parsers/display_memory.rb +2 -2
  36. data/lib/ctx_checker/h3c/parsers/display_ntp_service_status.rb +1 -1
  37. data/lib/ctx_checker/h3c/parsers/display_ospf_peer.rb +1 -1
  38. data/lib/ctx_checker/h3c/parsers/display_power.rb +1 -1
  39. data/lib/ctx_checker/h3c/parsers/display_transceiver_diagnosis.rb +1 -1
  40. data/lib/ctx_checker/hillstone/parsers/show_clock.rb +1 -1
  41. data/lib/ctx_checker/hillstone/parsers/show_cpu.rb +1 -1
  42. data/lib/ctx_checker/hillstone/parsers/show_cpu_cntr.rb +1 -1
  43. data/lib/ctx_checker/hillstone/parsers/show_environment.rb +1 -1
  44. data/lib/ctx_checker/hillstone/parsers/show_interface.rb +1 -1
  45. data/lib/ctx_checker/hillstone/parsers/show_interface_counter.rb +1 -1
  46. data/lib/ctx_checker/hillstone/parsers/show_ip_bgp_summary.rb +1 -1
  47. data/lib/ctx_checker/hillstone/parsers/show_memory.rb +1 -1
  48. data/lib/ctx_checker/hillstone/parsers/show_ntp_status.rb +1 -1
  49. data/lib/ctx_checker/hillstone/parsers/show_transceiver.rb +1 -1
  50. data/lib/ctx_checker/observation.rb +1 -2
  51. data/lib/ctx_checker/observation_merger.rb +26 -5
  52. data/lib/ctx_checker/palo_alto/parsers/show_clock.rb +1 -1
  53. data/lib/ctx_checker/palo_alto/parsers/show_counter_global.rb +1 -1
  54. data/lib/ctx_checker/palo_alto/parsers/show_counter_interface_all.rb +2 -2
  55. data/lib/ctx_checker/palo_alto/parsers/show_interface_all.rb +1 -1
  56. data/lib/ctx_checker/palo_alto/parsers/show_lacp_aggregate_ethernet.rb +1 -1
  57. data/lib/ctx_checker/palo_alto/parsers/show_ntp.rb +1 -1
  58. data/lib/ctx_checker/palo_alto/parsers/show_routing_protocol_bgp_peer.rb +1 -1
  59. data/lib/ctx_checker/palo_alto/parsers/show_routing_protocol_ospf_neighbor.rb +2 -2
  60. data/lib/ctx_checker/palo_alto/parsers/show_system_environmentals.rb +1 -1
  61. data/lib/ctx_checker/palo_alto/parsers/show_system_resources.rb +2 -2
  62. data/lib/ctx_checker/palo_alto/parsers/show_system_state_alarms.rb +1 -1
  63. data/lib/ctx_checker/palo_alto/parsers/show_transceiver_all.rb +1 -1
  64. data/lib/ctx_checker/parsers/base.rb +1 -1
  65. data/lib/ctx_checker/radware_alteon/parsers/info_l2_lacp_dump.rb +1 -1
  66. data/lib/ctx_checker/radware_alteon/parsers/info_l2_trunk.rb +1 -1
  67. data/lib/ctx_checker/radware_alteon/parsers/info_l3_bgp_summary.rb +1 -1
  68. data/lib/ctx_checker/radware_alteon/parsers/info_l3_ip.rb +1 -1
  69. data/lib/ctx_checker/radware_alteon/parsers/info_l3_ospf_nbr.rb +1 -1
  70. data/lib/ctx_checker/radware_alteon/parsers/info_link.rb +1 -1
  71. data/lib/ctx_checker/radware_alteon/parsers/info_sys_fan.rb +1 -1
  72. data/lib/ctx_checker/radware_alteon/parsers/info_sys_general.rb +1 -1
  73. data/lib/ctx_checker/radware_alteon/parsers/info_sys_ps.rb +1 -1
  74. data/lib/ctx_checker/radware_alteon/parsers/info_sys_temp.rb +1 -1
  75. data/lib/ctx_checker/radware_alteon/parsers/info_transceiver.rb +1 -1
  76. data/lib/ctx_checker/radware_alteon/parsers/stats_mp_cpu.rb +1 -1
  77. data/lib/ctx_checker/radware_alteon/parsers/stats_mp_mem.rb +1 -1
  78. data/lib/ctx_checker/radware_alteon/parsers/stats_port_ether.rb +1 -1
  79. data/lib/ctx_checker/radware_alteon/parsers/stats_port_if.rb +1 -1
  80. data/lib/ctx_checker/radware_alteon/parsers/stats_sp_allutils.rb +1 -1
  81. data/lib/ctx_checker/radware_alteon/parsers/stats_sys_ntp.rb +1 -1
  82. data/lib/ctx_checker/report.rb +6 -1
  83. data/lib/ctx_checker/snapshot.rb +3 -1
  84. data/lib/ctx_checker/version.rb +1 -1
  85. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 71962914e6b52e83b27e6c79345530ef9e92fb4168175a43e6cc46f464abc8f4
4
- data.tar.gz: e1e3adf05ec7d615920b49d65734e44cd3df35ba5efff584f11b5d0b5273df22
3
+ metadata.gz: 5439af7466ca88a19bb7e41cacb918eb78e73d8ae84b2390a060dfa75d5aba4a
4
+ data.tar.gz: b016023aac35315ea418c880a5b1feb29e277791702351feed6115e8eaaf0cd8
5
5
  SHA512:
6
- metadata.gz: 270d9b5bf20181cfc30f89dbe9b069ae5ac3499ab57efc726c991be5e2fe3f633156de1e8e51d612b7719be1981d74e89c26334fb546fbe19262f6290d759871
7
- data.tar.gz: 124f1e9f0609e3a46e382305ffd23768096b272868e852ffd1a25da317ae58defefac2ec1bfb6200fc94b381f1bb0bf594e69baf4d7db286731043c722ddb49e
6
+ metadata.gz: e087da6d36474f725cc0626762f79bdfd8b22fb28800fbb4fb98ce461ced95d6f8c72383e33be627c8ea513fe6ac988a0448d4a54ef6a1c6352ba11f2f673b91
7
+ data.tar.gz: bfa74421c286aefad66d9c7f2266367c79f2b6b006198b4ee461569f8ec4226b14046aeb7f8137872c38cafebe3187f35b7828c1c027b456a061430a7c9867be
data/CHANGELOG.md CHANGED
@@ -2,6 +2,27 @@
2
2
 
3
3
  Changes to the public contract are recorded here. Versions follow Semantic Versioning.
4
4
 
5
+ ## 0.2.3 (2026-10-18)
6
+
7
+ - Clear Bundler bootstrap variables when verifying packages in an isolated gem installation, including calls made through bundle exec.
8
+
9
+ - Index protocol coverage and conflicting scopes by field sets, preserving wildcard identities and OSPF interface scopes without quadratic scans.
10
+
11
+ ## 0.2.2 (2026-10-18)
12
+
13
+ - Snapshot validated collection targets and dimensions before caller callbacks can alter them.
14
+ - Require lifecycle connectors to provide exactly one session, preventing silent empty collections and repeated queries.
15
+ - Reject unrequested observations as command fragments rather than upgrading them to complete evidence.
16
+
17
+ - Exercise the current optional net-connector API and share protocol receipts across custom-session tests without tying generic fixtures to connector result internals.
18
+
19
+ - Preserve proven aggregation capacity lower bounds in partial observations while retaining unknown overall coverage.
20
+ - Parse vendor lines and reconstruct H3C counters independently of Ruby's global record and output separators.
21
+ - Require valid dates and explicit UTC offsets in snapshot and command evidence timestamps, consistent with record timestamps.
22
+ - Count only complete member sources when proving aggregation capacity; incomplete sources cannot prove a capacity shortage.
23
+ - Index command diagnostics once per observation when constructing reports, avoiding repeated scans for expanded port queries.
24
+ - Add a synthetic report construction benchmark for 64, 256, and 512 failed queries.
25
+
5
26
  ## 0.2.1 (2026-10-17)
6
27
 
7
28
  - Validate time-limit snapshot compatibility during policy construction; reject unsupported Numeric objects before evaluation while preserving exact Rational values.
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
- active = members.count { |row| row[:member_status] == :active && row[:oper_status] == :up && !member_down?(row) }
42
- known = group_rows.none? { |row| row[:member].nil? } && members.all? { |row| row[:member_status] && row[:member_status] != :unknown && %i[up down].include?(row[:oper_status]) }
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
- if status == :pass && !record_complete?(@current_record, @observation)
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,
@@ -57,6 +57,7 @@ module CtxChecker
57
57
  identities = {}
58
58
  scopes = {}
59
59
  incomplete = Hash.new { |hash, keys| hash[keys] = {} }
60
+ covered = covered_scope_index
60
61
  @observation.records.each do |record|
61
62
  identities[record.values_at(*fields)] = true
62
63
  scopes[record.values_at(*scope)] = true
@@ -67,7 +68,7 @@ module CtxChecker
67
68
  next if identities.key?(identity.values_at(*fields))
68
69
 
69
70
  ambiguous = incomplete.any? { |keys, values| values.key?(identity.values_at(*keys)) }
70
- enumerated = @observation.covered_scopes.any? { |entry| entry.all? { |key, value| identity[key] == value } }
71
+ enumerated = covered.any? { |keys, values| values.key?(identity.values_at(*keys)) }
71
72
  scope_observed = scopes.key?(identity.values_at(*scope))
72
73
  status = if ambiguous
73
74
  :unknown
@@ -81,6 +82,14 @@ module CtxChecker
81
82
  finding(status, :expected_peer_missing, entity: identity, severity: :critical)
82
83
  end
83
84
  end
85
+
86
+ # 按字段组合索引覆盖范围;OSPF 的进程级与接口级证明分别匹配。
87
+ def covered_scope_index
88
+ @observation.covered_scopes.each_with_object({}) do |entry, index|
89
+ fields = entry.keys.sort
90
+ (index[fields] ||= {})[entry.values_at(*fields)] = true
91
+ end
92
+ end
84
93
  end
85
94
  end
86
95
  end
@@ -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)
@@ -81,7 +81,7 @@ module CtxChecker
81
81
  # 仅拼接文档明确的续行;未完成的记录不能吞并新的作用域。
82
82
  def each_ntp_line(source, issues)
83
83
  pending = nil
84
- source.each_line.with_index(1) do |raw, number|
84
+ source.each_line("\n").with_index(1) do |raw, number|
85
85
  line = raw.strip
86
86
  next if line.empty?
87
87
 
@@ -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
@@ -15,7 +15,7 @@ module CtxChecker
15
15
  def parse_output(text, complete:)
16
16
  source = clean(text)
17
17
  zones, clocks = [], []
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
 
@@ -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