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,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Netstack
4
+ module Monitoring
5
+ # Hashes are projections; arrays are unordered collections with multiplicity.
6
+ # Each expected entry needs its own actual entry, including overlapping projections.
7
+ module StateComparison
8
+ module_function
9
+
10
+ def matches?(actual, expected)
11
+ case expected
12
+ when Hash
13
+ actual.is_a?(Hash) && expected.all? { |key, value| actual.key?(key) && matches?(actual[key], value) }
14
+ when Array
15
+ return false unless actual.is_a?(Array) && actual.size == expected.size
16
+ assigned = {}
17
+ expected.each_index.all? { |index| match_entry?(index, actual, expected, assigned, {}) }
18
+ else
19
+ actual == expected || ((expected.is_a?(Integer) || expected.is_a?(Float)) && actual.is_a?(String) && actual == expected.to_s)
20
+ end
21
+ end
22
+
23
+ def match_entry?(index, actual, expected, assigned, visited)
24
+ actual.each_index do |candidate|
25
+ next if visited[candidate] || !matches?(actual[candidate], expected[index])
26
+ visited[candidate] = true
27
+ if !assigned.key?(candidate) || match_entry?(assigned[candidate], actual, expected, assigned, visited)
28
+ assigned[candidate] = index
29
+ return true
30
+ end
31
+ end
32
+ false
33
+ end
34
+ private_class_method :match_entry?
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Netstack
4
+ # 不变量是区间有序、互不相交且已合并;遍历区间而不是逐个枚举主机。
5
+ class RangeSet
6
+ include Enumerable
7
+
8
+ attr_reader :ranges
9
+
10
+ # 校验、排序并合并区间,然后冻结集合以保证值语义。
11
+ def initialize(*ranges)
12
+ ordered = ranges.map do |range|
13
+ unless range.is_a?(Range) && range.begin.is_a?(Integer) && range.end.is_a?(Integer)
14
+ raise ArgumentError, "Intervals must have integer endpoints"
15
+ end
16
+ first, last = range.begin, range.end
17
+ last -= 1 if range.exclude_end?
18
+ raise ArgumentError, "Intervals must be nonnegative and ordered" if first.negative? || first > last
19
+ [ first, last ]
20
+ end.sort_by(&:first)
21
+ merged = []
22
+ ordered.each do |first, last|
23
+ if !merged.empty? && first <= merged.last[1] + 1
24
+ merged.last[1] = [ merged.last[1], last ].max
25
+ else
26
+ merged << [ first, last ]
27
+ end
28
+ end
29
+ @ranges = merged.map { |first, last| first..last }.freeze
30
+ @hash = @ranges.hash
31
+ freeze
32
+ end
33
+
34
+ # 遍历规范化后的区间;没有代码块时返回枚举器。
35
+ def each(&block)
36
+ return enum_for(__method__) unless block
37
+ ranges.each(&block)
38
+ self
39
+ end
40
+
41
+ # 判断集合是否没有区间。
42
+ def empty? = ranges.empty?
43
+
44
+ # 返回区间段数量,而不是地址或端口总数。
45
+ def size = ranges.size
46
+
47
+ # 返回最小端点;空集合返回 nil。
48
+ def min = ranges.first&.begin
49
+
50
+ # 返回最大端点;空集合返回 nil。
51
+ def max = ranges.last&.end
52
+
53
+ # 计算所有闭区间包含的元素总数。
54
+ def cardinality = sum { |range| range.end - range.begin + 1 }
55
+
56
+ attr_reader :hash
57
+ # 仅按规范化后的区间比较集合内容。
58
+ def ==(other) = other.is_a?(RangeSet) && ranges == other.ranges
59
+
60
+ alias_method :eql?, :==
61
+
62
+ # 计算并集;两个集合为空或相同对象时直接复用已有值。
63
+ def |(other)
64
+ validate_operand!(other)
65
+ return self if other.empty? || equal?(other)
66
+ return other if empty?
67
+ self.class.new(*ranges, *other.ranges)
68
+ end
69
+
70
+ # 用双指针扫描两个有序集合,计算交集。
71
+ def &(other)
72
+ validate_operand!(other)
73
+ return self if equal?(other) || empty?
74
+ return other if other.empty?
75
+
76
+ result = []
77
+ left = right = 0
78
+ while left < size && right < other.size
79
+ a, b = ranges[left], other.ranges[right]
80
+ first, last = [ a.begin, b.begin ].max, [ a.end, b.end ].min
81
+ result << (first..last) if first <= last
82
+ left += 1 if a.end <= b.end
83
+ right += 1 if b.end <= a.end
84
+ end
85
+ self.class.new(*result)
86
+ end
87
+
88
+ # 从当前集合中减去另一个集合,逐段截去重叠部分。
89
+ def -(other)
90
+ validate_operand!(other)
91
+ return self if empty? || other.empty?
92
+ return self.class.new if equal?(other)
93
+
94
+ result = []
95
+ right = 0
96
+ each do |range|
97
+ cursor = range.begin
98
+ right += 1 while right < other.size && other.ranges[right].end < cursor
99
+ scan = right
100
+ while scan < other.size && other.ranges[scan].begin <= range.end
101
+ removed = other.ranges[scan]
102
+ result << (cursor..(removed.begin - 1)) if removed.begin > cursor
103
+ cursor = [ cursor, removed.end + 1 ].max
104
+ break if cursor > range.end
105
+ scan += 1
106
+ end
107
+ result << (cursor..range.end) if cursor <= range.end
108
+ end
109
+ self.class.new(*result)
110
+ end
111
+
112
+ # 判断整数是否落在任一规范化区间内。
113
+ def include?(number)
114
+ raise ArgumentError, "Membership requires an integer" unless number.is_a?(Integer)
115
+ candidate = ranges.bsearch { |range| range.end >= number }
116
+ candidate ? candidate.begin <= number : false
117
+ end
118
+
119
+ # 判断集合是否被当前集合覆盖;空集合始终被覆盖。
120
+ def cover?(other)
121
+ return include?(other) if other.is_a?(Integer)
122
+ validate_operand!(other)
123
+ if other.size == 1
124
+ target = other.ranges.first
125
+ candidate = ranges.bsearch { |range| range.end >= target.end }
126
+ return candidate ? candidate.begin <= target.begin : false
127
+ end
128
+ index = 0
129
+ other.each do |range|
130
+ index += 1 while index < size && ranges[index].end < range.begin
131
+ return false if index == size || ranges[index].begin > range.begin || ranges[index].end < range.end
132
+ end
133
+ true
134
+ end
135
+
136
+ # 判断当前集合是否是另一个集合的子集。
137
+ def subset?(other)
138
+ validate_operand!(other)
139
+ other.cover?(self)
140
+ end
141
+
142
+ # 判断两个有序集合是否存在重叠区间。
143
+ def overlap?(other)
144
+ validate_operand!(other)
145
+ left = right = 0
146
+ while left < size && right < other.size
147
+ a, b = ranges[left], other.ranges[right]
148
+ return true if a.overlap?(b)
149
+ (a.end < b.end) ? left += 1 : right += 1
150
+ end
151
+ false
152
+ end
153
+
154
+ # 返回相等、包含、被包含或互不相同四种关系之一。
155
+ def relation(other)
156
+ validate_operand!(other)
157
+ return :equal if self == other
158
+ return :contains if cover?(other)
159
+ return :within if subset?(other)
160
+ :other
161
+ end
162
+
163
+ # 返回区间数组的浅副本,避免调用方修改内部数组。
164
+ def to_a = ranges.dup
165
+
166
+ # 哈希值只在当前进程有效;持久化区间,并在读取进程中重新建立冻结集合。
167
+ def marshal_dump = ranges
168
+
169
+ # 从持久化区间恢复集合的不变量和冻结状态。
170
+ def marshal_load(ranges)
171
+ initialize(*ranges)
172
+ end
173
+
174
+ private
175
+
176
+ # 统一校验集合运算的右值类型。
177
+ def validate_operand!(other)
178
+ raise ArgumentError, "Expected a RangeSet" unless other.is_a?(RangeSet)
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ipaddr"
4
+
5
+ module Netstack
6
+ # 路由证据与本地查询;不安装路由,也不执行设备 I/O。
7
+ module Routing
8
+ # 先限定目标、VRF 和地址族,再按前缀长度与优先级筛选。
9
+ # 排序依据未知时保留歧义;并列的最优路由保留 ECMP 候选。
10
+ class Lookup
11
+ def call(routes:, destination:, target_key:, vrf: "default")
12
+ unless routes.is_a?(Array) && routes.all? { |route| route.is_a?(Route) }
13
+ raise InputError, "lookup routes must be Route values"
14
+ end
15
+ target_key = Values.text(target_key, name: "route target")
16
+ vrf = Values.text(vrf, name: "VRF")
17
+ address = ::IPAddr.new(Values.text(destination, name: "lookup destination"))
18
+ raise InputError, "lookup requires one unscoped host" if destination.match?(/[\/%]/)
19
+
20
+ candidates = routes.select do |route|
21
+ route.target_key == target_key && route.vrf == vrf && route.network.ipv4? == address.ipv4? && route.network.include?(address)
22
+ end
23
+ return LookupResult.new(routes: [], status: :not_found) if candidates.empty?
24
+
25
+ longest = candidates.map { |route| route.network.prefix }.max
26
+ candidates.select! { |route| route.network.prefix == longest }
27
+ if candidates.size > 1 && candidates.any? { |route| route.preference.nil? }
28
+ return LookupResult.new(routes: candidates, status: :ambiguous, reason: "route preference is unknown")
29
+ end
30
+ preference = candidates.filter_map(&:preference).min
31
+ candidates.select! { |route| route.preference == preference } if preference
32
+ if candidates.map(&:protocol).uniq.size > 1
33
+ return LookupResult.new(routes: candidates, status: :ambiguous, reason: "metrics from different protocols are not comparable")
34
+ end
35
+ if candidates.size > 1 && candidates.any? { |route| route.metric.nil? }
36
+ return LookupResult.new(routes: candidates, status: :ambiguous, reason: "route metric is unknown")
37
+ end
38
+ metric = candidates.filter_map(&:metric).min
39
+ candidates.select! { |route| route.metric == metric } if metric
40
+ LookupResult.new(routes: candidates, status: :matched)
41
+ rescue ::IPAddr::Error
42
+ raise InputError, "invalid lookup destination"
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Netstack
4
+ module Routing
5
+ class LookupResult < Data.define(:routes, :status, :reason)
6
+ def initialize(routes:, status:, reason: nil)
7
+ super(routes: routes.dup.freeze, status: status, reason: Values.copy(reason))
8
+ end
9
+
10
+ def matched? = status == :matched
11
+ def ambiguous? = status == :ambiguous
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ipaddr"
4
+
5
+ module Netstack
6
+ module Routing
7
+ class Route < Data.define(:target_key, :vrf, :destination, :protocol, :preference, :metric, :next_hop, :interface)
8
+ def initialize(target_key:, destination:, protocol:, vrf: "default", preference: nil, metric: nil, next_hop: nil, interface: nil)
9
+ text = Values.text(destination, name: "route destination")
10
+ raise InputError, "route destination cannot contain a scope identifier" if text.include?("%")
11
+ network = ::IPAddr.new(text)
12
+ if next_hop
13
+ hop = ::IPAddr.new(Values.text(next_hop, name: "next hop"))
14
+ raise InputError, "route next hop family mismatch" if hop.ipv4? != network.ipv4?
15
+ raise InputError, "next hop must be an unscoped host" if next_hop.match?(/[\/%]/)
16
+
17
+ next_hop = hop.to_s
18
+ end
19
+ super(target_key: Values.text(target_key, name: "route target"), vrf: Values.text(vrf, name: "VRF"),
20
+ destination: "#{network}/#{network.prefix}".freeze, protocol: Values.text(protocol, name: "route protocol").downcase.freeze,
21
+ preference: preference.nil? ? nil : Values.integer(preference, name: "route preference", min: 0),
22
+ metric: metric.nil? ? nil : Values.integer(metric, name: "route metric", min: 0),
23
+ next_hop: Values.copy(next_hop), interface: Values.text(interface, name: "route interface", optional: true))
24
+ rescue ::IPAddr::Error
25
+ raise InputError, "invalid route address"
26
+ end
27
+
28
+ def network = ::IPAddr.new(destination)
29
+ def key = [target_key, vrf, destination, protocol, preference, metric, next_hop, interface].freeze
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Netstack
4
+ module Routing
5
+ class Snapshot < Data.define(:target, :source, :vrf, :dimension)
6
+ def initialize(target:, source:, vrf:, dimension:)
7
+ unless target.is_a?(Target) && dimension.is_a?(Dimension) && dimension.name == :routes
8
+ raise InputError, "invalid route snapshot"
9
+ end
10
+ vrf = Values.text(vrf, name: "VRF")
11
+ unless dimension.records.all? { |route| route.is_a?(Route) && route.target_key == target.key && route.vrf == vrf }
12
+ raise InputError, "route snapshot contains foreign routes"
13
+ end
14
+ identities = dimension.records.map(&:key)
15
+ raise InputError, "duplicate route identity" unless identities.uniq.size == identities.size
16
+
17
+ super(target: target, source: Values.text(source, name: "route source"), vrf: vrf, dimension: dimension)
18
+ end
19
+
20
+ def complete? = dimension.complete?
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Netstack
4
+ # 宿主提供的不可变目标身份;授权和凭据由宿主另外管理。
5
+ # 属性会进入计划,宿主必须确保其中不包含秘密。
6
+ class Target < Data.define(:key, :host, :platform, :attributes)
7
+ def initialize(key:, host:, platform:, attributes: {})
8
+ raise InputError, "attributes must be a Hash" unless attributes.is_a?(Hash)
9
+
10
+ super(key: Values.text(key, name: "target key"), host: Values.text(host, name: "host"),
11
+ platform: Values.text(platform.is_a?(Symbol) ? platform.to_s : platform, name: "platform"),
12
+ attributes: Values.copy(attributes))
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Netstack
4
+ class UnsupportedCapability < Error
5
+ def initialize(message = "unsupported capability", code: :unsupported_capability, stage: :validate, details: {})
6
+ super
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+ require "active_support/core_ext/object/deep_dup"
5
+ require "active_support/core_ext/object/blank"
6
+
7
+ module Netstack
8
+ # 防御性复制宿主交换值;只接受明确支持的类型,并建立深度不可变约束。
9
+ module Values
10
+ module_function
11
+
12
+ def copy(value)
13
+ freeze_value(value.deep_dup)
14
+ end
15
+
16
+ # ActiveSupport 负责深复制;此遍历只建立领域值的不可变契约。
17
+ def freeze_value(value)
18
+ case value
19
+ when Hash
20
+ value.to_h { |key, item| [freeze_value(key), freeze_value(item)] }.freeze
21
+ when Array
22
+ value.map! { |item| freeze_value(item) }.freeze
23
+ when String, Time
24
+ value.freeze
25
+ when NilClass, TrueClass, FalseClass, Integer, Symbol
26
+ value
27
+ when Float
28
+ raise InputError, "numeric values must be finite" unless value.finite?
29
+
30
+ value
31
+ when Data
32
+ value.class.new(**value.to_h.transform_values { |item| copy(item) })
33
+ else
34
+ raise ArgumentError, "unsupported value type: #{value.class}"
35
+ end
36
+ end
37
+ private_class_method :freeze_value
38
+
39
+ # 用于身份与标签的文本归一化,会去除首尾空白;不能用于不透明凭据。
40
+ def text(value, name:, optional: false)
41
+ return nil if optional && value.nil?
42
+ raise InputError, "#{name} must be nonempty text" unless value.is_a?(String) && value.present?
43
+
44
+ value.strip.freeze
45
+ end
46
+
47
+ def integer(value, name:, min: nil, max: nil)
48
+ parsed = if value.is_a?(Integer)
49
+ value
50
+ elsif value.is_a?(String) && /\A[0-9]+\z/.match?(value) && value.length <= 20
51
+ Integer(value, 10)
52
+ end
53
+ unless parsed && (!min || parsed >= min) && (!max || parsed <= max)
54
+ raise InputError, "#{name} must be an integer in range"
55
+ end
56
+
57
+ parsed
58
+ end
59
+
60
+ def time(value, name:, optional: false)
61
+ return nil if optional && value.nil?
62
+ return value.dup.freeze if value.is_a?(Time)
63
+ raise InputError, "#{name} must be an ISO 8601 timestamp" unless value.is_a?(String)
64
+
65
+ Time.iso8601(value).freeze
66
+ rescue ArgumentError
67
+ raise InputError, "#{name} must be an ISO 8601 timestamp"
68
+ end
69
+ end
70
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Netstack
4
- VERSION = "0.1.1"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Netstack
4
+ module Vxlan
5
+ class Auditor
6
+ def inspect(snapshot)
7
+ findings = []
8
+ fabric = snapshot.fabric
9
+ (fabric.expected_node_keys - fabric.nodes.map(&:key)).each do |key|
10
+ add(findings, :configuration_incomplete, :warning, [key], "Configuration evidence is unavailable")
11
+ inspect_runtime(Node.new(key: key), snapshot.runtime.fetch(key, {}), findings)
12
+ end
13
+ fabric.nodes.each do |node|
14
+ add(findings, :configuration_incomplete, :warning, [node.key], "Configuration evidence is incomplete") unless node.complete?
15
+ inspect_node(node, findings)
16
+ inspect_runtime(node, snapshot.runtime.fetch(node.key, {}), findings)
17
+ end
18
+ inspect_consistency(fabric, findings)
19
+ inspect_vpc(fabric, findings)
20
+ findings.freeze
21
+ end
22
+
23
+ private
24
+
25
+ def add(findings, code, severity, keys, message, **details)
26
+ findings << Finding.new(code: code, severity: severity, node_keys: keys, message: message, details: details)
27
+ end
28
+
29
+ def inspect_node(node, findings)
30
+ inspect_bgp(node, findings) if node.complete?
31
+ return unless node.vtep?
32
+
33
+ add(findings, :nve_shutdown, :critical, [node.key], "NVE interface is administratively down") if node.nve[:shutdown]
34
+ return unless node.complete?
35
+
36
+ add(findings, :nve_source_missing, :critical, [node.key], "NVE source interface is missing") unless node.nve[:source_interface]
37
+ node.segments.each do |segment|
38
+ details = { vlan_id: segment.vlan_id, vni: segment.vni, vrf: segment.vrf, prefix: segment.prefix }
39
+ add(findings, :anycast_gateway_disabled, :warning, [node.key], "SVI does not enable anycast gateway", **details) unless segment.anycast
40
+ add(findings, :anycast_mac_missing, :critical, [node.key], "Global anycast gateway MAC is missing", **details) if segment.anycast && !node.anycast_mac
41
+ member = node.nve.fetch(:members, {})[segment.vni]
42
+ add(findings, :nve_member_missing, :critical, [node.key], "Segment VNI is missing from NVE", **details) unless member
43
+ if member && (member[:associate_vrf] || (!member[:replication] && !member[:multicast_group]))
44
+ add(findings, :nve_member_invalid, :critical, [node.key], "L2 NVE member has invalid replication or L3 association", **details)
45
+ end
46
+ evpn = node.evpn[segment.vni]
47
+ add(findings, :evpn_vni_missing, :critical, [node.key], "L2 VNI is missing from EVPN", **details) unless evpn
48
+ if evpn && !bidirectional?(evpn[:route_targets])
49
+ add(findings, :evpn_route_target_missing, :critical, [node.key], "EVPN import/export route-target is incomplete", **details)
50
+ end
51
+ vrf = node.vrfs[segment.vrf]
52
+ unless vrf && vrf[:vni] && node.nve.fetch(:members, {}).dig(vrf[:vni], :associate_vrf)
53
+ add(findings, :vrf_l3_vni_missing, :critical, [node.key], "VRF L3 VNI or NVE association is missing", **details)
54
+ end
55
+ if vrf && !bidirectional?(vrf[:route_targets].select { |target| target[:evpn] })
56
+ add(findings, :vrf_route_target_missing, :critical, [node.key], "VRF EVPN import/export route-target is incomplete", **details)
57
+ end
58
+ advertisement = RoutePolicy.new.advertisement(node, segment)
59
+ unless advertisement == :permitted
60
+ add(findings, :route_advertisement, advertisement == :unknown ? :warning : :critical, [node.key],
61
+ "Segment route advertisement is #{advertisement}", **details, decision: advertisement)
62
+ end
63
+ segment.acl_bindings.each do |direction, name|
64
+ unless node.acls.key?(name)
65
+ add(findings, :acl_missing, :critical, [node.key], "SVI references an absent ACL", **details, acl: name,
66
+ direction: direction)
67
+ end
68
+ end
69
+ end
70
+ node.vlan_vnis.each do |vlan_id, vni|
71
+ next unless vni && !node.segment(vlan_id)
72
+ next if node.vrfs.values.any? { |vrf| vrf[:vni] == vni }
73
+
74
+ add(findings, :unrouted_vlan, :warning, [node.key], "Mapped VLAN has no parsed routed SVI", vlan_id: vlan_id, vni: vni)
75
+ end
76
+ end
77
+
78
+ def bidirectional?(targets)
79
+ directions = targets.flat_map { |target| target[:direction] == :both ? %i[import export] : [target[:direction]] }
80
+ (%i[import export] - directions).empty?
81
+ end
82
+
83
+ def inspect_bgp(node, findings)
84
+ node.bgp.fetch(:neighbors, {}).each do |peer, settings|
85
+ template = node.bgp.fetch(:templates, {})[settings[:inherit_peer]] || {}
86
+ families = (template[:address_families] || {}).merge(settings[:address_families]) do |_key, parent, child|
87
+ parent.merge(child.compact)
88
+ end
89
+ family = families["l2vpn evpn"]
90
+ next unless family
91
+
92
+ unless family[:extended_community]
93
+ add(findings, :bgp_extended_community_missing, :critical, [node.key], "EVPN neighbor lacks extended community advertisement",
94
+ peer_ip: peer)
95
+ end
96
+ if node.role == :spine && !family[:route_reflector_client]
97
+ add(findings, :bgp_reflector_client_missing, :warning, [node.key], "Spine EVPN neighbor is not a route-reflector client", peer_ip: peer)
98
+ end
99
+ end
100
+ end
101
+
102
+ def inspect_consistency(fabric, findings)
103
+ fabric.vteps.flat_map { |node| node.segments.map { |segment| [node, segment] } }.group_by { |_node, segment| segment.identity }.each do |identity, rows|
104
+ signatures = rows.map { |_node, segment| segment.to_h }.uniq
105
+ if signatures.size > 1
106
+ add(findings, :segment_inconsistent, :critical, rows.map do |node, _segment|
107
+ node.key
108
+ end, "Segment configuration differs between nodes", identity: identity, values: signatures)
109
+ end
110
+ acl_contents = rows.map do |node, segment|
111
+ segment.acl_bindings.transform_values { |name| node.acls[name] }
112
+ end
113
+ next unless acl_contents.uniq.size > 1
114
+
115
+ add(findings, :acl_inconsistent, :critical, rows.map do |node, _segment|
116
+ node.key
117
+ end, "Segment ACL contents differ between nodes", identity: identity)
118
+ end
119
+ fabric.vteps.flat_map { |node| node.segments.map { |segment| [node, segment] } }.group_by { |_node, segment| segment.vni }.each do |vni, rows|
120
+ identities = rows.map { |_node, segment| segment.identity }.uniq
121
+ next unless identities.size > 1
122
+
123
+ add(findings, :vni_conflict, :critical, rows.map do |node, _segment|
124
+ node.key
125
+ end.uniq, "VNI represents different routed segments", vni: vni, identities: identities)
126
+ end
127
+ macs = fabric.vteps.filter_map(&:anycast_mac).uniq
128
+ add(findings, :anycast_mac_inconsistent, :critical, fabric.vteps.map(&:key), "Anycast gateway MAC differs between VTEPs", values: macs) if macs.size > 1
129
+ end
130
+
131
+ def inspect_vpc(fabric, findings)
132
+ fabric.nodes.select(&:vpc_domain).group_by { |node| node.vpc_domain.id }.each do |id, nodes|
133
+ if nodes.size != 2
134
+ add(findings, :vpc_member_count, :warning, nodes.map(&:key), "vPC domain does not have exactly two observed nodes", domain_id: id,
135
+ count: nodes.size)
136
+ end
137
+ nodes.each do |node|
138
+ domain = node.vpc_domain
139
+ if node.complete? && (domain.peer_links.empty? || domain.peer_keepalive[:destination].nil?)
140
+ add(findings, :vpc_peer_configuration_missing, :critical, [node.key], "vPC peer-link or keepalive destination is missing", domain_id: id)
141
+ end
142
+ end
143
+ next unless nodes.size == 2
144
+
145
+ left, right = nodes.map(&:vpc_domain)
146
+ if left.peer_keepalive[:destination] != right.peer_keepalive[:source] || right.peer_keepalive[:destination] != left.peer_keepalive[:source]
147
+ add(findings, :vpc_keepalive_inconsistent, :critical, nodes.map(&:key), "vPC keepalive endpoints are not reciprocal", domain_id: id)
148
+ end
149
+ if left.member_ports.keys.sort != right.member_ports.keys.sort
150
+ add(findings, :vpc_members_inconsistent, :critical, nodes.map(&:key), "vPC member IDs differ between peers", domain_id: id)
151
+ end
152
+ end
153
+ end
154
+
155
+ def inspect_runtime(node, dimensions, findings)
156
+ dimensions.each do |name, result|
157
+ unless result.complete?
158
+ add(findings, :runtime_incomplete, :warning, [node.key], "Runtime dimension is #{result.status}", dimension: name, status: result.status)
159
+ end
160
+ result.records.each do |row|
161
+ abnormal = case name
162
+ when :nve_peers, :nve_vnis then row[:state] != :up
163
+ when :bgp_neighbors then row[:state] != :established
164
+ when :ospf_neighbors then !row[:state].start_with?("full")
165
+ when :port_channels then !row[:flags].include?("U")
166
+ when :vpc then row[:consistency_status] != "success" || !row.fetch(:peer_status, "").include?("formed ok")
167
+ else false
168
+ end
169
+ add(findings, :runtime_abnormal, :critical, [node.key], "Runtime #{name} is abnormal", dimension: name, record: row) if abnormal
170
+ end
171
+ next unless result.complete? && node.complete?
172
+
173
+ expected = if name == :nve_vnis
174
+ node.nve.fetch(:members, {}).keys
175
+ else
176
+ name == :bgp_neighbors ? node.bgp.fetch(:neighbors, {}).keys : []
177
+ end
178
+ observed = result.records.map { |row| name == :nve_vnis ? row[:vni] : row[:peer_ip] }
179
+ (expected - observed).each do |identity|
180
+ add(findings, :runtime_object_missing, :critical, [node.key], "Configured object is absent from complete runtime evidence", dimension: name,
181
+ identity: identity)
182
+ end
183
+ end
184
+ end
185
+ end
186
+ end
187
+ end