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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +22 -3
- data/{LICENSE.txt → LICENSE} +7 -6
- data/NOTICE +25 -0
- data/README.md +58 -20
- data/docs/acceptance.md +92 -0
- data/docs/backups.md +100 -0
- data/docs/configuration.md +198 -0
- data/docs/connector.md +187 -0
- data/docs/ddi.md +142 -0
- data/docs/deployment.md +47 -0
- data/docs/foundations.md +45 -0
- data/docs/implementation.md +68 -0
- data/docs/inventory.md +42 -0
- data/docs/monitoring.md +227 -0
- data/docs/motor_integration.md +96 -0
- data/docs/performance.md +39 -0
- data/docs/release.md +20 -0
- data/docs/vxlan.md +91 -0
- data/docs/wireless.md +81 -0
- data/lib/netstack/backups/artifact.rb +23 -0
- data/lib/netstack/backups/diff.rb +15 -0
- data/lib/netstack/backups/repository.rb +77 -0
- data/lib/netstack/backups/request.rb +78 -0
- data/lib/netstack/circuits/business_calendar.rb +51 -0
- data/lib/netstack/circuits/capacity_review.rb +191 -0
- data/lib/netstack/circuits/endpoint.rb +25 -0
- data/lib/netstack/circuits/monitoring_plan.rb +212 -0
- data/lib/netstack/configuration/artifact.rb +85 -0
- data/lib/netstack/configuration/artifact_resolver.rb +30 -0
- data/lib/netstack/configuration/bootstrap/payload.rb +67 -0
- data/lib/netstack/configuration/bootstrap/validator.rb +224 -0
- data/lib/netstack/configuration/catalog.rb +60 -0
- data/lib/netstack/configuration/generator.rb +87 -0
- data/lib/netstack/configuration/input_contract.rb +82 -0
- data/lib/netstack/configuration/payload.rb +168 -0
- data/lib/netstack/configuration/reader.rb +124 -0
- data/lib/netstack/configuration/support.rb +30 -0
- data/lib/netstack/configuration/system_configuration.rb +56 -0
- data/lib/netstack/configuration/template_manager.rb +174 -0
- data/lib/netstack/configuration/template_store.rb +40 -0
- data/lib/netstack/configuration/validator.rb +566 -0
- data/lib/netstack/connector/authentication.rb +77 -0
- data/lib/netstack/connector/authentication_error.rb +11 -0
- data/lib/netstack/connector/backup.rb +19 -0
- data/lib/netstack/connector/backup_busy.rb +11 -0
- data/lib/netstack/connector/backup_persistence_error.rb +32 -0
- data/lib/netstack/connector/command.rb +63 -0
- data/lib/netstack/connector/command_result.rb +24 -0
- data/lib/netstack/connector/command_timeout.rb +11 -0
- data/lib/netstack/connector/configuration.rb +138 -0
- data/lib/netstack/connector/connection_closed.rb +11 -0
- data/lib/netstack/connector/connection_error.rb +11 -0
- data/lib/netstack/connector/device.rb +284 -0
- data/lib/netstack/connector/device_error.rb +11 -0
- data/lib/netstack/connector/dialogue.rb +55 -0
- data/lib/netstack/connector/error.rb +44 -0
- data/lib/netstack/connector/error_metadata.rb +58 -0
- data/lib/netstack/connector/event.rb +24 -0
- data/lib/netstack/connector/execution.rb +110 -0
- data/lib/netstack/connector/interaction.rb +55 -0
- data/lib/netstack/connector/internal_error.rb +11 -0
- data/lib/netstack/connector/known_hosts.rb +128 -0
- data/lib/netstack/connector/local_backup.rb +63 -0
- data/lib/netstack/connector/log/event.rb +65 -0
- data/lib/netstack/connector/log/formatter.rb +18 -0
- data/lib/netstack/connector/log/messages.rb +54 -0
- data/lib/netstack/connector/log/redacting_writer.rb +33 -0
- data/lib/netstack/connector/log/transcript.rb +35 -0
- data/lib/netstack/connector/log.rb +262 -0
- data/lib/netstack/connector/log_error.rb +11 -0
- data/lib/netstack/connector/login_timeout.rb +11 -0
- data/lib/netstack/connector/output_limit_exceeded.rb +11 -0
- data/lib/netstack/connector/parsing_error.rb +11 -0
- data/lib/netstack/connector/profile/builder.rb +238 -0
- data/lib/netstack/connector/profile.rb +225 -0
- data/lib/netstack/connector/prompt_error.rb +11 -0
- data/lib/netstack/connector/recovery.rb +59 -0
- data/lib/netstack/connector/redactor.rb +100 -0
- data/lib/netstack/connector/response.rb +25 -0
- data/lib/netstack/connector/response_reader.rb +140 -0
- data/lib/netstack/connector/result.rb +50 -0
- data/lib/netstack/connector/running_config/rendered.rb +16 -0
- data/lib/netstack/connector/running_config/strategy.rb +28 -0
- data/lib/netstack/connector/running_config.rb +113 -0
- data/lib/netstack/connector/save_config.rb +23 -0
- data/lib/netstack/connector/script.rb +59 -0
- data/lib/netstack/connector/script_error.rb +11 -0
- data/lib/netstack/connector/script_output_limit_exceeded.rb +11 -0
- data/lib/netstack/connector/session.rb +414 -0
- data/lib/netstack/connector/session_busy.rb +11 -0
- data/lib/netstack/connector/storage/backup_lock.rb +117 -0
- data/lib/netstack/connector/storage/private_file.rb +111 -0
- data/lib/netstack/connector/storage/safe_file.rb +64 -0
- data/lib/netstack/connector/storage/saved_config.rb +76 -0
- data/lib/netstack/connector/terminal_renderer.rb +186 -0
- data/lib/netstack/connector/terminal_text.rb +32 -0
- data/lib/netstack/connector/textfsm.rb +71 -0
- data/lib/netstack/connector/tftp/file_upload.rb +53 -0
- data/lib/netstack/connector/tftp/strategy.rb +67 -0
- data/lib/netstack/connector/tftp.rb +116 -0
- data/lib/netstack/connector/tftp_completion_error.rb +36 -0
- data/lib/netstack/connector/tftp_receipt.rb +54 -0
- data/lib/netstack/connector/tftp_target.rb +65 -0
- data/lib/netstack/connector/topology/checkpoint_error.rb +16 -0
- data/lib/netstack/connector/topology/deployment_adapter.rb +163 -0
- data/lib/netstack/connector/topology/execution_result.rb +34 -0
- data/lib/netstack/connector/topology/immediate_strategy.rb +39 -0
- data/lib/netstack/connector/topology/interface_description.rb +46 -0
- data/lib/netstack/connector/topology/interface_name.rb +60 -0
- data/lib/netstack/connector/topology/plan.rb +105 -0
- data/lib/netstack/connector/topology/strategy.rb +76 -0
- data/lib/netstack/connector/topology.rb +255 -0
- data/lib/netstack/connector/transport_error.rb +11 -0
- data/lib/netstack/connector/transports.rb +201 -0
- data/lib/netstack/connector/underlying_error.rb +25 -0
- data/lib/netstack/connector/unsupported_operation.rb +11 -0
- data/lib/netstack/connector/vendors/cisco_ios/running_config.rb +22 -0
- data/lib/netstack/connector/vendors/cisco_ios/tftp_backup.rb +41 -0
- data/lib/netstack/connector/vendors/cisco_ios/topology.rb +54 -0
- data/lib/netstack/connector/vendors/cisco_ios.rb +44 -0
- data/lib/netstack/connector/vendors/cisco_nxos/running_config.rb +25 -0
- data/lib/netstack/connector/vendors/cisco_nxos/tftp_backup.rb +43 -0
- data/lib/netstack/connector/vendors/cisco_nxos.rb +51 -0
- data/lib/netstack/connector/vendors/h3c/tftp_backup.rb +34 -0
- data/lib/netstack/connector/vendors/h3c/topology.rb +72 -0
- data/lib/netstack/connector/vendors/h3c.rb +75 -0
- data/lib/netstack/connector/vendors/h3c_wireless.rb +13 -0
- data/lib/netstack/connector/vendors/hillstone/running_config.rb +16 -0
- data/lib/netstack/connector/vendors/hillstone/tftp_backup.rb +55 -0
- data/lib/netstack/connector/vendors/hillstone/topology.rb +64 -0
- data/lib/netstack/connector/vendors/hillstone.rb +39 -0
- data/lib/netstack/connector/vendors/huawei/tftp_backup.rb +26 -0
- data/lib/netstack/connector/vendors/huawei.rb +62 -0
- data/lib/netstack/connector/vendors/palo_alto/running_config.rb +77 -0
- data/lib/netstack/connector/vendors/palo_alto/tftp_backup.rb +49 -0
- data/lib/netstack/connector/vendors/palo_alto/topology.rb +77 -0
- data/lib/netstack/connector/vendors/palo_alto.rb +39 -0
- data/lib/netstack/connector/vendors/radware/running_config.rb +21 -0
- data/lib/netstack/connector/vendors/radware/tftp_backup.rb +49 -0
- data/lib/netstack/connector/vendors/radware/topology.rb +19 -0
- data/lib/netstack/connector/vendors/radware.rb +59 -0
- data/lib/netstack/connector/write_timeout.rb +11 -0
- data/lib/netstack/connector.rb +32 -0
- data/lib/netstack/ddi/applier.rb +107 -0
- data/lib/netstack/ddi/deployment.rb +137 -0
- data/lib/netstack/ddi/inputs.rb +61 -0
- data/lib/netstack/ddi/manager.rb +197 -0
- data/lib/netstack/ddi/plan.rb +99 -0
- data/lib/netstack/ddi/scope.rb +127 -0
- data/lib/netstack/ddi/settings.rb +44 -0
- data/lib/netstack/ddi/state.rb +166 -0
- data/lib/netstack/deployment/checkpoint_error.rb +14 -0
- data/lib/netstack/deployment/cli_adapter.rb +64 -0
- data/lib/netstack/deployment/executor.rb +131 -0
- data/lib/netstack/deployment/plan.rb +82 -0
- data/lib/netstack/deployment/receipt.rb +78 -0
- data/lib/netstack/deployment/recovery.rb +19 -0
- data/lib/netstack/deployment/step.rb +16 -0
- data/lib/netstack/dimension.rb +30 -0
- data/lib/netstack/error.rb +14 -0
- data/lib/netstack/input_error.rb +9 -0
- data/lib/netstack/integrations/bluecat/client.rb +84 -0
- data/lib/netstack/integrations/http/client.rb +146 -0
- data/lib/netstack/integrations/http/error.rb +10 -0
- data/lib/netstack/integrations/http/response.rb +28 -0
- data/lib/netstack/integrations/http_delivery/adapter.rb +51 -0
- data/lib/netstack/integrations/http_delivery/client.rb +127 -0
- data/lib/netstack/integrations/http_delivery/result.rb +27 -0
- data/lib/netstack/integrations/oxidized/client.rb +89 -0
- data/lib/netstack/integrations/zabbix/client.rb +211 -0
- data/lib/netstack/integrations/zabbix/error.rb +10 -0
- data/lib/netstack/inventory/address.rb +30 -0
- data/lib/netstack/inventory/changes.rb +13 -0
- data/lib/netstack/inventory/cli_parser.rb +75 -0
- data/lib/netstack/inventory/collector.rb +99 -0
- data/lib/netstack/inventory/device.rb +14 -0
- data/lib/netstack/inventory/interface.rb +25 -0
- data/lib/netstack/inventory/neighbor.rb +15 -0
- data/lib/netstack/inventory/reconciler.rb +74 -0
- data/lib/netstack/inventory/snapshot.rb +69 -0
- data/lib/netstack/inventory/transceiver.rb +23 -0
- data/lib/netstack/ipv4.rb +204 -0
- data/lib/netstack/ipv6.rb +88 -0
- data/lib/netstack/monitoring/applier.rb +179 -0
- data/lib/netstack/monitoring/device_plan.rb +23 -0
- data/lib/netstack/monitoring/host_plan.rb +152 -0
- data/lib/netstack/monitoring/inputs.rb +73 -0
- data/lib/netstack/monitoring/measurements.rb +89 -0
- data/lib/netstack/monitoring/mutation.rb +37 -0
- data/lib/netstack/monitoring/plan.rb +69 -0
- data/lib/netstack/monitoring/probe_plan.rb +19 -0
- data/lib/netstack/monitoring/state_comparison.rb +37 -0
- data/lib/netstack/range_set.rb +181 -0
- data/lib/netstack/routing/lookup.rb +46 -0
- data/lib/netstack/routing/lookup_result.rb +14 -0
- data/lib/netstack/routing/route.rb +32 -0
- data/lib/netstack/routing/snapshot.rb +23 -0
- data/lib/netstack/target.rb +15 -0
- data/lib/netstack/unsupported_capability.rb +9 -0
- data/lib/netstack/values.rb +70 -0
- data/lib/netstack/version.rb +1 -1
- data/lib/netstack/vxlan/auditor.rb +187 -0
- data/lib/netstack/vxlan/cisco_nxos/config_parser.rb +327 -0
- data/lib/netstack/vxlan/cisco_nxos/deployment_adapter.rb +63 -0
- data/lib/netstack/vxlan/cisco_nxos/runtime_parser.rb +182 -0
- data/lib/netstack/vxlan/cisco_nxos/segment_renderer.rb +42 -0
- data/lib/netstack/vxlan/collector.rb +77 -0
- data/lib/netstack/vxlan/fabric.rb +29 -0
- data/lib/netstack/vxlan/finding.rb +20 -0
- data/lib/netstack/vxlan/node.rb +51 -0
- data/lib/netstack/vxlan/route_policy.rb +57 -0
- data/lib/netstack/vxlan/runtime_result.rb +24 -0
- data/lib/netstack/vxlan/segment.rb +35 -0
- data/lib/netstack/vxlan/segment_plan.rb +25 -0
- data/lib/netstack/vxlan/segment_planner.rb +156 -0
- data/lib/netstack/vxlan/snapshot.rb +34 -0
- data/lib/netstack/vxlan/validation.rb +58 -0
- data/lib/netstack/vxlan/verifier.rb +52 -0
- data/lib/netstack/vxlan/vpc_domain.rb +20 -0
- data/lib/netstack/wireless/access_point.rb +12 -0
- data/lib/netstack/wireless/ap_group.rb +12 -0
- data/lib/netstack/wireless/assessment.rb +13 -0
- data/lib/netstack/wireless/assessor.rb +83 -0
- data/lib/netstack/wireless/authentication_connection.rb +12 -0
- data/lib/netstack/wireless/bss.rb +12 -0
- data/lib/netstack/wireless/client.rb +12 -0
- data/lib/netstack/wireless/collector.rb +191 -0
- data/lib/netstack/wireless/configuration.rb +13 -0
- data/lib/netstack/wireless/finding.rb +12 -0
- data/lib/netstack/wireless/h3c/config_parser.rb +229 -0
- data/lib/netstack/wireless/h3c/lldp_parser.rb +69 -0
- data/lib/netstack/wireless/h3c/runtime_parser.rb +240 -0
- data/lib/netstack/wireless/huawei/runtime_parser.rb +83 -0
- data/lib/netstack/wireless/invalid_snapshot.rb +11 -0
- data/lib/netstack/wireless/neighbor.rb +12 -0
- data/lib/netstack/wireless/normalizer.rb +90 -0
- data/lib/netstack/wireless/parse_result.rb +13 -0
- data/lib/netstack/wireless/radio.rb +12 -0
- data/lib/netstack/wireless/reconciler.rb +115 -0
- data/lib/netstack/wireless/reconciliation.rb +11 -0
- data/lib/netstack/wireless/security_policy.rb +12 -0
- data/lib/netstack/wireless/service_template.rb +12 -0
- data/lib/netstack/wireless/service_template_bindings.rb +56 -0
- data/lib/netstack/wireless/snapshot.rb +150 -0
- data/lib/netstack/wireless/state.rb +23 -0
- data/lib/netstack/wireless/view.rb +154 -0
- data/lib/netstack.rb +10 -3
- data/resources/configuration/cisco-bootstrap.erb +118 -0
- data/resources/configuration/cisco-distributed-gateway.erb +40 -0
- data/resources/configuration/cisco-n9k-vpc.erb +62 -0
- data/resources/configuration/cisco-server-port.erb +43 -0
- data/resources/configuration/h3c-bootstrap.erb +108 -0
- data/resources/configuration/h3c-irf.erb +57 -0
- data/resources/configuration/h3c-server-port.erb +36 -0
- data/resources/configuration/hillstone-ipsec-vpn.erb +68 -0
- data/resources/configuration/hillstone-policy.erb +31 -0
- data/resources/configuration/paloalto-ipsec-vpn.erb +39 -0
- data/resources/configuration/radware-load-balancer.erb +26 -0
- data/resources/textfsm/cisco_cdp_neighbors_detail.textfsm +7 -0
- data/resources/textfsm/cisco_ios_running_config_interfaces.textfsm +12 -0
- data/resources/textfsm/cisco_ios_show_ip_interface_brief.textfsm +7 -0
- data/resources/textfsm/h3c_interface_descriptions.textfsm +11 -0
- data/resources/textfsm/h3c_lldp_local_first.textfsm +8 -0
- data/resources/textfsm/h3c_lldp_name_first.textfsm +8 -0
- data/resources/textfsm/hillstone_interface_descriptions.textfsm +11 -0
- data/resources/textfsm/hillstone_lldp_neighbors.textfsm +7 -0
- data/resources/textfsm/index +8 -0
- data/resources/textfsm/palo_alto_interface_descriptions.textfsm +6 -0
- data/resources/textfsm/palo_alto_lldp_neighbors.textfsm +11 -0
- data/resources/textfsm/radware_port_names.textfsm +13 -0
- metadata +588 -27
- data/.rubocop.yml +0 -13
- data/CODE_OF_CONDUCT.md +0 -84
- data/Gemfile +0 -10
- data/Gemfile.lock +0 -40
- data/Rakefile +0 -8
- data/bin/console +0 -15
- 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
|