netstack 0.1.1 → 0.2.0

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 (279) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -3
  3. data/{LICENSE.txt → LICENSE} +7 -6
  4. data/NOTICE +25 -0
  5. data/README.md +58 -20
  6. data/docs/acceptance.md +92 -0
  7. data/docs/backups.md +100 -0
  8. data/docs/configuration.md +198 -0
  9. data/docs/connector.md +187 -0
  10. data/docs/ddi.md +142 -0
  11. data/docs/deployment.md +47 -0
  12. data/docs/foundations.md +45 -0
  13. data/docs/implementation.md +68 -0
  14. data/docs/inventory.md +42 -0
  15. data/docs/monitoring.md +227 -0
  16. data/docs/motor_integration.md +96 -0
  17. data/docs/performance.md +39 -0
  18. data/docs/release.md +20 -0
  19. data/docs/vxlan.md +91 -0
  20. data/docs/wireless.md +81 -0
  21. data/lib/netstack/backups/artifact.rb +23 -0
  22. data/lib/netstack/backups/diff.rb +15 -0
  23. data/lib/netstack/backups/repository.rb +77 -0
  24. data/lib/netstack/backups/request.rb +78 -0
  25. data/lib/netstack/circuits/business_calendar.rb +51 -0
  26. data/lib/netstack/circuits/capacity_review.rb +191 -0
  27. data/lib/netstack/circuits/endpoint.rb +25 -0
  28. data/lib/netstack/circuits/monitoring_plan.rb +212 -0
  29. data/lib/netstack/configuration/artifact.rb +85 -0
  30. data/lib/netstack/configuration/artifact_resolver.rb +30 -0
  31. data/lib/netstack/configuration/bootstrap/payload.rb +67 -0
  32. data/lib/netstack/configuration/bootstrap/validator.rb +224 -0
  33. data/lib/netstack/configuration/catalog.rb +60 -0
  34. data/lib/netstack/configuration/generator.rb +87 -0
  35. data/lib/netstack/configuration/input_contract.rb +82 -0
  36. data/lib/netstack/configuration/payload.rb +168 -0
  37. data/lib/netstack/configuration/reader.rb +124 -0
  38. data/lib/netstack/configuration/support.rb +30 -0
  39. data/lib/netstack/configuration/system_configuration.rb +56 -0
  40. data/lib/netstack/configuration/template_manager.rb +174 -0
  41. data/lib/netstack/configuration/template_store.rb +40 -0
  42. data/lib/netstack/configuration/validator.rb +566 -0
  43. data/lib/netstack/connector/authentication.rb +77 -0
  44. data/lib/netstack/connector/authentication_error.rb +11 -0
  45. data/lib/netstack/connector/backup.rb +19 -0
  46. data/lib/netstack/connector/backup_busy.rb +11 -0
  47. data/lib/netstack/connector/backup_persistence_error.rb +32 -0
  48. data/lib/netstack/connector/command.rb +63 -0
  49. data/lib/netstack/connector/command_result.rb +24 -0
  50. data/lib/netstack/connector/command_timeout.rb +11 -0
  51. data/lib/netstack/connector/configuration.rb +138 -0
  52. data/lib/netstack/connector/connection_closed.rb +11 -0
  53. data/lib/netstack/connector/connection_error.rb +11 -0
  54. data/lib/netstack/connector/device.rb +284 -0
  55. data/lib/netstack/connector/device_error.rb +11 -0
  56. data/lib/netstack/connector/dialogue.rb +55 -0
  57. data/lib/netstack/connector/error.rb +44 -0
  58. data/lib/netstack/connector/error_metadata.rb +58 -0
  59. data/lib/netstack/connector/event.rb +24 -0
  60. data/lib/netstack/connector/execution.rb +110 -0
  61. data/lib/netstack/connector/interaction.rb +55 -0
  62. data/lib/netstack/connector/internal_error.rb +11 -0
  63. data/lib/netstack/connector/known_hosts.rb +128 -0
  64. data/lib/netstack/connector/local_backup.rb +63 -0
  65. data/lib/netstack/connector/log/event.rb +65 -0
  66. data/lib/netstack/connector/log/formatter.rb +18 -0
  67. data/lib/netstack/connector/log/messages.rb +54 -0
  68. data/lib/netstack/connector/log/redacting_writer.rb +33 -0
  69. data/lib/netstack/connector/log/transcript.rb +35 -0
  70. data/lib/netstack/connector/log.rb +262 -0
  71. data/lib/netstack/connector/log_error.rb +11 -0
  72. data/lib/netstack/connector/login_timeout.rb +11 -0
  73. data/lib/netstack/connector/output_limit_exceeded.rb +11 -0
  74. data/lib/netstack/connector/parsing_error.rb +11 -0
  75. data/lib/netstack/connector/profile/builder.rb +238 -0
  76. data/lib/netstack/connector/profile.rb +225 -0
  77. data/lib/netstack/connector/prompt_error.rb +11 -0
  78. data/lib/netstack/connector/recovery.rb +59 -0
  79. data/lib/netstack/connector/redactor.rb +100 -0
  80. data/lib/netstack/connector/response.rb +25 -0
  81. data/lib/netstack/connector/response_reader.rb +140 -0
  82. data/lib/netstack/connector/result.rb +50 -0
  83. data/lib/netstack/connector/running_config/rendered.rb +16 -0
  84. data/lib/netstack/connector/running_config/strategy.rb +28 -0
  85. data/lib/netstack/connector/running_config.rb +113 -0
  86. data/lib/netstack/connector/save_config.rb +23 -0
  87. data/lib/netstack/connector/script.rb +59 -0
  88. data/lib/netstack/connector/script_error.rb +11 -0
  89. data/lib/netstack/connector/script_output_limit_exceeded.rb +11 -0
  90. data/lib/netstack/connector/session.rb +414 -0
  91. data/lib/netstack/connector/session_busy.rb +11 -0
  92. data/lib/netstack/connector/storage/backup_lock.rb +117 -0
  93. data/lib/netstack/connector/storage/private_file.rb +111 -0
  94. data/lib/netstack/connector/storage/safe_file.rb +64 -0
  95. data/lib/netstack/connector/storage/saved_config.rb +76 -0
  96. data/lib/netstack/connector/terminal_renderer.rb +186 -0
  97. data/lib/netstack/connector/terminal_text.rb +32 -0
  98. data/lib/netstack/connector/textfsm.rb +71 -0
  99. data/lib/netstack/connector/tftp/file_upload.rb +53 -0
  100. data/lib/netstack/connector/tftp/strategy.rb +67 -0
  101. data/lib/netstack/connector/tftp.rb +116 -0
  102. data/lib/netstack/connector/tftp_completion_error.rb +36 -0
  103. data/lib/netstack/connector/tftp_receipt.rb +54 -0
  104. data/lib/netstack/connector/tftp_target.rb +65 -0
  105. data/lib/netstack/connector/topology/checkpoint_error.rb +16 -0
  106. data/lib/netstack/connector/topology/deployment_adapter.rb +163 -0
  107. data/lib/netstack/connector/topology/execution_result.rb +34 -0
  108. data/lib/netstack/connector/topology/immediate_strategy.rb +39 -0
  109. data/lib/netstack/connector/topology/interface_description.rb +46 -0
  110. data/lib/netstack/connector/topology/interface_name.rb +60 -0
  111. data/lib/netstack/connector/topology/plan.rb +105 -0
  112. data/lib/netstack/connector/topology/strategy.rb +76 -0
  113. data/lib/netstack/connector/topology.rb +255 -0
  114. data/lib/netstack/connector/transport_error.rb +11 -0
  115. data/lib/netstack/connector/transports.rb +201 -0
  116. data/lib/netstack/connector/underlying_error.rb +25 -0
  117. data/lib/netstack/connector/unsupported_operation.rb +11 -0
  118. data/lib/netstack/connector/vendors/cisco_ios/running_config.rb +22 -0
  119. data/lib/netstack/connector/vendors/cisco_ios/tftp_backup.rb +41 -0
  120. data/lib/netstack/connector/vendors/cisco_ios/topology.rb +54 -0
  121. data/lib/netstack/connector/vendors/cisco_ios.rb +44 -0
  122. data/lib/netstack/connector/vendors/cisco_nxos/running_config.rb +25 -0
  123. data/lib/netstack/connector/vendors/cisco_nxos/tftp_backup.rb +43 -0
  124. data/lib/netstack/connector/vendors/cisco_nxos.rb +51 -0
  125. data/lib/netstack/connector/vendors/h3c/tftp_backup.rb +34 -0
  126. data/lib/netstack/connector/vendors/h3c/topology.rb +72 -0
  127. data/lib/netstack/connector/vendors/h3c.rb +75 -0
  128. data/lib/netstack/connector/vendors/h3c_wireless.rb +13 -0
  129. data/lib/netstack/connector/vendors/hillstone/running_config.rb +16 -0
  130. data/lib/netstack/connector/vendors/hillstone/tftp_backup.rb +55 -0
  131. data/lib/netstack/connector/vendors/hillstone/topology.rb +64 -0
  132. data/lib/netstack/connector/vendors/hillstone.rb +39 -0
  133. data/lib/netstack/connector/vendors/huawei/tftp_backup.rb +26 -0
  134. data/lib/netstack/connector/vendors/huawei.rb +62 -0
  135. data/lib/netstack/connector/vendors/palo_alto/running_config.rb +77 -0
  136. data/lib/netstack/connector/vendors/palo_alto/tftp_backup.rb +49 -0
  137. data/lib/netstack/connector/vendors/palo_alto/topology.rb +77 -0
  138. data/lib/netstack/connector/vendors/palo_alto.rb +39 -0
  139. data/lib/netstack/connector/vendors/radware/running_config.rb +21 -0
  140. data/lib/netstack/connector/vendors/radware/tftp_backup.rb +49 -0
  141. data/lib/netstack/connector/vendors/radware/topology.rb +19 -0
  142. data/lib/netstack/connector/vendors/radware.rb +59 -0
  143. data/lib/netstack/connector/write_timeout.rb +11 -0
  144. data/lib/netstack/connector.rb +32 -0
  145. data/lib/netstack/ddi/applier.rb +107 -0
  146. data/lib/netstack/ddi/deployment.rb +137 -0
  147. data/lib/netstack/ddi/inputs.rb +61 -0
  148. data/lib/netstack/ddi/manager.rb +197 -0
  149. data/lib/netstack/ddi/plan.rb +99 -0
  150. data/lib/netstack/ddi/scope.rb +127 -0
  151. data/lib/netstack/ddi/settings.rb +44 -0
  152. data/lib/netstack/ddi/state.rb +166 -0
  153. data/lib/netstack/deployment/checkpoint_error.rb +14 -0
  154. data/lib/netstack/deployment/cli_adapter.rb +64 -0
  155. data/lib/netstack/deployment/executor.rb +131 -0
  156. data/lib/netstack/deployment/plan.rb +82 -0
  157. data/lib/netstack/deployment/receipt.rb +78 -0
  158. data/lib/netstack/deployment/recovery.rb +19 -0
  159. data/lib/netstack/deployment/step.rb +16 -0
  160. data/lib/netstack/dimension.rb +30 -0
  161. data/lib/netstack/error.rb +14 -0
  162. data/lib/netstack/input_error.rb +9 -0
  163. data/lib/netstack/integrations/bluecat/client.rb +84 -0
  164. data/lib/netstack/integrations/http/client.rb +146 -0
  165. data/lib/netstack/integrations/http/error.rb +10 -0
  166. data/lib/netstack/integrations/http/response.rb +28 -0
  167. data/lib/netstack/integrations/http_delivery/adapter.rb +51 -0
  168. data/lib/netstack/integrations/http_delivery/client.rb +127 -0
  169. data/lib/netstack/integrations/http_delivery/result.rb +27 -0
  170. data/lib/netstack/integrations/oxidized/client.rb +89 -0
  171. data/lib/netstack/integrations/zabbix/client.rb +211 -0
  172. data/lib/netstack/integrations/zabbix/error.rb +10 -0
  173. data/lib/netstack/inventory/address.rb +30 -0
  174. data/lib/netstack/inventory/changes.rb +13 -0
  175. data/lib/netstack/inventory/cli_parser.rb +75 -0
  176. data/lib/netstack/inventory/collector.rb +99 -0
  177. data/lib/netstack/inventory/device.rb +14 -0
  178. data/lib/netstack/inventory/interface.rb +25 -0
  179. data/lib/netstack/inventory/neighbor.rb +15 -0
  180. data/lib/netstack/inventory/reconciler.rb +74 -0
  181. data/lib/netstack/inventory/snapshot.rb +69 -0
  182. data/lib/netstack/inventory/transceiver.rb +23 -0
  183. data/lib/netstack/ipv4.rb +204 -0
  184. data/lib/netstack/ipv6.rb +88 -0
  185. data/lib/netstack/monitoring/applier.rb +179 -0
  186. data/lib/netstack/monitoring/device_plan.rb +23 -0
  187. data/lib/netstack/monitoring/host_plan.rb +152 -0
  188. data/lib/netstack/monitoring/inputs.rb +73 -0
  189. data/lib/netstack/monitoring/measurements.rb +89 -0
  190. data/lib/netstack/monitoring/mutation.rb +37 -0
  191. data/lib/netstack/monitoring/plan.rb +69 -0
  192. data/lib/netstack/monitoring/probe_plan.rb +19 -0
  193. data/lib/netstack/monitoring/state_comparison.rb +37 -0
  194. data/lib/netstack/range_set.rb +181 -0
  195. data/lib/netstack/routing/lookup.rb +46 -0
  196. data/lib/netstack/routing/lookup_result.rb +14 -0
  197. data/lib/netstack/routing/route.rb +32 -0
  198. data/lib/netstack/routing/snapshot.rb +23 -0
  199. data/lib/netstack/target.rb +15 -0
  200. data/lib/netstack/unsupported_capability.rb +9 -0
  201. data/lib/netstack/values.rb +70 -0
  202. data/lib/netstack/version.rb +1 -1
  203. data/lib/netstack/vxlan/auditor.rb +187 -0
  204. data/lib/netstack/vxlan/cisco_nxos/config_parser.rb +327 -0
  205. data/lib/netstack/vxlan/cisco_nxos/deployment_adapter.rb +63 -0
  206. data/lib/netstack/vxlan/cisco_nxos/runtime_parser.rb +182 -0
  207. data/lib/netstack/vxlan/cisco_nxos/segment_renderer.rb +42 -0
  208. data/lib/netstack/vxlan/collector.rb +77 -0
  209. data/lib/netstack/vxlan/fabric.rb +29 -0
  210. data/lib/netstack/vxlan/finding.rb +20 -0
  211. data/lib/netstack/vxlan/node.rb +51 -0
  212. data/lib/netstack/vxlan/route_policy.rb +57 -0
  213. data/lib/netstack/vxlan/runtime_result.rb +24 -0
  214. data/lib/netstack/vxlan/segment.rb +35 -0
  215. data/lib/netstack/vxlan/segment_plan.rb +25 -0
  216. data/lib/netstack/vxlan/segment_planner.rb +156 -0
  217. data/lib/netstack/vxlan/snapshot.rb +34 -0
  218. data/lib/netstack/vxlan/validation.rb +58 -0
  219. data/lib/netstack/vxlan/verifier.rb +52 -0
  220. data/lib/netstack/vxlan/vpc_domain.rb +20 -0
  221. data/lib/netstack/wireless/access_point.rb +12 -0
  222. data/lib/netstack/wireless/ap_group.rb +12 -0
  223. data/lib/netstack/wireless/assessment.rb +13 -0
  224. data/lib/netstack/wireless/assessor.rb +83 -0
  225. data/lib/netstack/wireless/authentication_connection.rb +12 -0
  226. data/lib/netstack/wireless/bss.rb +12 -0
  227. data/lib/netstack/wireless/client.rb +12 -0
  228. data/lib/netstack/wireless/collector.rb +191 -0
  229. data/lib/netstack/wireless/configuration.rb +13 -0
  230. data/lib/netstack/wireless/finding.rb +12 -0
  231. data/lib/netstack/wireless/h3c/config_parser.rb +229 -0
  232. data/lib/netstack/wireless/h3c/lldp_parser.rb +69 -0
  233. data/lib/netstack/wireless/h3c/runtime_parser.rb +240 -0
  234. data/lib/netstack/wireless/huawei/runtime_parser.rb +83 -0
  235. data/lib/netstack/wireless/invalid_snapshot.rb +11 -0
  236. data/lib/netstack/wireless/neighbor.rb +12 -0
  237. data/lib/netstack/wireless/normalizer.rb +90 -0
  238. data/lib/netstack/wireless/parse_result.rb +13 -0
  239. data/lib/netstack/wireless/radio.rb +12 -0
  240. data/lib/netstack/wireless/reconciler.rb +115 -0
  241. data/lib/netstack/wireless/reconciliation.rb +11 -0
  242. data/lib/netstack/wireless/security_policy.rb +12 -0
  243. data/lib/netstack/wireless/service_template.rb +12 -0
  244. data/lib/netstack/wireless/service_template_bindings.rb +56 -0
  245. data/lib/netstack/wireless/snapshot.rb +150 -0
  246. data/lib/netstack/wireless/state.rb +23 -0
  247. data/lib/netstack/wireless/view.rb +154 -0
  248. data/lib/netstack.rb +10 -3
  249. data/resources/configuration/cisco-bootstrap.erb +118 -0
  250. data/resources/configuration/cisco-distributed-gateway.erb +40 -0
  251. data/resources/configuration/cisco-n9k-vpc.erb +62 -0
  252. data/resources/configuration/cisco-server-port.erb +43 -0
  253. data/resources/configuration/h3c-bootstrap.erb +108 -0
  254. data/resources/configuration/h3c-irf.erb +57 -0
  255. data/resources/configuration/h3c-server-port.erb +36 -0
  256. data/resources/configuration/hillstone-ipsec-vpn.erb +68 -0
  257. data/resources/configuration/hillstone-policy.erb +31 -0
  258. data/resources/configuration/paloalto-ipsec-vpn.erb +39 -0
  259. data/resources/configuration/radware-load-balancer.erb +26 -0
  260. data/resources/textfsm/cisco_cdp_neighbors_detail.textfsm +7 -0
  261. data/resources/textfsm/cisco_ios_running_config_interfaces.textfsm +12 -0
  262. data/resources/textfsm/cisco_ios_show_ip_interface_brief.textfsm +7 -0
  263. data/resources/textfsm/h3c_interface_descriptions.textfsm +11 -0
  264. data/resources/textfsm/h3c_lldp_local_first.textfsm +8 -0
  265. data/resources/textfsm/h3c_lldp_name_first.textfsm +8 -0
  266. data/resources/textfsm/hillstone_interface_descriptions.textfsm +11 -0
  267. data/resources/textfsm/hillstone_lldp_neighbors.textfsm +7 -0
  268. data/resources/textfsm/index +8 -0
  269. data/resources/textfsm/palo_alto_interface_descriptions.textfsm +6 -0
  270. data/resources/textfsm/palo_alto_lldp_neighbors.textfsm +11 -0
  271. data/resources/textfsm/radware_port_names.textfsm +13 -0
  272. metadata +588 -27
  273. data/.rubocop.yml +0 -13
  274. data/CODE_OF_CONDUCT.md +0 -84
  275. data/Gemfile +0 -10
  276. data/Gemfile.lock +0 -40
  277. data/Rakefile +0 -8
  278. data/bin/console +0 -15
  279. data/bin/setup +0 -8
