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,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "json"
5
+
6
+ module Netstack
7
+ module Deployment
8
+ # 可持久化的单目标有序意图;规范序列化产生稳定摘要供恢复核验。
9
+ # 配置正文应放在私有工件中,参数与元数据只保留引用和安全摘要。
10
+ class Plan < Data.define(:target, :steps, :preconditions, :metadata)
11
+ SCHEMA_VERSION = 1
12
+
13
+ def initialize(target:, steps:, preconditions: {}, metadata: {})
14
+ raise InputError, "plan target must be a Target" unless target.is_a?(Target)
15
+ unless steps.is_a?(Array) && steps.all? { |step| step.is_a?(Step) } && steps.map(&:id).uniq.size == steps.size
16
+ raise InputError, "plan steps must have unique identities"
17
+ end
18
+ raise InputError, "plan metadata must be hashes" unless preconditions.is_a?(Hash) && metadata.is_a?(Hash)
19
+
20
+ super(target: target, steps: steps.dup.freeze, preconditions: Values.copy(preconditions), metadata: Values.copy(metadata))
21
+ # 适配器执行预检读取或写入之前,先验证序列化契约。
22
+ payload
23
+ end
24
+
25
+ def payload
26
+ canonical({ schema_version: SCHEMA_VERSION, target: target.to_h, steps: steps.map(&:to_h), preconditions: preconditions, metadata: metadata })
27
+ end
28
+
29
+ def digest = Digest::SHA256.hexdigest(JSON.generate(payload))
30
+
31
+ def self.from_h(value)
32
+ raise InputError, "plan must be an object" unless value.is_a?(Hash)
33
+
34
+ validate_serialized_keys!(value)
35
+ data = JSON.parse(JSON.generate(value), symbolize_names: true)
36
+ raise InputError, "unsupported plan schema" unless data.fetch(:schema_version) == SCHEMA_VERSION
37
+
38
+ new(target: Target.new(**data.fetch(:target)), steps: data.fetch(:steps).map { |step| Step.new(**step) },
39
+ preconditions: data.fetch(:preconditions), metadata: data.fetch(:metadata))
40
+ rescue KeyError, TypeError, NoMethodError, JSON::GeneratorError, JSON::ParserError, ArgumentError
41
+ raise InputError, "invalid plan payload"
42
+ end
43
+
44
+ def self.validate_serialized_keys!(value)
45
+ case value
46
+ when Hash
47
+ raise InputError, "plan keys must be strings or symbols" unless value.keys.all? { |key| key.is_a?(String) || key.is_a?(Symbol) }
48
+
49
+ keys = value.keys.map(&:to_s)
50
+ raise InputError, "duplicate serialized plan keys" unless keys.uniq.size == keys.size
51
+
52
+ value.each_value { |item| validate_serialized_keys!(item) }
53
+ when Array then value.each { |item| validate_serialized_keys!(item) }
54
+ end
55
+ end
56
+ private_class_method :validate_serialized_keys!
57
+
58
+ private
59
+
60
+ def canonical(value)
61
+ case value
62
+ when Hash
63
+ raise InputError, "plan keys must be strings or symbols" unless value.keys.all? { |key| key.is_a?(String) || key.is_a?(Symbol) }
64
+
65
+ keys = value.keys.map(&:to_s)
66
+ raise InputError, "duplicate serialized plan keys" unless keys.uniq.size == keys.size
67
+
68
+ value.to_h { |key, item| [key.to_s, canonical(item)] }.sort.to_h
69
+ when Array then value.map { |item| canonical(item) }
70
+ when Time then value.getutc.iso8601(6)
71
+ when Symbol then value.to_s
72
+ when Float
73
+ raise InputError, "plan numbers must be finite" unless value.finite?
74
+
75
+ value
76
+ when String, Integer, TrueClass, FalseClass, NilClass then value
77
+ else raise InputError, "plan contains a non-serializable value"
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Netstack
6
+ module Deployment
7
+ # 不可变的执行前缀;证据只应包含安全的身份与摘要,
8
+ # 不能包含凭据、配置正文或传输原文。
9
+ class Receipt < Data.define(:execution_id, :plan_digest, :target_key, :steps, :planned_step_ids)
10
+ STATES = %i[started applied accepted verified failed unknown not_applied].freeze
11
+ SCHEMA_VERSION = 1
12
+ STEP_FIELDS = %i[step_id status idempotency_key remote_id error_code evidence].freeze
13
+
14
+ def initialize(execution_id:, plan_digest:, target_key:, planned_step_ids:, steps: [])
15
+ raise InputError, "receipt steps must be an Array" unless steps.is_a?(Array)
16
+ unless planned_step_ids.is_a?(Array) && planned_step_ids.all? { |id| id.is_a?(String) && !id.empty? } && planned_step_ids.uniq == planned_step_ids
17
+ raise InputError, "planned step identities must be unique strings"
18
+ end
19
+
20
+ records = steps.map do |step|
21
+ raise InputError, "receipt step must be a Hash" unless step.is_a?(Hash)
22
+
23
+ self.class.send(:validate_serialized_keys!, step)
24
+ item = step.transform_keys(&:to_sym)
25
+ raise InputError, "unknown receipt step fields" unless (item.keys - STEP_FIELDS).empty?
26
+ item[:step_id] = Values.text(item.fetch(:step_id), name: "receipt step id")
27
+ item[:idempotency_key] = Values.text(item[:idempotency_key], name: "idempotency key") if item.key?(:idempotency_key)
28
+ item[:remote_id] = Values.text(item[:remote_id], name: "remote id") if item.key?(:remote_id)
29
+ state = item.fetch(:status).to_sym
30
+ raise InputError, "invalid receipt status" unless STATES.include?(state)
31
+ item[:error_code] = item[:error_code].to_sym if item[:error_code]
32
+
33
+ Values.copy(item.merge(status: state))
34
+ end
35
+ raise InputError, "duplicate receipt steps" unless records.map { |step| step[:step_id] }.uniq.size == records.size
36
+ unless records.map { |step| step[:step_id] } == planned_step_ids.first(records.size)
37
+ raise InputError, "receipt steps must follow plan order"
38
+ end
39
+
40
+ super(execution_id: Values.text(execution_id, name: "execution id"), plan_digest: Values.text(plan_digest, name: "plan digest"),
41
+ target_key: Values.text(target_key, name: "target key"), steps: records.freeze, planned_step_ids: Values.copy(planned_step_ids))
42
+ rescue KeyError, NoMethodError
43
+ raise InputError, "invalid receipt step"
44
+ end
45
+
46
+ def verified? = !steps.empty? && steps.size == planned_step_ids.size && steps.all? { |step| step[:status] == :verified }
47
+ def no_changes? = planned_step_ids.empty?
48
+ def unknown? = steps.any? { |step| %i[started unknown applied accepted].include?(step[:status]) }
49
+ def failed? = steps.any? { |step| %i[failed not_applied].include?(step[:status]) }
50
+
51
+ def payload = { schema_version: SCHEMA_VERSION, **to_h }
52
+
53
+ def self.from_h(value)
54
+ raise InputError, "receipt must be an object" unless value.is_a?(Hash)
55
+
56
+ validate_serialized_keys!(value)
57
+ data = JSON.parse(JSON.generate(value), symbolize_names: true)
58
+ raise InputError, "unsupported receipt schema" unless data.delete(:schema_version) == SCHEMA_VERSION
59
+
60
+ new(**data)
61
+ rescue JSON::GeneratorError, JSON::ParserError, ArgumentError, TypeError
62
+ raise InputError, "invalid receipt payload"
63
+ end
64
+
65
+ def self.validate_serialized_keys!(value)
66
+ case value
67
+ when Hash
68
+ unless value.keys.all? { |key| key.is_a?(String) || key.is_a?(Symbol) } && value.keys.map(&:to_s).uniq.size == value.size
69
+ raise InputError, "ambiguous receipt keys"
70
+ end
71
+ value.each_value { |item| validate_serialized_keys!(item) }
72
+ when Array then value.each { |item| validate_serialized_keys!(item) }
73
+ end
74
+ end
75
+ private_class_method :validate_serialized_keys!
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Netstack
4
+ module Deployment
5
+ # 根据收据建议宿主的下一步动作;不调度,也不执行动作。
6
+ # 未知结果包括已经开始、应用或接受但尚未确认效果的步骤。
7
+ class Recovery
8
+ def decision(receipt:)
9
+ raise InputError, "receipt required" unless receipt.is_a?(Receipt)
10
+ return :no_changes if receipt.no_changes?
11
+ return :verify if receipt.unknown?
12
+ return :review if receipt.failed?
13
+ return :complete if receipt.verified?
14
+
15
+ :resume
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Netstack
4
+ module Deployment
5
+ # 步骤身份、动作参数和预期状态;具体动作的业务校验由领域适配器负责。
6
+ # 身份参与幂等键计算;此对象本身不代表已经提交或已经执行。
7
+ class Step < Data.define(:id, :action, :parameters, :expected)
8
+ def initialize(id:, action:, parameters: {}, expected: {})
9
+ raise InputError, "step parameters and expected state must be hashes" unless parameters.is_a?(Hash) && expected.is_a?(Hash)
10
+
11
+ super(id: Values.text(id, name: "step id"), action: Values.text(action, name: "step action"),
12
+ parameters: Values.copy(parameters), expected: Values.copy(expected))
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Netstack
4
+ # A collection result for one explicitly requested scope; empty data is not a success signal.
5
+ class Dimension < Data.define(:name, :status, :records, :started_at, :completed_at, :evidence, :diagnostics)
6
+ STATUSES = %i[complete partial unsupported failed not_requested].freeze
7
+
8
+ def initialize(name:, status:, records: [], started_at: nil, completed_at: nil, evidence: [], diagnostics: [])
9
+ name = Values.text(name.is_a?(Symbol) ? name.to_s : name, name: "dimension name").to_sym
10
+ status = status.to_sym if status.is_a?(String)
11
+ raise InputError, "invalid dimension status" unless STATUSES.include?(status)
12
+ raise InputError, "dimension records must be an Array" unless records.is_a?(Array)
13
+ if %i[unsupported failed not_requested].include?(status) && !records.empty?
14
+ raise InputError, "unavailable dimensions cannot contain records"
15
+ end
16
+ started_at = Values.time(started_at, name: "collection start", optional: true)
17
+ completed_at = Values.time(completed_at, name: "collection end", optional: true)
18
+ if (!started_at.nil? ^ !completed_at.nil?) || (started_at && completed_at < started_at)
19
+ raise InputError, "invalid collection time window"
20
+ end
21
+
22
+ super(name: name, status: status, records: Values.copy(records), started_at: started_at, completed_at: completed_at,
23
+ evidence: Values.copy(evidence), diagnostics: Values.copy(diagnostics))
24
+ end
25
+
26
+ def complete? = status == :complete
27
+ def partial? = status == :partial
28
+ def unavailable? = %i[unsupported failed not_requested].include?(status)
29
+ end
30
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Netstack
4
+ class Error < StandardError
5
+ attr_reader :code, :stage, :details
6
+
7
+ def initialize(message = "network operation failed", code: :operation_failed, stage: nil, details: {})
8
+ @code = code.to_sym
9
+ @stage = stage&.to_sym
10
+ @details = Values.copy(details)
11
+ super(message)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Netstack
4
+ class InputError < Error
5
+ def initialize(message = "invalid input", code: :invalid_input, stage: :validate, details: {})
6
+ super
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Netstack
4
+ module Integrations
5
+ module Bluecat
6
+ # BlueCat REST v1 协议边界;分页必须正常结束后才能向调用者
7
+ # 返回集合;耗尽页数预算不会将部分数据作为成功结果返回。
8
+ class Client
9
+ BASE_PATH = "Services/REST/v1"
10
+
11
+ def initialize(base_url: nil, username:, password:, http: nil, **http_options)
12
+ @username = Values.text(username, name: "BlueCat username")
13
+ raise InputError, "BlueCat password must be nonempty text" unless password.is_a?(String) && password.present?
14
+
15
+ # 密码是凭据字节,不能像普通名称一样去除前后空格。
16
+ @password = password.dup.freeze
17
+ @http = http || Http::Client.new(base_url: base_url, **http_options)
18
+ end
19
+
20
+ def inspect = "#<#{self.class.name}>"
21
+
22
+ def json(endpoint, **query)
23
+ request(:get, endpoint, query).json
24
+ end
25
+
26
+ def entity(endpoint, **query)
27
+ validate_entity(json(endpoint, **query), allow_missing: true)
28
+ end
29
+
30
+ def entities(parent_id:, type:, page_size: 100, max_pages: 100)
31
+ page_size = Values.integer(page_size, name: "page_size", min: 1, max: 1000)
32
+ max_pages = Values.integer(max_pages, name: "max_pages", min: 1)
33
+ all = []
34
+ max_pages.times do |page|
35
+ rows = json("getEntities", parentId: parent_id, type: type, start: page * page_size, count: page_size)
36
+ invalid! unless rows.is_a?(Array) && rows.size <= page_size
37
+ rows = rows.map { |row| validate_entity(row) }
38
+ invalid! unless (all + rows).map { |row| row["id"] }.uniq.size == all.size + rows.size
39
+ all.concat(rows)
40
+ return Values.copy(all) if rows.size < page_size
41
+ end
42
+ raise Netstack::Error.new("BlueCat entity list is incomplete", code: :incomplete_snapshot)
43
+ end
44
+
45
+ def write(method, endpoint, query:, returns_id: false)
46
+ response = request(method, endpoint, query)
47
+ if returns_id
48
+ id = response.json
49
+ invalid! unless id.is_a?(Integer) && id.positive?
50
+ id
51
+ else
52
+ invalid! unless ["", "null", "true"].include?(response.body.strip)
53
+ true
54
+ end
55
+ end
56
+
57
+ def validate_entity(value, allow_missing: false)
58
+ invalid! unless value.is_a?(Hash) && value["id"].is_a?(Integer) && value["id"] >= 0
59
+ return nil if allow_missing && value["id"].zero?
60
+ invalid! unless value["id"].positive? && value["name"].is_a?(String) && value["type"].is_a?(String) && value["properties"].is_a?(String)
61
+ Values.copy(value)
62
+ end
63
+
64
+ private
65
+
66
+ def request(method, endpoint, query)
67
+ raise InputError, "Invalid BlueCat endpoint" unless endpoint.is_a?(String) && endpoint.match?(/\A[a-zA-Z][a-zA-Z0-9]+\z/)
68
+ authenticate! unless @token
69
+ @http.request(method, "#{BASE_PATH}/#{endpoint}", query: query, headers: { "Authorization" => "BAMAuthToken: #{@token}" })
70
+ end
71
+
72
+ def authenticate!
73
+ response = @http.request(:get, "#{BASE_PATH}/login", query: { username: @username, password: @password })
74
+ @token = response.body[/BAMAuthToken:\s+([^\s"<>]+)/, 1]
75
+ raise Netstack::Error.new("BlueCat authentication failed", code: :authentication_failed) unless @token
76
+ end
77
+
78
+ def invalid!
79
+ raise Netstack::Error.new("Invalid BlueCat response", code: :invalid_response, stage: :response)
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+ require "json"
6
+ require "openssl"
7
+ require "timeout"
8
+
9
+ module Netstack
10
+ # 显式调用的外部协议客户端;构造和加载常量不会建立连接。
11
+ module Integrations
12
+ module Http
13
+ # One request, no redirects or implicit retries. Transport receives keyword
14
+ # arguments and returns Response, which also makes protocol tests offline.
15
+ class Client
16
+ METHODS = %w[get post put patch delete head].freeze
17
+ FORBIDDEN_HEADERS = %w[host content-length connection transfer-encoding proxy-authorization].freeze
18
+
19
+ def initialize(base_url:, headers: {}, open_timeout: 5, read_timeout: 30,
20
+ write_timeout: 30, request_timeout: 60, max_response_bytes: 2_097_152, verify_tls: true, transport: nil)
21
+ @base_uri = parse_uri(base_url)
22
+ @headers = validate_headers(headers)
23
+ @timeouts = { open_timeout: open_timeout, read_timeout: read_timeout, write_timeout: write_timeout }
24
+ invalid!("HTTP timeouts must be positive numbers") unless @timeouts.values.all? { |value| value.is_a?(Numeric) && value.finite? && value.positive? }
25
+ invalid!("HTTP request timeout must be positive") unless request_timeout.is_a?(Numeric) && request_timeout.finite? && request_timeout.positive?
26
+ @request_timeout = request_timeout
27
+ invalid!("Response byte limit must be a positive integer") unless max_response_bytes.is_a?(Integer) && max_response_bytes.positive?
28
+ invalid!("verify_tls must be boolean") unless [true, false].include?(verify_tls)
29
+ @max_response_bytes = max_response_bytes
30
+ @verify_tls = verify_tls
31
+ @transport = transport
32
+ end
33
+
34
+ def inspect = "#<#{self.class.name}>"
35
+
36
+ def request(method, path = "", query: {}, headers: {}, json: nil, body: nil)
37
+ method = method.to_s.downcase
38
+ invalid!("Unsupported HTTP method") unless METHODS.include?(method)
39
+ invalid!("Supply json or body, not both") if !json.nil? && !body.nil?
40
+ uri = request_uri(path, query)
41
+ request_headers = @headers.merge(validate_headers(headers))
42
+ unless json.nil?
43
+ request_headers["Content-Type"] ||= "application/json"
44
+ body = JSON.generate(json, allow_nan: false)
45
+ end
46
+ invalid!("HTTP body must be text") unless body.nil? || body.is_a?(String)
47
+ options = { method: method, url: uri.to_s, headers: request_headers, body: body,
48
+ **@timeouts, max_response_bytes: @max_response_bytes, verify_tls: @verify_tls }
49
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + @request_timeout
50
+ response = Timeout.timeout(@request_timeout) do
51
+ @transport ? @transport.call(**options) : transmit(**options, deadline: deadline)
52
+ end
53
+ raise Timeout::Error if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
54
+ raise Error.new("Invalid HTTP transport response", code: "invalid_response", stage: "response") unless response.is_a?(Response)
55
+
56
+ check_size!(response.body.bytesize)
57
+ unless (200..299).cover?(response.status)
58
+ raise Error.new("HTTP request was not successful", code: "http_status", stage: "response",
59
+ details: { "status" => response.status })
60
+ end
61
+ response
62
+ rescue Timeout::Error
63
+ raise Error.new("HTTP request timed out", code: "http_timeout", stage: "transport"), cause: nil
64
+ rescue SocketError, IOError, SystemCallError, OpenSSL::SSL::SSLError, Net::HTTPBadResponse, Net::ProtocolError
65
+ raise Error.new("HTTP connection failed", code: "http_connection_failed", stage: "transport"), cause: nil
66
+ rescue JSON::GeneratorError
67
+ invalid!("Request is not valid JSON")
68
+ end
69
+
70
+ private
71
+
72
+ def transmit(method:, url:, headers:, body:, open_timeout:, read_timeout:, write_timeout:, max_response_bytes:, verify_tls:, deadline:)
73
+ uri = URI.parse(url)
74
+ http = Net::HTTP.new(uri.host, uri.port, nil)
75
+ http.use_ssl = uri.scheme == "https"
76
+ http.verify_mode = verify_tls ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
77
+ http.open_timeout = open_timeout
78
+ http.read_timeout = read_timeout
79
+ http.write_timeout = write_timeout
80
+ http.max_retries = 0
81
+ request = Net::HTTPGenericRequest.new(method.upcase, !body.nil?, method != "head", uri.request_uri, headers)
82
+ request.body = body if body
83
+ http.start do
84
+ http.request(request) do |response|
85
+ bytes = +""
86
+ response.read_body do |chunk|
87
+ raise Timeout::Error if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
88
+ check_size!(bytes.bytesize + chunk.bytesize, max_response_bytes)
89
+ bytes << chunk
90
+ end
91
+ return Response.new(status: response.code.to_i, headers: response.to_hash.transform_values { |values| values.join(", ") }, body: bytes)
92
+ end
93
+ end
94
+ end
95
+
96
+ def parse_uri(value)
97
+ invalid!("HTTP base URL must be text") unless value.is_a?(String)
98
+ uri = URI.parse(value)
99
+ unless %w[http https].include?(uri.scheme) && uri.host && !uri.userinfo && !uri.fragment
100
+ invalid!("HTTP base URL must identify an HTTP origin without credentials or fragment")
101
+ end
102
+ uri
103
+ rescue URI::InvalidURIError
104
+ invalid!("Invalid HTTP URL")
105
+ end
106
+
107
+ def request_uri(path, query)
108
+ invalid!("HTTP path must be text") unless path.is_a?(String)
109
+ invalid!("HTTP query must be a hash") unless query.is_a?(Hash)
110
+ uri = path.empty? ? @base_uri.dup : URI.join(@base_uri.to_s.sub(%r{/\z}, "") + "/", path)
111
+ unless [uri.scheme, uri.host, uri.port] == [@base_uri.scheme, @base_uri.host, @base_uri.port] && !uri.userinfo && !uri.fragment
112
+ invalid!("Request path must stay within the configured origin")
113
+ end
114
+ unless query.empty?
115
+ encoded = URI.encode_www_form(query)
116
+ uri.query = [uri.query, encoded].compact.reject(&:empty?).join("&")
117
+ end
118
+ uri
119
+ rescue URI::InvalidURIError
120
+ invalid!("Invalid HTTP path")
121
+ end
122
+
123
+ def validate_headers(headers)
124
+ invalid!("HTTP headers must be a hash") unless headers.is_a?(Hash)
125
+ headers.to_h do |key, value|
126
+ name = key.to_s
127
+ unless name.match?(/\A[!#$%&'*+.^_`|~0-9a-z-]+\z/i) && value.is_a?(String) && !value.match?(/[\r\n\x00]/) && !FORBIDDEN_HEADERS.include?(name.downcase)
128
+ invalid!("Invalid HTTP header")
129
+ end
130
+ [name.dup.freeze, value.dup.freeze]
131
+ end
132
+ end
133
+
134
+ def check_size!(bytes, maximum = @max_response_bytes)
135
+ return if bytes <= maximum
136
+
137
+ raise Error.new("HTTP response exceeds the byte limit", code: "response_too_large", stage: "response")
138
+ end
139
+
140
+ def invalid!(message)
141
+ raise Netstack::InputError.new(message, code: "invalid_http_input", stage: "input")
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Netstack
4
+ module Integrations
5
+ module Http
6
+ class Error < Netstack::Error
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Netstack
6
+ module Integrations
7
+ module Http
8
+ class Response
9
+ attr_reader :status, :headers, :body
10
+
11
+ def initialize(status:, headers: {}, body: "")
12
+ @status = Integer(status)
13
+ @headers = headers.to_h.transform_keys { |key| key.to_s.downcase }.transform_values { |value| value.to_s.dup.freeze }.freeze
14
+ @body = String(body).dup.freeze
15
+ freeze
16
+ end
17
+
18
+ def json
19
+ JSON.parse(body)
20
+ rescue JSON::ParserError, EncodingError
21
+ raise Error.new("HTTP response is not valid JSON", code: "invalid_json", stage: "response"), cause: nil
22
+ end
23
+
24
+ def inspect = "#<#{self.class.name} status=#{status} bytes=#{body.bytesize}>"
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Netstack
4
+ module Integrations
5
+ module HttpDelivery
6
+ # 将工件引用接入 Deployment 的提交与回读协议。
7
+ # 每次新提交都重新解析工件并核对摘要;恢复按操作身份查询。
8
+ class Adapter
9
+ def initialize(client:, artifact_resolver:)
10
+ @client = client
11
+ @artifacts = Configuration::ArtifactResolver.new(resolver: artifact_resolver)
12
+ end
13
+
14
+ def preflight(plan:, previous:, credentials:) # rubocop:disable Naming/PredicateMethod -- Deployment adapter protocol.
15
+ @client.validate_credentials!(credentials)
16
+ previous_ids = previous ? previous.steps.map { |step| step.fetch(:step_id) } : []
17
+ plan.steps.each { |step| @artifacts.resolve(step).validate_target!(plan.target) unless previous_ids.include?(step.id) }
18
+ true
19
+ end
20
+
21
+ def apply(step:, target:, credentials:, idempotency_key:)
22
+ artifact = @artifacts.resolve(step)
23
+ result = @client.submit(target: target, artifact: artifact, credentials: credentials, idempotency_key: idempotency_key,
24
+ request_no: step.parameters[:request_no], target_id: step.parameters[:target_id])
25
+ response(result, submitting: true)
26
+ end
27
+
28
+ def verify(step:, target:, credentials:, receipt:) # rubocop:disable Lint/UnusedMethodArgument -- Deployment adapter protocol; lookup needs no device credentials.
29
+ # Even a previous POST terminal reply is reconciled using the same identity.
30
+ result = @client.status(remote_id: receipt[:remote_id], idempotency_key: receipt.fetch(:idempotency_key))
31
+ response(result, submitting: false)
32
+ end
33
+
34
+ private
35
+
36
+ def response(result, submitting:)
37
+ status = case result.status
38
+ when :succeeded then submitting ? :applied : :verified
39
+ when :running then submitting ? :accepted : :unknown
40
+ when :failed then :failed
41
+ else :unknown
42
+ end
43
+ value = { status: status, evidence: { remote_status: result.status } }
44
+ value[:remote_id] = result.remote_id if result.remote_id
45
+ value[:error_code] = :remote_rejected if result.failed?
46
+ value
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end