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,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Netstack
4
+ module Monitoring
5
+ # The host must persist every checkpoint synchronously and return true. A
6
+ # recovery never repeats a write for which a started checkpoint exists.
7
+ class Applier
8
+ def initialize(client:)
9
+ @client = client
10
+ end
11
+
12
+ def call(plan:, checkpoint:, credentials: nil, execution_id: nil)
13
+ execute(plan, checkpoint: checkpoint, credentials: credentials, events: [], execution_id: execution_id)
14
+ end
15
+
16
+ def recover(plan:, events:, checkpoint:, credentials: nil, execution_id: nil)
17
+ execute(plan, checkpoint: checkpoint, credentials: credentials, events: events, execution_id: execution_id)
18
+ end
19
+
20
+ private
21
+
22
+ def execute(plan, checkpoint:, credentials:, events:, execution_id:)
23
+ Inputs.invalid!("Expected a monitoring plan") unless plan.is_a?(Plan)
24
+ Inputs.invalid!("A durable checkpoint callback is required") unless checkpoint.respond_to?(:call)
25
+ execution_id = Inputs.text(execution_id, name: "execution ID") unless execution_id.nil?
26
+ history = validate_events!(plan, events, execution_id)
27
+ ids = {}
28
+ plan.operations.each do |operation|
29
+ prior = history.fetch(operation.key, [])
30
+ attributes = resolve(operation.attributes, ids, credentials: credentials, secrets: false)
31
+ if prior.empty?
32
+ preflight!(operation, ids)
33
+ payload = resolve(operation.attributes, ids, credentials: credentials, secrets: true)
34
+ emit!(checkpoint, event(plan, operation, "started", execution_id: execution_id))
35
+ id = if operation.action == "create"
36
+ @client.create(operation.resource, payload)
37
+ else
38
+ @client.update(operation.resource, resolve(operation.id, ids), payload)
39
+ end
40
+ id = Inputs.identifier(id)
41
+ emit!(checkpoint, event(plan, operation, "receipt", id: id, execution_id: execution_id))
42
+ else
43
+ receipt = prior.find { |item| item["stage"] == "receipt" }
44
+ id = receipt && receipt["id"]
45
+ id ||= recover_id(operation, ids, attributes)
46
+ Inputs.invalid!("Recovery receipt belongs to a different resource") if operation.action == "update" && id != resolve(operation.id, ids)
47
+ emit!(checkpoint, event(plan, operation, "receipt", id: id, recovered: true, execution_id: execution_id)) unless receipt
48
+ end
49
+ verify!(operation, id, attributes)
50
+ unless prior.any? { |item| item["stage"] == "verified" }
51
+ emit!(checkpoint, event(plan, operation, "verified", id: id, execution_id: execution_id,
52
+ verification: operation.resource == "usermacro" ? "metadata" : "readback"))
53
+ end
54
+ ids[operation.key] = id
55
+ end
56
+ Values.copy({ "status" => "verified", "plan_digest" => plan.digest, "resources" => ids, "managed" => plan.managed, "execution_id" => execution_id })
57
+ end
58
+
59
+ def preflight!(operation, ids)
60
+ if operation.action == "create"
61
+ current = @client.find(operation.resource, resolve(operation.selector, ids))
62
+ fail!("Plan no longer matches the remote state", "stale_monitoring_plan") if current
63
+ else
64
+ current = @client.get(operation.resource, resolve(operation.id, ids))
65
+ fail!("Plan no longer matches the remote state", "stale_monitoring_plan") unless current && matches?(normalize(current), operation.before)
66
+ end
67
+ end
68
+
69
+ def recover_id(operation, ids, attributes)
70
+ current = if operation.action == "create"
71
+ @client.find(operation.resource, resolve(operation.selector, ids))
72
+ else
73
+ @client.get(operation.resource, resolve(operation.id, ids))
74
+ end
75
+ # Hidden macro values cannot prove whether a lost response represented a
76
+ # successful write. Require the original receipt or manual reconciliation.
77
+ if operation.resource == "usermacro" || !current || !matches?(normalize(current), verifiable(attributes))
78
+ fail!("Write outcome remains unknown; do not replay it", "monitoring_outcome_unknown")
79
+ end
80
+ Inputs.identifier(current.fetch(@client.resource_keys(operation.resource).first))
81
+ end
82
+
83
+ def verify!(operation, id, attributes)
84
+ current = @client.get(operation.resource, id)
85
+ return if current && matches?(normalize(current), verifiable(attributes))
86
+
87
+ fail!("Remote readback did not confirm the intended state", "monitoring_verification_failed")
88
+ end
89
+
90
+ def verifiable(value)
91
+ case value
92
+ when Hash
93
+ value.each_with_object({}) do |(key, item), result|
94
+ next if item.is_a?(Hash) && item.key?("$secret")
95
+
96
+ result[key] = verifiable(item)
97
+ end
98
+ when Array then value.map { |item| verifiable(item) }
99
+ else value
100
+ end
101
+ end
102
+
103
+ def normalize(row)
104
+ value = Inputs.object(row)
105
+ value["templates"] = value["parentTemplates"] if value.key?("parentTemplates")
106
+ value
107
+ end
108
+
109
+ def matches?(actual, expected)
110
+ StateComparison.matches?(actual, expected)
111
+ end
112
+
113
+ def resolve(value, ids, credentials: nil, secrets: false)
114
+ case value
115
+ when Hash
116
+ if value.keys == ["$ref"]
117
+ ids.fetch(value.fetch("$ref"))
118
+ elsif value.keys == ["$secret"]
119
+ return value unless secrets
120
+
121
+ Inputs.invalid!("A credentials resolver is required") unless credentials.respond_to?(:call)
122
+ secret = resolve_secret(credentials, value.fetch("$secret"))
123
+ Inputs.invalid!("Credential reference did not resolve") unless secret.is_a?(String) && !secret.empty?
124
+ secret
125
+ else
126
+ value.transform_values { |item| resolve(item, ids, credentials: credentials, secrets: secrets) }
127
+ end
128
+ when Array then value.map { |item| resolve(item, ids, credentials: credentials, secrets: secrets) }
129
+ else value
130
+ end
131
+ end
132
+
133
+ def resolve_secret(credentials, key)
134
+ credentials.call(key)
135
+ rescue StandardError
136
+ raise Netstack::Error.new("Credential resolution failed", code: "credential_unavailable", stage: "credentials"), cause: nil
137
+ end
138
+
139
+ def validate_events!(plan, events, execution_id)
140
+ Inputs.invalid!("Recovery events must be an array") unless events.is_a?(Array)
141
+ history = {}
142
+ current_index = 0
143
+ events.each do |raw|
144
+ item = Inputs.object(raw)
145
+ operation = plan.operations[current_index]
146
+ previous = operation && history.fetch(operation.key, [])
147
+ expected_stage = %w[started receipt verified][previous&.size || 0]
148
+ unless operation && item["plan_digest"] == plan.digest && item["operation"] == operation.key && item["stage"] == expected_stage &&
149
+ item["execution_id"] == execution_id && item["resource"] == operation.resource && item["managed"] == plan.managed
150
+ Inputs.invalid!("Recovery events do not match this plan or stage order")
151
+ end
152
+ Inputs.identifier(item["id"]) unless expected_stage == "started"
153
+ Inputs.invalid!("Recovery resource ID changed") if expected_stage == "verified" && previous.last["id"] != item["id"]
154
+ (history[operation.key] ||= []) << item
155
+ current_index += 1 if expected_stage == "verified"
156
+ end
157
+ history
158
+ end
159
+
160
+ def event(plan, operation, stage, **fields)
161
+ Values.copy({ "plan_digest" => plan.digest, "operation" => operation.key, "resource" => operation.resource,
162
+ "stage" => stage, "managed" => plan.managed }.merge(fields.transform_keys(&:to_s)))
163
+ end
164
+
165
+ def emit!(checkpoint, event)
166
+ begin
167
+ acknowledged = checkpoint.call(event)
168
+ rescue StandardError
169
+ raise Netstack::Error.new("Checkpoint persistence failed", code: "checkpoint_failed", stage: "monitoring"), cause: nil
170
+ end
171
+ fail!("Checkpoint was not durably acknowledged", "checkpoint_not_committed") unless acknowledged == true
172
+ end
173
+
174
+ def fail!(message, code)
175
+ raise Netstack::Error.new(message, code: code, stage: "monitoring")
176
+ end
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Netstack
4
+ module Monitoring
5
+ class DevicePlan
6
+ def initialize(client:)
7
+ @hosts = HostPlan.new(client: client)
8
+ end
9
+
10
+ def call(target:, groups:, community_ref:, templates: [], proxy_group_id: nil, name: nil,
11
+ enabled: true, inventory: {}, tags: {}, previous_managed: {})
12
+ address = Inputs.ip(target.host)
13
+ secret = Inputs.text(community_ref, name: "SNMP community reference")
14
+ @hosts.call(target: target, kind: "device", groups: groups, templates: templates,
15
+ proxy_group_id: proxy_group_id, name: name, enabled: enabled, inventory: inventory, tags: tags,
16
+ previous_managed: previous_managed,
17
+ interface: { "type" => 2, "main" => 1, "useip" => 1, "ip" => address, "dns" => "", "port" => "161",
18
+ "details" => { "version" => 2, "bulk" => 1, "community" => "{$SNMP_COMMUNITY}" } },
19
+ secret_macro: { "macro" => "{$SNMP_COMMUNITY}", "value" => { "$secret" => secret }, "type" => 1 })
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Netstack
4
+ module Monitoring
5
+ # Build a read-only plan. Only previously owned collection members are
6
+ # removed; manual groups, templates, tags and inventory fields survive.
7
+ class HostPlan
8
+ def initialize(client:)
9
+ @client = client
10
+ end
11
+
12
+ def call(target:, kind:, groups:, templates: [], proxy_group_id: nil, name: nil, enabled: true,
13
+ inventory: {}, tags: {}, interface: nil, secret_macro: nil, previous_managed: {})
14
+ Inputs.boolean(enabled)
15
+ identity = Inputs.text(target.key, name: "technical host")
16
+ Inputs.invalid!("Technical host contains expression delimiters") if identity.match?(%r{[/\[\](),]})
17
+ old = previous_ownership(previous_managed, target.key)
18
+ group_ids = Inputs.ids(groups)
19
+ template_ids = Inputs.ids(templates)
20
+ existing = @client.find("host", "filter" => { "host" => [identity] })
21
+ Inputs.invalid!("Host lookup returned a different identity") if existing && existing["host"] != identity
22
+ unless enabled
23
+ operations = if existing && existing["status"].to_s != "1"
24
+ [Mutation.new(key: "host", resource: "host", action: "update",
25
+ id: existing.fetch("hostid"), attributes: { "status" => 1 }, before: { "status" => existing.fetch("status") })]
26
+ else
27
+ []
28
+ end
29
+ managed = existing ? old : previous_ownership({}, target.key)
30
+ return Plan.new(key: "#{kind}:#{target.key}", operations: operations, managed: managed)
31
+ end
32
+ managed = { "target" => target.key, "group_ids" => group_ids, "template_ids" => template_ids }
33
+ Inputs.invalid!("At least one host group is required") if group_ids.empty?
34
+ inventory = Inputs.object(inventory)
35
+ Inputs.invalid!("Inventory fields must be text") unless inventory.values.all? { |value| value.is_a?(String) }
36
+ desired_tags = Inputs.object(tags).merge("kind" => kind.to_s, "target" => target.key).map do |key, value|
37
+ { "tag" => "netstack.#{Inputs.text(key, name: "tag name")}", "value" => Inputs.text(value, name: "tag value") }
38
+ end
39
+ attributes = {
40
+ "host" => identity, "name" => Inputs.text(name || target.key, name: "visible host name"),
41
+ "status" => 0, "groups" => references(group_ids, "groupid"),
42
+ "templates" => references(template_ids, "templateid"), "tags" => desired_tags,
43
+ "inventory_mode" => 0, "inventory" => inventory
44
+ }
45
+ attributes.merge!("monitored_by" => 2, "proxy_groupid" => Inputs.identifier(proxy_group_id)) if proxy_group_id
46
+ managed["tag_names"] = desired_tags.map { |tag| tag.fetch("tag") }
47
+ if existing
48
+ groups_before = refs!(existing["groups"], "groupid")
49
+ templates_before = refs!(existing["parentTemplates"], "templateid")
50
+ attributes["groups"] = references((groups_before - old.fetch("group_ids")) | group_ids, "groupid")
51
+ attributes["templates"] = references((templates_before - old.fetch("template_ids")) | template_ids, "templateid")
52
+ removed_tags = old.fetch("tag_names") | managed.fetch("tag_names")
53
+ tags_before = tag_rows!(existing["tags"])
54
+ attributes["tags"] = tags_before.reject { |tag| removed_tags.include?(tag.fetch("tag")) } + desired_tags
55
+ attributes["inventory"] = Inputs.object(existing["inventory"]).merge(inventory)
56
+ before = attributes.keys.to_h do |key|
57
+ current = key == "templates" ? references(templates_before, "templateid") : existing[key]
58
+ [key, current]
59
+ end
60
+ before.compact!
61
+ changes = attributes.reject { |key, value| equivalent?(before[key], value) }
62
+ operations = if changes.empty?
63
+ []
64
+ else
65
+ [Mutation.new(key: "host", resource: "host", action: "update", id: existing.fetch("hostid"), attributes: changes, before: before)]
66
+ end
67
+ host_id = existing.fetch("hostid").to_s
68
+ else
69
+ operations = [Mutation.new(key: "host", resource: "host", action: "create", attributes: attributes,
70
+ selector: { "filter" => { "host" => [identity] } })]
71
+ host_id = { "$ref" => "host" }
72
+ end
73
+ append_interface(operations, host_id, interface, existing) if interface
74
+ append_macro(operations, host_id, secret_macro, existing) if secret_macro
75
+ Plan.new(key: "#{kind}:#{target.key}", operations: operations, managed: managed)
76
+ end
77
+
78
+ private
79
+
80
+ def previous_ownership(value, target_key)
81
+ previous = Inputs.object(value)
82
+ if previous.key?("target") && previous["target"] != target_key
83
+ Inputs.invalid!("Previously managed state belongs to a different target")
84
+ end
85
+ tags = previous.fetch("tag_names", [])
86
+ unless tags.is_a?(Array) && tags.all? { |key| key.is_a?(String) && key.start_with?("netstack.") }
87
+ Inputs.invalid!("Previously managed tags must belong to Netstack")
88
+ end
89
+ { "target" => target_key, "group_ids" => Inputs.ids(previous.fetch("group_ids", [])),
90
+ "template_ids" => Inputs.ids(previous.fetch("template_ids", [])), "tag_names" => tags.uniq }
91
+ end
92
+
93
+ def references(ids, key) = ids.map { |id| { key => id } }
94
+
95
+ def equivalent?(actual, desired)
96
+ StateComparison.matches?(actual, desired)
97
+ end
98
+
99
+ def refs!(rows, key)
100
+ Inputs.invalid!("Malformed remote reference collection") unless rows.is_a?(Array) && rows.all? { |row| row.is_a?(Hash) && row.key?(key) }
101
+ Inputs.ids(rows.map { |row| row.fetch(key) })
102
+ end
103
+
104
+ def tag_rows!(rows)
105
+ unless rows.is_a?(Array) && rows.all? { |row| row.is_a?(Hash) && row["tag"].is_a?(String) && row["value"].is_a?(String) }
106
+ Inputs.invalid!("Malformed remote tag collection")
107
+ end
108
+ rows.map { |row| row.slice("tag", "value") }
109
+ end
110
+
111
+ def append_interface(operations, host_id, interface, existing)
112
+ attributes = Inputs.object(interface)
113
+ selector = { "hostids" => [host_id], "filter" => { "type" => attributes.fetch("type"), "main" => 1 } }
114
+ current = existing && @client.find("hostinterface", selector)
115
+ if current
116
+ unless current["hostid"].to_s == host_id.to_s && current["type"].to_s == attributes.fetch("type").to_s && current["main"].to_s == "1" && attributes.keys.all? { |key| current.key?(key) }
117
+ Inputs.invalid!("Interface lookup returned incomplete or foreign state")
118
+ end
119
+ before = attributes.keys.to_h { |key| [key, current.fetch(key)] }
120
+ return if equivalent?(before, attributes)
121
+
122
+ before["hostid"] = current.fetch("hostid")
123
+ if before["details"].is_a?(Hash)
124
+ before["details"] = before["details"].except("community", "authpassphrase", "privpassphrase")
125
+ end
126
+
127
+ operations << Mutation.new(key: "interface", resource: "hostinterface", action: "update",
128
+ id: current.fetch("interfaceid"), attributes: attributes, before: before)
129
+ else
130
+ operations << Mutation.new(key: "interface", resource: "hostinterface", action: "create",
131
+ attributes: attributes.merge("hostid" => host_id), selector: selector)
132
+ end
133
+ end
134
+
135
+ def append_macro(operations, host_id, macro, existing)
136
+ attributes = Inputs.object(macro)
137
+ selector = { "hostids" => [host_id], "filter" => { "macro" => attributes.fetch("macro") } }
138
+ current = existing && @client.find("usermacro", selector)
139
+ if current && (current["hostid"].to_s != host_id.to_s || current["macro"] != attributes.fetch("macro") || !current.key?("type"))
140
+ Inputs.invalid!("Macro lookup returned incomplete or foreign state")
141
+ end
142
+ operations << if current
143
+ Mutation.new(key: "macro", resource: "usermacro", action: "update", id: current.fetch("hostmacroid"),
144
+ attributes: attributes, before: current.slice("hostid", "macro", "type"))
145
+ else
146
+ Mutation.new(key: "macro", resource: "usermacro", action: "create",
147
+ attributes: attributes.merge("hostid" => host_id), selector: selector)
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ipaddr"
4
+
5
+ module Netstack
6
+ module Monitoring
7
+ module Inputs
8
+ module_function
9
+
10
+ def object(value)
11
+ invalid!("Expected an object") unless value.is_a?(Hash)
12
+ value.each_with_object({}) do |(key, item), result|
13
+ invalid!("Object keys must be strings or symbols") unless key.is_a?(String) || key.is_a?(Symbol)
14
+ invalid!("Duplicate object key") if result.key?(key.to_s)
15
+ result[key.to_s] = normalize(item)
16
+ end
17
+ end
18
+
19
+ def normalize(value)
20
+ case value
21
+ when Hash then object(value)
22
+ when Array then value.map { |item| normalize(item) }
23
+ when Float
24
+ invalid!("Numbers must be finite") unless value.finite?
25
+ value
26
+ when String, Integer, TrueClass, FalseClass, NilClass then value
27
+ else invalid!("Value must be JSON-compatible")
28
+ end
29
+ end
30
+
31
+ def text(value, name:)
32
+ invalid!("#{name} must be nonempty text") unless value.is_a?(String) && !value.strip.empty? && !value.match?(/[\x00-\x1f\x7f]/)
33
+ value.strip
34
+ end
35
+
36
+ def identifier(value)
37
+ valid = (value.is_a?(Integer) && value.positive?) || (value.is_a?(String) && value.match?(/\A[1-9][0-9]*\z/))
38
+ invalid!("Invalid monitoring identifier") unless valid
39
+ value.to_s
40
+ end
41
+
42
+ def ids(values)
43
+ invalid!("Expected an identifier array") unless values.is_a?(Array)
44
+ values.map { |value| identifier(value) }.uniq
45
+ end
46
+
47
+ def boolean(value)
48
+ invalid!("Expected a boolean") unless [true, false].include?(value)
49
+ value
50
+ end
51
+
52
+ def number(value, positive: false)
53
+ numeric = value.is_a?(Integer) || value.is_a?(Float) || (value.is_a?(String) && value.match?(/\A-?(?:\d+(?:\.\d+)?|\.\d+)(?:[eE][+-]?\d+)?\z/))
54
+ invalid!("Expected a finite number") unless numeric
55
+ number = Float(value)
56
+ invalid!("Expected a finite nonnegative number") unless number.finite? && (positive ? number.positive? : number >= 0)
57
+ number
58
+ end
59
+
60
+ def ip(value)
61
+ value = text(value, name: "IP address")
62
+ invalid!("Expected an IP address without a prefix") if value.include?("/")
63
+ IPAddr.new(value).to_s
64
+ rescue IPAddr::InvalidAddressError
65
+ invalid!("Invalid IP address")
66
+ end
67
+
68
+ def invalid!(message)
69
+ raise Netstack::InputError.new(message, code: "invalid_monitoring_input", stage: "input")
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Netstack
4
+ module Monitoring
5
+ class Measurements
6
+ class Series < Data.define(:item_id, :source, :from, :to, :points, :status)
7
+ def initialize(item_id:, source:, from:, to:, points:, status:)
8
+ super(item_id: item_id.freeze, source: source.freeze, from: from, to: to,
9
+ points: Values.copy(points), status: status.freeze)
10
+ end
11
+ end
12
+
13
+ def initialize(client:)
14
+ @client = client
15
+ end
16
+
17
+ def history(item_id:, value_type:, from:, to:, limit: 10_000)
18
+ item_id = Inputs.identifier(item_id)
19
+ rows = @client.history(item_id: item_id, value_type: value_type, from: from, to: to, limit: limit)
20
+ points = rows.map do |row|
21
+ validate_identity!(row, item_id)
22
+ clock = integer!(row.fetch("clock"))
23
+ ns = integer!(row.fetch("ns", 0))
24
+ Inputs.invalid!("Invalid measurement nanoseconds") unless ns < 1_000_000_000
25
+ { "clock" => clock, "ns" => ns, "value" => Inputs.number(row.fetch("value")) }
26
+ end
27
+ build_series(item_id, "history", from, to, points, limit)
28
+ rescue KeyError
29
+ Inputs.invalid!("Measurement row is incomplete")
30
+ end
31
+
32
+ def trends(item_id:, from:, to:, limit: 10_000)
33
+ item_id = Inputs.identifier(item_id)
34
+ rows = @client.trends(item_id: item_id, from: from, to: to, limit: limit)
35
+ points = rows.map do |row|
36
+ validate_identity!(row, item_id)
37
+ count = integer!(row.fetch("num"))
38
+ Inputs.invalid!("Trend row must contain actual samples") unless count.positive?
39
+ minimum = Inputs.number(row.fetch("value_min"))
40
+ average = Inputs.number(row.fetch("value_avg"))
41
+ maximum = Inputs.number(row.fetch("value_max"))
42
+ Inputs.invalid!("Invalid trend bounds") unless minimum <= average && average <= maximum
43
+ { "clock" => integer!(row.fetch("clock")), "ns" => 0, "count" => count,
44
+ "minimum" => minimum, "average" => average, "maximum" => maximum }
45
+ end
46
+ build_series(item_id, "trend", from, to, points, limit)
47
+ rescue KeyError
48
+ Inputs.invalid!("Trend row is incomplete")
49
+ end
50
+
51
+ # Exact timestamp joins: missing direction stays nil. Trend averages are
52
+ # not traffic samples and cannot be used to claim a raw-sample percentile.
53
+ def self.align(inbound:, outbound:)
54
+ Inputs.invalid!("Alignment requires raw history series") unless [inbound, outbound].all? { |series| series.is_a?(Series) && series.source == "history" }
55
+ Inputs.invalid!("Measurement intervals must match") unless inbound.from == outbound.from && inbound.to == outbound.to
56
+ indexed = [inbound, outbound].map do |series|
57
+ series.points.to_h { |point| [[point.fetch("clock"), point.fetch("ns")], point.fetch("value")] }
58
+ end
59
+ Values.copy((indexed[0].keys | indexed[1].keys).sort.map do |clock, ns|
60
+ { "clock" => clock, "ns" => ns, "in_bps" => indexed[0][[clock, ns]], "out_bps" => indexed[1][[clock, ns]] }
61
+ end)
62
+ end
63
+
64
+ private
65
+
66
+ def validate_identity!(row, item_id)
67
+ Inputs.invalid!("Measurement belongs to a different item") unless row.is_a?(Hash) && Inputs.identifier(row["itemid"]) == item_id
68
+ end
69
+
70
+ def integer!(value)
71
+ number = value.is_a?(Integer) ? value : (Integer(value, 10) if value.is_a?(String) && value.match?(/\A\d+\z/))
72
+ Inputs.invalid!("Invalid measurement timestamp or count") unless number && number >= 0
73
+ number
74
+ end
75
+
76
+ def build_series(item_id, source, from, to, points, limit)
77
+ Inputs.invalid!("Measurement falls outside the requested interval") unless points.all? { |point| (from..to).cover?(point["clock"]) }
78
+ keys = points.map { |point| [point["clock"], point["ns"]] }
79
+ Inputs.invalid!("Duplicate measurement timestamp") unless keys.uniq.size == keys.size
80
+ status = if points.size >= limit then "truncated"
81
+ elsif points.empty? then "empty"
82
+ else "available"
83
+ end
84
+ Series.new(item_id: item_id, source: source, from: from, to: to,
85
+ points: points.sort_by { |point| [point["clock"], point["ns"]] }, status: status)
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Netstack
4
+ module Monitoring
5
+ class Mutation
6
+ attr_reader :key, :resource, :action, :attributes, :id, :before, :selector
7
+
8
+ def initialize(key:, resource:, action:, attributes:, id: nil, before: {}, selector: {})
9
+ @key = Inputs.text(key, name: "operation key").freeze
10
+ @resource = resource.to_s.freeze
11
+ Inputs.invalid!("Unsupported monitoring resource") unless %w[host hostinterface usermacro item trigger].include?(@resource)
12
+ @action = action.to_s.freeze
13
+ Inputs.invalid!("Unsupported monitoring action") unless %w[create update].include?(@action)
14
+ @attributes = Values.copy(Inputs.object(attributes))
15
+ @before = Values.copy(Inputs.object(before))
16
+ @selector = Values.copy(Inputs.object(selector))
17
+ @id = if id.nil?
18
+ nil
19
+ else
20
+ Values.copy(id.is_a?(Hash) ? Inputs.object(id) : Inputs.identifier(id))
21
+ end
22
+ Inputs.invalid!("Update requires a resource ID") if @action == "update" && @id.nil?
23
+ Inputs.invalid!("Create requires a recovery selector") if @action == "create" && @selector.empty?
24
+ if @resource == "usermacro" && @attributes.key?("value")
25
+ secret = @attributes["value"]
26
+ Inputs.invalid!("Macro values must use a secret reference") unless secret.is_a?(Hash) && secret.keys == ["$secret"] && secret["$secret"].is_a?(String)
27
+ end
28
+ freeze
29
+ end
30
+
31
+ def to_h
32
+ { "key" => key, "resource" => resource, "action" => action, "attributes" => attributes,
33
+ "id" => id, "before" => before, "selector" => selector }
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "digest"
5
+
6
+ module Netstack
7
+ # 监控意图与确认;提供方 RPC 细节由集成模块管理。
8
+ module Monitoring
9
+ # 不可变的有序变更;引用只能指向更早的操作。
10
+ # 秘密通过解析器引用传递;受管理资源的归属由宿主提供。
11
+ class Plan
12
+ attr_reader :key, :operations, :managed, :digest
13
+
14
+ def initialize(key:, operations:, managed: {})
15
+ @key = Inputs.text(key, name: "plan key").freeze
16
+ Inputs.invalid!("Expected monitoring mutations") unless operations.is_a?(Array) && operations.all? { |item| item.is_a?(Mutation) }
17
+ known = []
18
+ operations.each do |operation|
19
+ Inputs.invalid!("Duplicate operation key") if known.include?(operation.key)
20
+ validate_references!(operation.to_h, known)
21
+ known << operation.key
22
+ end
23
+ @operations = operations.dup.freeze
24
+ @managed = Values.copy(Inputs.object(managed))
25
+ @digest = Digest::SHA256.hexdigest(JSON.generate(canonical(to_h))).freeze
26
+ freeze
27
+ end
28
+
29
+ def to_h
30
+ { "key" => key, "operations" => operations.map(&:to_h), "managed" => managed }
31
+ end
32
+
33
+ def self.from_h(value)
34
+ value = Inputs.object(value)
35
+ operations = value.fetch("operations").map do |operation|
36
+ Mutation.new(**operation.transform_keys(&:to_sym))
37
+ end
38
+ new(key: value.fetch("key"), operations: operations, managed: value.fetch("managed", {}))
39
+ rescue KeyError, NoMethodError, ArgumentError
40
+ Inputs.invalid!("Invalid persisted monitoring plan")
41
+ end
42
+
43
+ private
44
+
45
+ def validate_references!(value, known)
46
+ case value
47
+ when Hash
48
+ if value.key?("$ref")
49
+ Inputs.invalid!("Reference must identify an earlier operation") unless value.keys == ["$ref"] && known.include?(value["$ref"])
50
+ elsif value.key?("$secret")
51
+ Inputs.invalid!("Invalid secret reference") unless value.keys == ["$secret"] && value["$secret"].is_a?(String) && !value["$secret"].empty?
52
+ else
53
+ value.each_value { |item| validate_references!(item, known) }
54
+ end
55
+ when Array
56
+ value.each { |item| validate_references!(item, known) }
57
+ end
58
+ end
59
+
60
+ def canonical(value)
61
+ case value
62
+ when Hash then value.keys.sort.to_h { |key| [key, canonical(value[key])] }
63
+ when Array then value.map { |item| canonical(item) }
64
+ else value
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Netstack
4
+ module Monitoring
5
+ class ProbePlan
6
+ def initialize(client:)
7
+ @hosts = HostPlan.new(client: client)
8
+ end
9
+
10
+ def call(target:, groups:, templates:, proxy_group_id: nil, name: nil, enabled: true,
11
+ inventory: {}, tags: {}, previous_managed: {})
12
+ Inputs.invalid!("An ICMP probe template is required") if Inputs.ids(templates).empty?
13
+ @hosts.call(target: target, kind: "probe", groups: groups, templates: templates, proxy_group_id: proxy_group_id,
14
+ name: name, enabled: enabled, inventory: inventory, tags: tags, previous_managed: previous_managed,
15
+ interface: { "type" => 1, "main" => 1, "useip" => 1, "ip" => Inputs.ip(target.host), "dns" => "", "port" => "10050" })
16
+ end
17
+ end
18
+ end
19
+ end