@@ -0,0 +1,227 @@
1
+ # Monitoring and circuit capacity
2
+
3
+ The monitoring API is plain Ruby. It does not discover devices, query Netdisco,
4
+ load application configuration, or persist records. The host supplies authorized
5
+ targets, Zabbix resource identifiers, credentials and durable checkpoints.
6
+
7
+ ## Zabbix and HTTP boundaries
8
+
9
+ ```ruby
10
+ client = Netstack::Integrations::Zabbix::Client.new(
11
+ api_url: "https://zabbix.example.test/api_jsonrpc.php",
12
+ token: credential_store.fetch("zabbix/api")
13
+ )
14
+ client.api_version
15
+ ```
16
+
17
+ The client targets the Zabbix 7.0 API shape: `selectHostGroups` returns
18
+ `hostgroups`, normalized to `groups` in host snapshots. Trigger queries request
19
+ expanded expressions for readback. Requests validate JSON-RPC version, exact
20
+ integer request ID, mutually exclusive result/error, provider error types and
21
+ resource ID receipts. Malformed query rows are errors, not absent resources.
22
+ Only the provider's validated numeric error code is retained; remote messages
23
+ and response bodies do not appear in exceptions.
24
+
25
+ The shared `Integrations::Http::Client` takes `base_url:`, optional `headers:`,
26
+ `open_timeout:`, `read_timeout:`, `write_timeout:`, `max_response_bytes:` and
27
+ `verify_tls:`. Its `request(method, path = "", query: {}, headers: {}, json: nil,
28
+ body: nil)` returns `Http::Response` with `status`, `headers`, `body` and `json`.
29
+ TLS verification is on by default. Cross-origin paths, redirects, invalid headers
30
+ and oversized responses are rejected. No request is implicitly retried.
31
+
32
+ An injected HTTP transport implements `call(method:, url:, headers:, body:,
33
+ open_timeout:, read_timeout:, write_timeout:, max_response_bytes:, verify_tls:)`
34
+ and returns `Http::Response`. Client and response inspection omit credentials
35
+ and bodies. Explicit `body` access remains available to integrations that need it.
36
+
37
+ ## Device and probe plans
38
+
39
+ ```ruby
40
+ target = Netstack::Target.new(
41
+ key: "switch-serial-001", host: "192.0.2.10", platform: "h3c"
42
+ )
43
+ plan = Netstack::Monitoring::DevicePlan.new(client: client).call(
44
+ target: target, groups: ["14"], templates: ["27"],
45
+ community_ref: "devices/switch-serial-001/snmp",
46
+ previous_managed: previous_result.fetch("managed", {})
47
+ )
48
+ persist_plan(plan.to_h, digest: plan.digest)
49
+ ```
50
+
51
+ `DevicePlan` manages an SNMPv2 interface and a secret community macro. It stores
52
+ a credential reference, never the community itself. SNMPv3 is not claimed by this
53
+ entry point. `ProbePlan` accepts `target`, `groups`, `templates` and optional
54
+ `proxy_group_id`; the supplied template must implement the intended ICMP probe.
55
+ Both accept visible `name`, `enabled`, `inventory`, `tags` and `previous_managed`.
56
+ Group/template IDs are explicit: the library does not create shared group or
57
+ template resources as a side effect of planning.
58
+
59
+ Planning reads remote state without writing it. Old managed group/template IDs
60
+ and tag names are removed, while unrelated members and inventory fields survive.
61
+ Only `netstack.*` tag names may be declared as previously managed. A disabled
62
+ absent host is not created. Matching probe state produces an empty mutation plan.
63
+ Secret macro rotation is an explicit mutation because hidden values cannot be
64
+ compared with their previous contents.
65
+
66
+ Disabling an existing host changes only its enabled status and preserves its
67
+ previously managed group/template IDs and tag names. Membership changes requested
68
+ while disabled are applied when the host is enabled again. A disabled absent host
69
+ has no managed members. Pass the last verified result's `managed` value back to
70
+ planning; when it includes `target`, that identity must match the current target.
71
+ Older callers may omit `target`, but all ownership fields are still validated.
72
+
73
+ ## Durable application and recovery
74
+
75
+ ```ruby
76
+ applier = Netstack::Monitoring::Applier.new(client: client)
77
+ result = applier.call(
78
+ plan: plan,
79
+ credentials: ->(reference) { credential_store.fetch(reference) },
80
+ checkpoint: ->(event) {
81
+ persist_event_in_its_own_committed_transaction(event)
82
+ true
83
+ }
84
+ )
85
+ ```
86
+
87
+ Each mutation emits `started` before the external write, `receipt` after a valid
88
+ ID response, and `verified` after readback. Returning anything except literal
89
+ `true`, or raising in the callback, stops execution. A callback inside an
90
+ uncommitted outer database transaction is not a durable checkpoint. A progress
91
+ observer cannot substitute for this callback.
92
+ Callback exceptions are reported as `checkpoint_failed` without retaining their
93
+ message or exception cause; durable events already accepted by the host remain
94
+ the recovery source.
95
+
96
+ The host owns plan storage, authorization, serialization per target, run ownership
97
+ and revision checks. It must bind checkpoint records to the current run, enforce
98
+ their order, and refuse stale workers. Remote Zabbix operations do not provide
99
+ database-style compare-and-swap: the library's preflight checks detect observed
100
+ drift but do not replace host locking. A plan and its events must remain immutable.
101
+
102
+ After interruption, reconstruct with `Monitoring::Plan.from_h` and call
103
+ `applier.recover(plan:, events:, checkpoint:, credentials:)`. Events must belong
104
+ to the same plan digest and follow the exact stage order. Recovery reads back an
105
+ already-started mutation; it never repeats that write. If the state is not
106
+ confirmed, it raises `monitoring_outcome_unknown` and stops. Unstarted operations
107
+ may continue after previous operations have been verified. Calling `call` again
108
+ with an interrupted plan is not the recovery protocol.
109
+
110
+ Secret macro values cannot be read back. A valid write receipt plus the macro's
111
+ name/type/identity yields a `verified` event whose `verification` is `metadata`.
112
+ This does not prove the secret's bytes or device authentication. If its receipt
113
+ was lost, recovery leaves the macro outcome unknown even if a matching macro
114
+ exists. Credential resolver failures are sanitized and occur before `started`.
115
+
116
+ ## Circuit monitoring
117
+
118
+ `Circuits::Endpoint` describes side `a` or `z`. A managed endpoint requires
119
+ `host_id`, `technical_host` and `interface_name`; an external endpoint can carry
120
+ only its address. Optional `layer3: true` enables peer reachability when the other
121
+ endpoint has an address.
122
+
123
+ `Circuits::MonitoringPlan#call(key:, name:, bandwidth_mbps:, endpoints:,
124
+ previous_host_ids: [], thresholds: {})` builds status, high-bandwidth and
125
+ low-bandwidth triggers. Traffic triggers depend on interface status. At least one
126
+ traffic direction, speed and interface-status items are required; ambiguous item
127
+ matches fail. Numeric interface boundaries distinguish `Gi1` from `Gi10`.
128
+ Thresholds enforce high/recovery and low/recovery hysteresis. Unsafe expression
129
+ keys are rejected rather than interpolated into expressions.
130
+
131
+ Old endpoint assignments are cleaned across all previous and current hosts, using
132
+ the host and side together. Removing one side on a retained host or exchanging
133
+ endpoint hosts disables the obsolete triggers. Only triggers with the exact circuit
134
+ ownership tag are cleanup targets; manual triggers survive. A trigger associated
135
+ with several hosts is updated once, and an assignment retained on a current endpoint
136
+ is preserved. All started events include the
137
+ plan's `managed.touched_host_ids`, containing old and new hosts. The host must
138
+ persist that scope before acknowledging the first write; it is needed if endpoint
139
+ replacement succeeds remotely but the process exits before local finalization.
140
+ After a partial failure, retain the scope and original plan for recovery.
141
+
142
+ ## Measurements and capacity review
143
+
144
+ `Monitoring::Measurements#history` and `#trends` accept an item ID, epoch `from` /
145
+ `to`, and a limit. History also takes numeric `value_type` 0 or 3. They validate
146
+ identity, numeric values, timestamps, duplicates and interval bounds. Reaching the
147
+ limit reports `truncated`; zero rows report `empty`. Missing data is never zero.
148
+ Trend min/average/max remain aggregates and are not raw history percentiles.
149
+
150
+ `Measurements.align(inbound:, outbound:)` joins exact history timestamps, leaving
151
+ an absent direction as `nil`. For capacity assessment, `Circuits::CapacityReview`
152
+ buckets actual observations using an explicit sample interval. It never forwards
153
+ a value across an empty interval. Different nanosecond observations in the same
154
+ interval retain the latest actual value for each direction.
155
+ Both history retrieval and capacity summaries include `from` and `to`; a sample
156
+ at the ending timestamp is retained. Summaries record `from`, `to` and
157
+ `interval_seconds` alongside their coverage and statistics.
158
+
159
+ ```ruby
160
+ calendar = Netstack::Circuits::BusinessCalendar.new(
161
+ utc_offset: "+08:00", starts_at: "09:00", ends_at: "18:00", holidays: []
162
+ )
163
+ review = Netstack::Circuits::CapacityReview.new(calendar: calendar)
164
+ end_epoch = Time.now.to_i
165
+ start_epoch = end_epoch - (7 * 86_400)
166
+ summary = review.summarize(
167
+ samples: samples, inbound_capacity_mbps: 100, outbound_capacity_mbps: 100,
168
+ from: start_epoch, to: end_epoch, complete: !truncated
169
+ )
170
+ decision = review.decide(windows: { "7" => summary })
171
+ ```
172
+
173
+ Defaults are 300-second sampling, 90% coverage in each direction and at least 12
174
+ business-time observations per direction. P95 uses nearest rank. Gaps break
175
+ continuous high-utilization runs; incomplete days cannot count as continuously
176
+ low workdays. Off-hours peaks are reported separately and do not drive resizing.
177
+ Decisions distinguish insufficient data, watch, expand review, expand now and
178
+ shrink review. Seven-day emergency expansion requires that window to be valid.
179
+ Shrink review requires ten consecutive complete low workdays in a valid window,
180
+ or both valid low 15/31-day windows. `decide` validates that
181
+ keys `7`, `15` and `31` correspond to exactly that many elapsed 24-hour periods,
182
+ and that compared windows share their ending timestamp. Unknown labels, wrong
183
+ durations and legacy summaries without interval metadata cannot produce resizing
184
+ advice. These are rolling elapsed-time windows; the business calendar determines
185
+ which observations inside them count as business time. Units for utilization
186
+ are ratios (`0.95`), and traffic/capacity calculations use bits per second.
187
+
188
+ The bundled calendar uses an explicit fixed UTC offset, weekdays and holidays.
189
+ For DST, inject a calendar implementing `call(Time) -> {"date", "business"}`,
190
+ `daily_seconds`, and `consecutive_workdays?`; do not mutate process timezone.
191
+ Capacity decisions are evidence for review, not automatic circuit resize orders.
192
+
193
+ ## Evidence and references
194
+
195
+ The host stores each plan and checkpoint stream under a separate execution ID,
196
+ with an exclusive execution lock and uniqueness constraints. A plan digest
197
+ identifies content, not an execution: never reuse events from another execution
198
+ of the same plan. Pass `execution_id:` to both `Applier#call` and `#recover` to
199
+ bind the event stream explicitly; omitting it is supported only for hosts that
200
+ already enforce stream isolation. Recovery also validates resource and managed
201
+ metadata. An empty plan returns `status: "verified"` and emits no remote
202
+ write checkpoints; the host records that terminal result itself. Checkpoint
203
+ callbacks must return `true` only after their enclosing transaction commits.
204
+
205
+ Existing SNMP interface communities and authentication/privacy passphrases are
206
+ never copied into plan preconditions. Non-secret interface fields and host ID
207
+ remain preconditions; concurrent changes to unreadable credential bytes cannot
208
+ be checked. The desired community is the macro-reference string, whose readback
209
+ can be checked. Secret macro bytes remain metadata-only verification. Existing
210
+ manual trigger dependencies are preserved alongside the managed status dependency.
211
+ Circuit item association requires the explicit interface name in an item key or
212
+ name; an OID or an ifIndex is not treated as a port identity. Hosts with abbreviated
213
+ or otherwise differing names must supply the matching interface name explicitly.
214
+
215
+ HTTP defaults include a 60-second total request budget (`request_timeout`) in
216
+ addition to open/read/write timeouts. The total budget also wraps an injected
217
+ transport; a transport must cooperate with Ruby interruption and must not catch
218
+ or suppress timeout exceptions. No HTTP write is automatically retried.
219
+
220
+ Local tests cover protocol failures, resource ID mismatches, durable checkpoint
221
+ failures, lost receipts, secret redaction, stale plans, manual-field preservation,
222
+ old-host cleanup, missing measurements and capacity gaps. These are controlled
223
+ HTTP/Zabbix fixtures; no real Zabbix server, SNMP device or host database was used.
224
+
225
+ - [Zabbix 7.0 host.get](https://www.zabbix.com/documentation/7.0/en/manual/api/reference/host/get)
226
+ - [Zabbix 7.0 trigger.get](https://www.zabbix.com/documentation/7.0/en/manual/api/reference/trigger/get)
227
+ - [Zabbix 7.0 usermacro.get](https://www.zabbix.com/documentation/7.0/en/manual/api/reference/usermacro/get)
@@ -0,0 +1,96 @@
1
+ # Motor 宿主接入
2
+
3
+ Netstack 不引入 Rails 持久化。Motor 保留授权、加密配置、数据库约束、锁、任务、审计和通知;下列入口在 Motor 工作树中实际实现并具有 RSpec 契约测试。
4
+
5
+ ## 显式集成 bundle
6
+
7
+ Motor 的 `backend/Gemfile.netstack` 先读取其现有 Gemfile,再通过 `NETSTACK_PATH` 引入本库。这样可以验证尚未发布的库,而不把本地兄弟目录写死到生产依赖,也不错误地从 RubyGems 获取同名的其他包。
8
+
9
+ ```sh
10
+ cd /path/to/motor/backend
11
+ NETSTACK_PATH=/path/to/netstack BUNDLE_GEMFILE=Gemfile.netstack bundle install
12
+ NETSTACK_PATH=/path/to/netstack BUNDLE_GEMFILE=Gemfile.netstack bundle exec rspec \
13
+ spec/services/motor/wireless/cli_sync_spec.rb \
14
+ spec/services/motor/devices/netstack_repository_spec.rb \
15
+ spec/netstack/netconf_generator_contract.rb \
16
+ spec/netstack/delivery_contract.rb \
17
+ spec/netstack/monitoring_execution_contract.rb \
18
+ spec/netstack/ddi_execution_contract.rb
19
+ ```
20
+
21
+ 使用 Motor 自己要求的 Ruby 版本和独立测试 PostgreSQL/Redis。集成 bundle 不改变生产 Gemfile/lock。正式切换依赖源与入口、发布 gem 或部署服务是另外的交付动作;本次没有执行这些动作。
22
+
23
+ ## 无线采集
24
+
25
+ `Motor::Wireless::CliSync#call(controller:, platform:, dimensions: nil, expected_revision: nil)` 调用 Netstack 只读采集,再映射至 Motor 已有的 AP、Radio、BSS、Client 四张表。
26
+
27
+ 网络读取在事务外;写入时锁定设备及现有无线行,重新检查设备身份、lock_version、来源修订与四张表指纹。部分或失败维度保留旧记录;不会通过删除父记录级联抹去未完整采集的子维度。AP 人工管理字段保留。完整领域观察、对账与评估从 Result 返回,设置中只保留有界的水位与状态元数据。
28
+
29
+ CLI 是显式新来源。现有 Netdisco 同步、job 和 controller 默认调用链没有被切换。
30
+
31
+ ## 配置生成
32
+
33
+ `Motor::Netconf::NetstackGenerator.call(scenario:, vendor:, input:)` 从宿主读取当前选中模板、系统配置与已有 Radware 默认值,交给 Netstack 的校验、载荷与生成器处理。
34
+
35
+ 返回单个 Artifact 或 bootstrap 批次的 Artifact 数组。宿主继续决定加密存储、ZIP、下载、邮件与保留期;没有在库中再次实现 Redis ArtifactStore。原 Generator 的默认入口保持原契约,可逐场景替换调用。
36
+
37
+ ## 配置交付
38
+
39
+ `Motor::Api::NetstackDeliveryClient.new(config:, target:, platform:, http: nil)` 可以显式注入现有 `Motor::Netconf::Executor`:
40
+
41
+ ```ruby
42
+ client = Motor::Api::NetstackDeliveryClient.new(
43
+ config: delivery_settings, target: persisted_target, platform: "huawei"
44
+ )
45
+ Motor::Netconf::Executor.new(persisted_target.id, client: client).call
46
+ ```
47
+
48
+ 该入口沿用持久化目标、稳定 idempotency_key、认领次数、加密脚本和原有结果持久化。第一次提交核对设备身份及实际脚本摘要;同步成功还需按任务 ID/原 key 回读。已有认领但未保存任务 ID 时只按原 key 查询,不重新 POST。回读失败不能被误认为首次提交被明确拒绝。
49
+
50
+ HTTP 服务需要提供按原 key 关联的结果查询;没有这项协议能力时结果保持未知,不能通过重试 POST 来制造确定性。HTTP 服务报告成功与独立设备运行状态验证仍是不同层次,详见 configuration.md。
51
+
52
+ ## 外部配置备份
53
+
54
+ `Motor::Devices::NetstackRepository` 显式接收 `Netstack::Backups::Repository`。Motor 继续读取其集成开关并提供已有状态、原文、版本与 diff 响应结构。原 OxidizedConfiguration 默认 client 保持原样。
55
+
56
+ 备份请求要求 `execution_id` 与同步持久 checkpoint;恢复还要核对设备与节点身份。普通进度日志不能作为持久确认,也不能把队列接受视为备份完成。
57
+
58
+ ## 监控与 DDI 持久执行
59
+
60
+ Motor 新增 `NetworkExecution` 及 `20260930000000_create_network_executions.rb`。接入前须在宿主数据库运行该迁移;本次只迁移了独立测试库。表保存 execution_id、领域计划及摘要、目标/集成配置快照、按阶段分开的检查点与结果。凭据继续从现有加密配置和设备凭据读取,不进入计划或事件。
61
+
62
+ ```ruby
63
+ monitoring = Motor::Devices::NetstackMonitoring.new
64
+ execution = monitoring.plan(device: device)
65
+ # 宿主完成授权、计划审阅与任务调度后执行。
66
+ monitoring.call(execution_id: execution.execution_id)
67
+ # 进程重启后只需读取持久身份。
68
+ Motor::Devices::NetstackMonitoring.new.recover(execution_id: execution.execution_id)
69
+
70
+ ddi = Motor::Bluecat::NetstackExecution.new
71
+ execution = ddi.plan(
72
+ operation: "dns_record",
73
+ attributes: { name: "edge.example.test", type: "A", value: "192.0.2.10" },
74
+ settings: ddi_settings
75
+ )
76
+ ddi.call(execution_id: execution.execution_id)
77
+ Motor::Bluecat::NetstackExecution.new.recover(execution_id: execution.execution_id)
78
+ ```
79
+
80
+ 监控入口读取已有 `MonitoringProposal` 和 Zabbix 意图,从服务端精确解析组、模板与代理组,再保存 `DevicePlan`。执行和成功回写均核对设备身份、版本、sync_revision 和集成配置版本;成功后更新已有 Zabbix host_id、managed IDs 与同步状态。
81
+
82
+ DDI 入口只接受明确列出的业务操作与参数,要求显式 `Ddi::Settings`。恢复从已保存的 scope 重建设置,核对原配置记录、endpoint 和版本,分别继续对象变更与服务器部署。`pending` 表示仍需宿主安排查询;`unknown` 保留原占用与证据,不能创建替代执行来重放。
83
+
84
+ 共享 `NetworkExecutions::Runner` 复用 PostgreSQL session advisory lock,网络 I/O 期间不持有数据库事务。每次检查点在独立的最外层短事务提交后才返回 `true`;所有入口拒绝调用者已有的外层事务。活跃状态的部分唯一索引限制同一目标只有一个计划/执行,进程退出后 session 锁释放,数据库意图仍保留。
85
+
86
+ BlueCat 占用键使用规范化 endpoint 的 SHA-256,因此两个配置记录指向相同 endpoint 也不能并发执行;证据同时保留原配置记录与 scope。不同 DNS 别名是否对应同一物理 BAM,仍需宿主统一配置。session 锁要求直连 PostgreSQL 或保持 session 的连接代理,不能使用 transaction pooling。
87
+
88
+ 该锁协调这些新入口。迁移某个目标时,宿主须停用对应旧写者,不能让旧 Zabbix job、旧 BlueCat 路径与新执行同时修改它。外部管理员的操作也不受此锁约束。授权、审批、定时重试和人工处置仍属于 Motor 的工作流。
89
+
90
+ 执行记录从 RailsAdmin 通用 CRUD 排除,避免直接编辑状态或删除未决意图。历史记录的外键会保护设备和集成配置;保留期与显式归档由宿主统一设计,不能级联删除未知执行的证据。
91
+
92
+ ## 其他宿主边界
93
+
94
+ 链路监控、探针、容量评估和 VXLAN 仍通过 Netstack 领域 API 接入宿主工作流;上述持久适配覆盖设备 Zabbix 与 DDI。Motor 的既有 controller、job 和生产默认依赖源没有被自动切换。VXLAN 计划由宿主保存,再通过 Deployment::Executor 执行;不能将普通进度回调当作 durable checkpoint。
95
+
96
+ 本次接入测试证明上述显式入口可与当前 Motor/Rails/PostgreSQL 组合;它不等于默认生产路径已切换,也不等于真实设备、HTTP 交付服务、Zabbix、BlueCat 或 Oxidized 已完成联调。
@@ -0,0 +1,39 @@
1
+ # Offline performance checks — 2026-10-01
2
+
3
+ These measurements compare the preserved pre-review source snapshot with the
4
+ reviewed working tree, on macOS arm64 / Ruby 4.0.7 with the same bundle. Each result
5
+ is the median of three sequential runs; fixture construction and explicit GC run
6
+ outside the measured interval. Timing uses the monotonic clock; allocation counts
7
+ use `GC.stat(:total_allocated_objects)`.
8
+
9
+ | Workload | Before | After | Allocated objects before / after |
10
+ | --- | --- | --- | --- |
11
+ | Inventory: 10,000 unchanged interfaces | 0.2948 s | 0.0190 s | 120,028 / 120,028 |
12
+ | Inventory: 20,000 unchanged interfaces | 0.8674 s | 0.0383 s | 240,028 / 240,028 |
13
+ | Deployment: 50 fresh steps | 0.079662 s | 0.019481 s | 1,062,924 / 139,483 |
14
+ | Deployment: 200 fresh steps | 1.219080 s | 0.288421 s | 16,446,399 / 2,042,608 |
15
+ | Deployment: 200 already verified steps, recovery | 0.133616 s | 0.001273 s | 2,084,005 / 16,115 |
16
+
17
+ Inventory now indexes observed and retained identities instead of repeatedly
18
+ scanning arrays. Source, scope, manual fields and output ordering retain their
19
+ existing contracts. The fixture asserts that unchanged records produce no changes.
20
+
21
+ Executor computes immutable execution identity once per call and indexes a validated
22
+ receipt prefix directly. The adapter and checkpoint are in-memory fixtures; all
23
+ before/after receipt JSON digests match. Complete receipt copying at each checkpoint
24
+ still has cumulative quadratic cost for large plans. These measurements describe
25
+ library overhead, not device, network, provider or durable database latency.
26
+
27
+ Run from the project root under one Ruby version and bundle, sequentially:
28
+
29
+ ```sh
30
+ bundle exec ruby script/benchmark_inventory.rb /path/to/baseline/lib
31
+ bundle exec ruby script/benchmark_inventory.rb
32
+ bundle exec ruby script/benchmark_deployment.rb /path/to/baseline/lib
33
+ bundle exec ruby script/benchmark_deployment.rb
34
+ ```
35
+
36
+ The scripts are optional developer checks, not timing thresholds in CI. They do
37
+ not contact external systems. The repository had no HEAD at review time; the
38
+ baseline was an immutable source copy with a SHA-256 manifest, rather than an
39
+ invented commit reference.
data/docs/release.md ADDED
@@ -0,0 +1,20 @@
1
+ # 发布
2
+
3
+ 0.2.0 采用新的独立 Netstack API;旧 0.1.x 调用代码需要迁移。旧 GitHub
4
+ 源码保留在历史提交中,不作为当前 gem 的资源或运行时依赖。
5
+
6
+ 发布前执行 `bundle exec rake ci`,检查源码、历史和最终 gem 的敏感信息,
7
+ 并确认版本尚未发布。只提交项目源码、测试、文档、模板与开发配置;IDE、
8
+ 缓存、临时文件和构建目录不提交。
9
+
10
+ 推送后等待 `.github/workflows/ci.yml` 的四项 Ruby/platform 验证及 package
11
+ 任务全部成功。通过 `gh run download RUN_ID --name netstack-gem --dir DEST`
12
+ 取得已验证的 gem 和 SHA256SUMS,核对摘要后将同一个 gem 上传到 GitHub
13
+ Release 和 RubyGems。发布期间不重新构建或替换工件。
14
+
15
+ RubyGems MFA 使用当前操作的交互验证;凭据、验证码不写入文件或提交。
16
+ 上传结果未知时先查询远端状态,不能为了通过验证重复上传。
17
+
18
+ 发布后读取标签目标、CI 结论、Release 资产和 RubyGems 版本元数据,
19
+ 下载两端 gem 并比较 SHA-256,确认 RubyGems 未撤回,再检查最终工作区状态。
20
+ 离线门禁、宿主集成和真实设备验证分别报告。
data/docs/vxlan.md ADDED
@@ -0,0 +1,91 @@
1
+ # VXLAN fabric operations
2
+
3
+ `Netstack::Vxlan` is a Ruby domain library for Cisco NX-OS fabric evidence, auditing, and additive segment changes. It does not use PDK APIs, Perl, Netdisco, a database, or a task queue. The host owns target inventory, authorization, credentials, durable checkpoints, and the target execution lease.
4
+
5
+ ## Configuration and runtime evidence
6
+
7
+ ```ruby
8
+ target = Netstack::Target.new(key: "dc1-leaf1", host: "192.0.2.1", platform: :cisco_nxos,
9
+ attributes: { role: :leaf })
10
+ snapshot = Netstack::Vxlan::Collector.new.collect(
11
+ targets: [target], fabric_key: "dc1",
12
+ connection_options: { username: "operator", password: secret },
13
+ dimensions: %i[configuration nve_peers nve_vnis bgp_neighbors vpc]
14
+ )
15
+ findings = Netstack::Vxlan::Auditor.new.inspect(snapshot)
16
+ ```
17
+
18
+ Collection uses one owned connector session per target and continues other targets after a device failure. A target's host always wins over a `host` in connection options. Configuration and each runtime dimension have separate outcomes. Unsupported platforms are reported without a connection attempt. Credentials and exception messages are not included in snapshots.
19
+
20
+ The collected `Fabric#expected_node_keys` retains every requested target, including targets with failed or unsupported configuration collection. Configuration is complete only when all expected nodes are present and complete. Selecting a successful subset for a segment change does not bypass missing fabric evidence. The auditor reports each missing configuration target. Runtime failures remain separate and do not invalidate an otherwise complete configuration baseline.
21
+
22
+ Configuration parsing covers VLAN/VNI, VRF and route targets, SVI addresses and ACL bindings, NVE membership/replication, EVPN, BGP peer templates/neighbors/VRF redistribution, vPC, ordered ACL entries, prefix lists, route maps, and static routes. Models are immutable. Source configuration is not retained in a `Node`; its fingerprint excludes volatile `!` show headers while retaining configuration commands, including commands not interpreted by the domain parser.
23
+
24
+ For offline parsing, transport completeness is an explicit assertion by the caller:
25
+
26
+ ```ruby
27
+ node = Netstack::Vxlan::CiscoNxos::ConfigParser.new.parse(text, target: target, complete: true)
28
+ fabric = Netstack::Vxlan::Fabric.new(key: "dc1", nodes: [node])
29
+ snapshot = Netstack::Vxlan::Snapshot.new(fabric: fabric)
30
+ ```
31
+
32
+ The default is `complete: false`; a parser cannot prove that an arbitrary file is whole. Invalid/unsupported SVI addressing, ambiguous multiple NVE interfaces, and unsupported NVE member ranges mark configuration evidence incomplete. This parser models the listed NX-OS constructs, not every possible command in every firmware release. Unmodeled general configuration remains covered by the baseline fingerprint.
33
+
34
+ Offline `Fabric.new` accepts `expected_node_keys:`; when omitted, its scope is the supplied node keys. Expected keys must be unique and include every supplied node. Fabric serialization and its digest include this expected coverage, so this baseline digest differs from versions that fingerprinted only successful nodes. Rebuild old fabric plans before execution.
35
+
36
+ Runtime dimensions are `nve_peers`, `nve_vnis`, `bgp_neighbors`, `ospf_neighbors`, `vpc`, `interfaces`, `port_channels`, and `mac_addresses`. `RuntimeResult#status` distinguishes `complete`, `partial`, `unsupported`, `failed`, and `not_requested`. A complete empty table is different from an unrecognized output or failed command. Partial results retain recognized rows and diagnostics. Do not remove inventory from incomplete dimensions. Runtime-only collections do not establish a complete configuration baseline for planning.
37
+
38
+ Runtime parsing accepts explicit known headers, separators, and protocol framing. Every other body line must parse or makes the result partial; diagnostics contain counts, not the rejected text. A table header alone cannot prove an empty table. A complete empty result requires a recognized zero-entry response; unknown firmware framing remains partial until its grammar is supported.
39
+
40
+ The auditor reports cross-node segment/VNI/anycast-MAC/ACL differences, missing NVE/EVPN/VRF associations, route-target completeness, route advertisement, BGP extended communities and reflector settings, vPC peers/member differences, and abnormal runtime rows. Missing runtime objects are inferred only from complete configuration and complete relevant runtime evidence. A finding is evidence for review, not an automatic repair authorization.
41
+
42
+ ## Additive segment planning
43
+
44
+ Configuration readback requires the global anycast gateway MAC when the segment
45
+ uses anycast, and requires every bound ACL definition to exist. An SVI reference
46
+ alone does not prove those prerequisites; missing prerequisites remain unknown.
47
+
48
+ ```ruby
49
+ segment = Netstack::Vxlan::Segment.new(
50
+ vlan_id: 120, vni: 10120, vrf: "production",
51
+ prefix: "10.120.0.0/24", gateway: "10.120.0.1"
52
+ )
53
+ change = Netstack::Vxlan::SegmentPlanner.new.plan(
54
+ fabric: snapshot.fabric, segment: segment,
55
+ target_keys: ["dc1-leaf1"], prefix_list: "EXPORT"
56
+ )
57
+ change.plans.each { |plan| review(plan.payload, plan.digest) }
58
+ ```
59
+
60
+ VLAN, VNI, VRF, network, and gateway are explicit inputs; no address, naming, or organization heuristic supplies them. The planner requires complete fabric configuration, valid target management IPs, an existing VRF/L3 VNI and NVE association, an NVE source/BGP host-reachability configuration, and the anycast MAC when needed. It rejects VLAN/VNI/prefix/address/policy conflicts before returning any plans. Conflicts are checked across the supplied fabric, including unselected nodes.
61
+
62
+ The returned `SegmentPlan` reports `new`, `partial`, or `existing`, target counts, the fabric baseline digest, and per-device `Deployment::Plan` objects. Existing complete segments yield no executable plans. Partial configurations produce only missing commands; an existing SVI's VRF/address is not reapplied merely because another component is missing. Desired ACLs must already exist; planning does not create access policy.
63
+
64
+ New EVPN objects use `rd auto` and automatic import/export route targets. Existing complete custom EVPN policies are preserved; incomplete custom policies are rejected instead of guessed. NVE ingress replication is explicitly `:bgp` or `:multicast` with a valid multicast group.
65
+
66
+ `prefix_list:` opts into route advertisement changes. The selected prefix list must already be the sole predicate of a single permit route-map referenced by the VRF's connected/direct redistribution, with EVPN advertisement enabled. Policies with denies, extra predicates, or more complex ordering are rejected for mutation. The read-only `RoutePolicy` honors prefix-list order and prefix length ranges; unsupported predicates produce `:unknown`. Without `prefix_list:`, the segment plan does not claim to change route advertisement.
67
+
68
+ ## Execution and verification
69
+
70
+ ```ruby
71
+ adapter = Netstack::Vxlan::CiscoNxos::DeploymentAdapter.new
72
+ executor = Netstack::Deployment::Executor.new(adapter: adapter)
73
+ receipt = executor.apply(plan: change.plans.first, execution_id: execution_id,
74
+ credentials: credentials, checkpoint: durable_checkpoint,
75
+ previous: previous_receipt)
76
+ ```
77
+
78
+ The host acquires exclusive target ownership, stores the approved plan and execution identity, and provides a synchronous checkpoint callback returning `true` only after durable commit. The adapter performs a fresh configuration read before execution. Identity changes and baseline drift block a new write. On recovery, the existing receipt is reconciled by readback; an uncertain configuration command is never automatically replayed.
79
+
80
+ Each device's segment change is one business step. Command timeout, disconnect, or incomplete execution means `unknown`, because preceding CLI commands may already have changed the device. Readback verifies VLAN/VNI, SVI/gateway/VRF/ACL, NVE replication, EVPN route targets, and opted-in advertisement before returning `verified`. Missing or partial readback remains `unknown`; it does not imply that nothing happened.
81
+
82
+ Generated changes modify running configuration only. They do not save startup configuration, delete objects, roll back, or rebuild the fabric underlay/control plane. Saving or rollback needs a separate reviewed host operation and its own verification. Configuration verification proves desired configuration, not convergence of BGP/NVE forwarding; collect runtime dimensions after deployment when operational readiness is required.
83
+
84
+ ## Offline acceptance
85
+
86
+ ```sh
87
+ bundle exec ruby -Ilib:test -e 'Dir["test/vxlan/*_test.rb"].sort.each { |file| require_relative file }'
88
+ bundle exec rubocop lib/netstack/vxlan test/vxlan
89
+ ```
90
+
91
+ Tests cover configuration and runtime fixtures, incomplete/unsupported/empty evidence, immutable values, conflict and prerequisite rejection, minimal partial plans, routing order, multi-device audit, collection failures, stale preflight, unknown writes, durable checkpoint ordering, and recovery by readback. These are offline parser and injected-transport checks; they are not real NX-OS firmware or device acceptance.
data/docs/wireless.md ADDED
@@ -0,0 +1,81 @@
1
+ # Wireless 领域
2
+
3
+ `Netstack::Wireless` 是单控制器的只读业务领域,直接组合内置 Connector。它不依赖 Netdisco、Perl、SQL、Active Record,也不创建任务队列或执行记录库。宿主负责凭据、授权、调度、重试、事务、人工字段以及证据文件。
4
+
5
+ ## 采集与结果
6
+
7
+ ```ruby
8
+ require "netstack"
9
+
10
+ target = Netstack::Target.new(key: "controller-1", host: "192.0.2.1", platform: "h3c")
11
+ collector = Netstack::Wireless::Collector.new(
12
+ connector_factory: ->(target) {
13
+ Netstack::Connector.build(:h3c_wireless, host: target.host,
14
+ username: ENV.fetch("WLC_USERNAME"), password: ENV.fetch("WLC_PASSWORD"))
15
+ }
16
+ )
17
+ observation = collector.call(target: target)
18
+ assessment = Netstack::Wireless::Assessor.new.call(observation)
19
+ result = Netstack::Wireless::Reconciler.new.call(previous: nil, observed: observation)
20
+ # 宿主持久化 result.state 和 result.observation,并在自己的事务中应用 result.changes。
21
+ ```
22
+
23
+ 也可传入已打开的 `connector:`;此时连接/关闭属于调用者。工厂创建的连接始终由 Collector 关闭。采集按顺序执行固定 `display` 命令,共用 AP/radio 及配置查询结果,默认 H3c 共 7 条业务查询。所有命令 `privilege: false`,使用完整已确认提示符,并标记输出敏感。不会运行 system-view、probe、save、开启 AP console 或使用默认口令。
24
+
25
+ | 平台 | 实际支持维度 | 查询 |
26
+ | --- | --- | --- |
27
+ | H3c / h3c_wireless | access_points、radios | display wlan ap all verbose |
28
+ | H3c | clients | display wlan client verbose |
29
+ | H3c | bsses | display wlan bss all verbose |
30
+ | H3c | dot1x_connections、mab_connections | display dot1x connection / display mac-authentication connection |
31
+ | H3c | blocked_clients | display wlan client-security block-mac |
32
+ | H3c | configuration、ap_groups、service_templates | display current-configuration |
33
+ | Huawei | access_points、clients | display ap all / display station all |
34
+ | H3c AP | lldp(显式请求) | display lldp neighbor-information verbose |
35
+
36
+ Huawei 的 radio/BSS/配置/认证/LLDP 等返回 `unsupported`。其他平台不会连接。默认不采集 LLDP。显式 `dimensions: [:lldp]` 自动包含 AP 清单查询,并要求 `ap_connector_factory: ->(target, access_point) { ... }` 提供每台 AP 的直接连接;工厂自持凭据,不从控制器开启任何功能。只有全部 AP 覆盖且 AP 清单完整时,LLDP 才完整。
37
+
38
+ 每个 `Netstack::Dimension` 包含 `name/status/records/started_at/completed_at/evidence/diagnostics`。状态使用 `complete/partial/unsupported/failed/not_requested`。失败连接和失败命令不会转成成功空清单。完整零条要求明确总数/零条格式;有记录的正常完整命令可提供结束证据。未知行、非法字段、总数不符、截断与不可解析输出都保留可见诊断。
39
+
40
+ 黑名单只有表头或分隔线时不能认定零条;LLDP 已出现邻居索引但缺少邻居内容时保留 `incomplete_neighbor` 诊断。即使命令已结束,这些结果也不能授权删除旧记录。
41
+
42
+ `evidence` 默认只包含命令、原始输出 SHA-256、字节数。可传 `evidence_writer: ->(target:, command:, output:) { reference }` 接受原始字节并返回 JSON 值引用。此显式回调可能收到配置密钥,宿主须按自身加密存储策略处理;原文不会进入 snapshot、异常消息或解析诊断。解析只在副本上移除命令回显、提示符、终端表现字符。未知配置行只记录行号,PSK 仅保留 `has_preshared_key`。
43
+
44
+ ## 模型与关系
45
+
46
+ 具体值对象包括 AccessPoint、Radio、Client、Bss、ApGroup、ServiceTemplate、AuthenticationConnection、Neighbor、Configuration、SecurityPolicy、Finding;字符串、数组、嵌套哈希均防御复制且冻结。H3c 提取 AP 在线状态/时长、CAPWAP、客户端数,radio 信道/利用率/底噪;client 地址、AP/radio/BSSID/SSID/VLAN、相对 RSSI 或 dBm、协商与吞吐 bps、终端能力、安全参数;BSS 与 dot1x/MAB 的域、协议、授权 VLAN、在线时间。
47
+
48
+ 配置覆盖 AP/组/型号模板/radio/service-template,保持显式 false 与缺项 nil 的差异:自动注册/持久化、客户端保活/限速、band navigation、channel/功率/开关、mandatory/supported/disabled rates、anti-sticky、RSSI 门槛、模板绑定、BTM、PSK/dot1x/MAB/portal、黑白名单。未知无线语法使配置 partial,不宣称已经覆盖所有固件语法。
49
+
50
+ `observation.view` 派生 AP、radio、BSS、客户端关系。组型号 radio 配置先于 AP 显式配置;runtime 状态保留独立字段。组通过 AP 名称或 serial 关联;同一 SSID 对应多个模板时返回 `ambiguous_service_template`,不按遍历顺序挑选。认证连接优先提供 client 的认证类型,其次配置,再其次运行时 AKM。`clients_for`、`radios_for`、`bsses_for`、`neighbors_for`、`authentications_for` 及黑白名单查询支持宿主视图。
51
+
52
+ 记录键采用长度前缀组合,避免名称含分隔符时碰撞,并始终作用于单一 controller target。Snapshot 验证重复 AP 名称/ID/MAC/序列号、重复客户端/BSSID、AP 名称和 ID 冲突、BSS/radio 引用冲突。完整父维度缺少被引用父对象时拒绝快照;父维度不完整时允许未知关系,但不把它当删除依据。顺序查询可能遇到真实漫游竞态,矛盾快照拒绝导入后由宿主重新采集。
53
+
54
+ ## 对账与宿主持久化
55
+
56
+ `Reconciler#call(previous: State或Snapshot或nil, observed: Snapshot)` 先验证 target key/host/platform、各维度时间和身份,再返回不可变 `Reconciliation(state:, changes:, observation:)`。相同时间的冲突内容及倒序观测会被拒绝;完全相同的重放不产生更新。
57
+
58
+ | 观测状态 | State 更新 | 删除 |
59
+ | --- | --- | --- |
60
+ | complete | 按该维度替换 | 只删除该维度中已证实不存在的键 |
61
+ | partial | 有效记录合并;缺项 nil 不覆盖旧值,显式 false 可更新 | 无 |
62
+ | failed / unsupported / not_requested | 保留旧记录 | 无 |
63
+
64
+ 部分配置中的黑白名单按 MAC 身份合并;新观察的非 nil 字段覆盖旧字段,
65
+ 未观察的 MAC 和未观察字段保留,避免旧记录吞掉已观察的更新。
66
+
67
+ 部分配置还按 AP/组/模板及嵌套绑定合并,避免顶层 singleton 更新间接抹去旧子对象。`State.records` 是保留后的事实,`State.watermarks` 是各维度观测水位;`observation` 是本次状态。故失败维度没有记录与保留旧事实并不冲突。宿主须保留两者区别,不可把 State 当作最新的完整观测。`changes` 包含每维 added/updated/removed/retained/status,引用字段属于运行数据,宿主映射到自己的模型时仍须保护人工字段和来源边界。
68
+
69
+ 同一 radio 的 service-template 绑定以不区分大小写的模板名称为身份,保留展示拼写。partial 更新已观察绑定的非 nil 字段并保留未观察绑定;AP 显式绑定优先于组内同名绑定。同一输入作用域重复绑定同名模板会被拒绝,配置解析返回 failed,直接构建 Snapshot 抛出 `InvalidSnapshot`;不会任取其中一条。该规则只适用于模板绑定,不改变速率等其他数组的含义。名称身份和 AP 覆盖组设置依据 [H3C service-template 命令](https://www.h3c.com/en/d_202208/1675573_294551_0.htm)。
70
+
71
+ Reconciler 不执行数据库写入,不保证两个宿主并发事务的互斥。宿主须在持锁/事务内重读旧 State、检查来源修订号并提交;采集网络 IO 应在持锁前完成。
72
+
73
+ ## 评估
74
+
75
+ Assessor 生成具体 Finding。AP 非在线、预期 AP 缺失、近期上线、radio down/忙信道/高底噪、客户端弱信号/近期上线/明确漫游异常、关联歧义与维度不完整分别有独立代码。AP 清单不完整时预期缺失仅为 `expected_ap_unconfirmed`。普通 `Intra-AC roam` 不是故障;近期上线不被描述为反复掉线。H3c 正 RSSI 相对量与 Huawei 负 dBm 使用独立阈值;0 RSSI/非负底噪是未知遥测。健康判定要求实际完整证据。
76
+
77
+ ## 来源与验证
78
+
79
+ 业务语法和关系来自本地 PDK:`Inspection/Runtime/H3c/WirelessAp/Parser.pm`、`Inspection/Runtime/Huawei/WirelessAp.pm`、`Inspection/Runtime/WirelessController/{Profile,Parser,ApLldpDiscovery}.pm`、`Wireless/H3c/ConfigParser.pm` 及其 AccessPoint/ApGroup/Radio/ServiceTemplate/Global/LineOption、`Wireless/H3c/DomainBuilder.pm`、无线各模型、`Wireless/Util.pm` 与 Controller/IssueEvaluator。这里按 Ruby 值对象/组合职责重新实现,未保留 Perl/Netdisco 入口。PDK LLDP 的隐式控制台启用步骤明确未迁入。
80
+
81
+ `test/fixtures/wireless` 全部是合成实验数据:192.0.2.0/24、2001:db8::/32、02 开头 MAC、LAB 序列号和测试密钥。未搬现场采集文件。Minitest 覆盖解析、配置覆盖、认证关系、身份冲突、完整空结果、异常/partial 不删除、嵌套配置保留、时间水位、证据回调、只读命令、连接清理、LLDP 覆盖与诊断、RSSI尺度。离线样例及 mock Connector 的通过不等同于真实设备/所有固件验收。
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+
5
+ module Netstack
6
+ module Backups
7
+ # Access to raw secrets is deliberate; logging this object never prints bytes.
8
+ class Artifact
9
+ attr_reader :content, :sha256, :bytesize
10
+
11
+ def initialize(content)
12
+ raise InputError, "Backup content must be text" unless content.is_a?(String)
13
+ @content = content.dup.freeze
14
+ @sha256 = Digest::SHA256.hexdigest(content).freeze
15
+ @bytesize = content.bytesize
16
+ freeze
17
+ end
18
+
19
+ def inspect = "#<#{self.class.name} bytesize=#{bytesize} sha256=#{sha256}>"
20
+ alias to_s inspect
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Netstack
4
+ module Backups
5
+ class Diff < Artifact
6
+ attr_reader :lines
7
+
8
+ def initialize(lines)
9
+ raise InputError, "Backup diff must contain text lines" unless lines.is_a?(Array) && lines.all? { |line| line.is_a?(String) }
10
+ @lines = Values.copy(lines)
11
+ super(lines.join)
12
+ end
13
+ end
14
+ end
15
+ end