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,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module Netstack
|
|
7
|
+
module Integrations
|
|
8
|
+
module HttpDelivery
|
|
9
|
+
# Motor-compatible POST fields, plus an explicit key-based recovery endpoint.
|
|
10
|
+
class Client
|
|
11
|
+
def initialize(http:, lookup_path: "", lookup_parameter: "idempotency_key")
|
|
12
|
+
@http = http
|
|
13
|
+
@lookup_path = lookup_path == "" ? "" : Values.text(lookup_path, name: "lookup path", optional: true) || ""
|
|
14
|
+
@lookup_parameter = Values.text(lookup_parameter, name: "lookup parameter")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def inspect = "#<#{self.class.name}>"
|
|
18
|
+
|
|
19
|
+
def submit(target:, artifact:, credentials:, idempotency_key:, request_no: nil, target_id: nil)
|
|
20
|
+
key = validate_key!(idempotency_key)
|
|
21
|
+
request_no = Values.text(request_no, name: "request number") unless request_no.nil?
|
|
22
|
+
validate_identifier!(target_id, name: "target id") unless target_id.nil?
|
|
23
|
+
unless artifact.is_a?(Configuration::Artifact) && artifact.executable?
|
|
24
|
+
raise InputError, "delivery requires a single-target artifact"
|
|
25
|
+
end
|
|
26
|
+
artifact.validate_target!(target)
|
|
27
|
+
auth = validate_credentials!(credentials)
|
|
28
|
+
device = device_snapshot(target)
|
|
29
|
+
response = @http.request(:post, "", headers: { "Accept" => "application/json", "Idempotency-Key" => key }, json: {
|
|
30
|
+
request_no: request_no, target_id: target_id || target.key, idempotency_key: key,
|
|
31
|
+
device: device, credentials: auth, script: artifact.content
|
|
32
|
+
})
|
|
33
|
+
data = response.json
|
|
34
|
+
if data.is_a?(Hash) && data.key?("idempotency_key") && data["idempotency_key"] != key
|
|
35
|
+
raise Error.new("delivery submission returned a different operation", code: :delivery_identity_mismatch, stage: :submit)
|
|
36
|
+
end
|
|
37
|
+
normalize(data)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def status(remote_id: nil, idempotency_key:)
|
|
41
|
+
key = validate_key!(idempotency_key)
|
|
42
|
+
remote_id = validate_identifier!(remote_id, name: "remote id") unless remote_id.nil?
|
|
43
|
+
response = if remote_id
|
|
44
|
+
id = Values.text(remote_id.to_s, name: "remote id")
|
|
45
|
+
raise InputError, "delivery task identifier cannot be a path dot segment" if %w[. ..].include?(id)
|
|
46
|
+
@http.request(:get, URI.encode_uri_component(id), headers: { "Accept" => "application/json" })
|
|
47
|
+
else
|
|
48
|
+
@http.request(:get, @lookup_path, query: { @lookup_parameter => key }, headers: { "Accept" => "application/json" })
|
|
49
|
+
end
|
|
50
|
+
data = response.json
|
|
51
|
+
if data.is_a?(Hash) && ((!remote_id && data["idempotency_key"] != key) || (data.key?("idempotency_key") && data["idempotency_key"] != key))
|
|
52
|
+
raise Error.new("delivery lookup returned a different operation", code: :delivery_identity_mismatch, stage: :verify)
|
|
53
|
+
end
|
|
54
|
+
result = normalize(data, fallback_id: remote_id)
|
|
55
|
+
if remote_id && result.remote_id != remote_id.to_s
|
|
56
|
+
raise Error.new("delivery lookup returned a different remote task", code: :delivery_identity_mismatch, stage: :verify)
|
|
57
|
+
end
|
|
58
|
+
result
|
|
59
|
+
rescue Http::Error => error
|
|
60
|
+
# A missing index record cannot establish non-execution after an uncertain POST.
|
|
61
|
+
return Result.new(status: :not_found) if error.code.to_s == "http_status" && error.details["status"] == 404
|
|
62
|
+
|
|
63
|
+
raise
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def validate_credentials!(credentials)
|
|
67
|
+
auth = Configuration::Support.object(credentials, name: "delivery credentials")
|
|
68
|
+
unless auth.keys.sort == %w[password username] && auth.values.all? { |value| value.is_a?(String) && !value.empty? }
|
|
69
|
+
raise InputError, "delivery credentials require username and password"
|
|
70
|
+
end
|
|
71
|
+
auth
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def normalize(data, fallback_id: nil)
|
|
77
|
+
raise Error.new("invalid delivery response", code: :invalid_delivery_response, stage: :response) unless data.is_a?(Hash)
|
|
78
|
+
|
|
79
|
+
status = case data["status"]&.to_s&.downcase
|
|
80
|
+
when "success", "succeeded", "completed" then :succeeded
|
|
81
|
+
when "failed", "error", "rejected" then :failed
|
|
82
|
+
when "accepted", "pending", "queued", "running", "processing" then :running
|
|
83
|
+
when nil, ""
|
|
84
|
+
if data["success"] == true then :succeeded
|
|
85
|
+
elsif data["success"] == false then :failed
|
|
86
|
+
else :unknown
|
|
87
|
+
end
|
|
88
|
+
else :unknown
|
|
89
|
+
end
|
|
90
|
+
task_id = validate_identifier!(data["task_id"], name: "task id") unless data["task_id"].nil?
|
|
91
|
+
identifier = validate_identifier!(data["id"], name: "task id") unless data["id"].nil?
|
|
92
|
+
if task_id && identifier && task_id != identifier
|
|
93
|
+
raise Error.new("conflicting delivery task identifiers", code: :delivery_identity_mismatch, stage: :response)
|
|
94
|
+
end
|
|
95
|
+
remote_id = task_id || identifier || fallback_id
|
|
96
|
+
if %w[. ..].include?(remote_id)
|
|
97
|
+
raise Error.new("invalid delivery task identifier", code: :invalid_delivery_response, stage: :response)
|
|
98
|
+
end
|
|
99
|
+
output = data["output"] || data["result"]
|
|
100
|
+
output = JSON.generate(output) unless output.nil? || output.is_a?(String)
|
|
101
|
+
Result.new(status: status, remote_id: remote_id, output: output)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def validate_identifier!(value, name:)
|
|
105
|
+
unless value.is_a?(String) || value.is_a?(Integer)
|
|
106
|
+
raise InputError, "#{name} must be text or an integer"
|
|
107
|
+
end
|
|
108
|
+
Values.text(value.to_s, name: name)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def validate_key!(key)
|
|
112
|
+
unless key.is_a?(String) && key.match?(/\A[a-zA-Z0-9_.:-]{1,256}\z/)
|
|
113
|
+
raise InputError, "invalid delivery idempotency key"
|
|
114
|
+
end
|
|
115
|
+
key
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def device_snapshot(target)
|
|
119
|
+
allowed = %w[id serial name hostname vendor model os device_role source]
|
|
120
|
+
supplied = Configuration::Support.object(target.attributes, name: "target attributes")
|
|
121
|
+
supplied.slice(*allowed).merge("id" => supplied.fetch("id", target.key), "name" => supplied.fetch("name", target.key),
|
|
122
|
+
"ip" => target.host, "vendor" => supplied.fetch("vendor", target.platform))
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Integrations
|
|
5
|
+
module HttpDelivery
|
|
6
|
+
class Result
|
|
7
|
+
STATUSES = %i[succeeded failed running unknown not_found].freeze
|
|
8
|
+
attr_reader :status, :remote_id
|
|
9
|
+
|
|
10
|
+
def initialize(status:, remote_id: nil, output: nil)
|
|
11
|
+
raise InputError, "invalid delivery result status" unless STATUSES.include?(status)
|
|
12
|
+
raise InputError, "delivery output must be text" unless output.nil? || output.is_a?(String)
|
|
13
|
+
|
|
14
|
+
@status = status
|
|
15
|
+
@remote_id = remote_id.nil? ? nil : Values.text(remote_id.to_s, name: "remote id")
|
|
16
|
+
@output = output&.dup&.freeze
|
|
17
|
+
freeze
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def output = @output
|
|
21
|
+
def succeeded? = status == :succeeded
|
|
22
|
+
def failed? = status == :failed
|
|
23
|
+
def inspect = "#<#{self.class.name} status=#{status}>"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Netstack
|
|
7
|
+
module Integrations
|
|
8
|
+
module Oxidized
|
|
9
|
+
# Oxidized Web's queue endpoint is a side-effectful GET. HTTP never retries it.
|
|
10
|
+
class Client
|
|
11
|
+
def initialize(base_url: nil, http: nil, **http_options)
|
|
12
|
+
@http = http || Http::Client.new(base_url: base_url, **http_options)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def inspect = "#<#{self.class.name}>"
|
|
16
|
+
|
|
17
|
+
def nodes
|
|
18
|
+
rows = @http.request(:get, "/nodes.json").json
|
|
19
|
+
invalid! unless rows.is_a?(Array) && rows.all? { |row| row.is_a?(Hash) && row["name"].is_a?(String) && !row["name"].empty? }
|
|
20
|
+
rows = rows.map do |row|
|
|
21
|
+
next row unless row["last"]
|
|
22
|
+
invalid! unless row["last"].is_a?(Hash)
|
|
23
|
+
last = row["last"].dup
|
|
24
|
+
%w[start end].each { |key| last[key] = timestamp(last[key]) if last[key] }
|
|
25
|
+
row.merge("last" => last)
|
|
26
|
+
end
|
|
27
|
+
Values.copy(rows)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def configuration(node_name:, group: nil)
|
|
31
|
+
@http.request(:get, node_path("/node/fetch", node_name, group), headers: { "Accept" => "text/plain" }).body
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def request_backup(node_name:, group: nil) # rubocop:disable Naming/PredicateMethod -- command, not a predicate
|
|
35
|
+
result = @http.request(:get, "#{node_path("/node/next", node_name, group)}.json").json
|
|
36
|
+
invalid! unless result == ["ok"] || result == "ok"
|
|
37
|
+
true
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def versions(node_name:, group: nil)
|
|
41
|
+
node_name = segment(node_name)
|
|
42
|
+
group = segment(group) unless group.nil?
|
|
43
|
+
rows = @http.request(:get, "/node/version.json", query: { node_full: [group, node_name].compact.join("/") }).json
|
|
44
|
+
invalid! unless rows.is_a?(Array) && rows.all? { |row| row.is_a?(Hash) && row["oid"].is_a?(String) && !row["oid"].empty? }
|
|
45
|
+
invalid! unless rows.map { |row| row["oid"] }.uniq.length == rows.length
|
|
46
|
+
Values.copy(rows)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def diff(node_name:, group: nil, from_oid:, to_oid:)
|
|
50
|
+
rows = @http.request(:get, "/node/version/diffs", query: {
|
|
51
|
+
format: "json", node: segment(node_name), group: group.nil? ? "" : segment(group),
|
|
52
|
+
oid: Values.text(from_oid, name: "from_oid"), oid2: Values.text(to_oid, name: "to_oid"), epoch: 0, num: 0
|
|
53
|
+
}).json
|
|
54
|
+
invalid! unless rows.is_a?(Array) && rows.all? { |row| row.is_a?(String) }
|
|
55
|
+
Values.copy(rows)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
# Upstream JSON.generate serializes Time objects via Time#to_s (UTC).
|
|
61
|
+
def timestamp(value)
|
|
62
|
+
invalid! unless value.is_a?(String)
|
|
63
|
+
time = if value.match?(/\A\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)? UTC\z/)
|
|
64
|
+
Time.iso8601(value.sub(" ", "T").sub(" UTC", "Z"))
|
|
65
|
+
else
|
|
66
|
+
Time.iso8601(value)
|
|
67
|
+
end
|
|
68
|
+
time.iso8601(6)
|
|
69
|
+
rescue ArgumentError
|
|
70
|
+
invalid!
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def node_path(prefix, name, group)
|
|
74
|
+
[prefix, *[group, name].compact.map { |value| URI.encode_www_form_component(segment(value)).gsub("+", "%20") }].join("/")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def segment(value)
|
|
78
|
+
value = Values.text(value, name: "Oxidized node or group")
|
|
79
|
+
raise InputError, "Invalid Oxidized node or group" if value.match?(/[\x00-\x1f\x7f]/) || %w[. ..].include?(value)
|
|
80
|
+
value
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def invalid!
|
|
84
|
+
raise Netstack::Error.new("Invalid Oxidized response", code: :invalid_response, stage: :response)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Integrations
|
|
5
|
+
module Zabbix
|
|
6
|
+
# JSON-RPC 传输与资源映射;监控资源归属策略由上层决定。
|
|
7
|
+
# 响应 ID 必须匹配请求;凭据和 RPC 错误正文保持私有。
|
|
8
|
+
class Client
|
|
9
|
+
RESOURCES = {
|
|
10
|
+
"host" => %w[hostid hostids], "hostgroup" => %w[groupid groupids],
|
|
11
|
+
"template" => %w[templateid templateids], "proxygroup" => %w[proxy_groupid proxy_groupids],
|
|
12
|
+
"hostinterface" => %w[interfaceid interfaceids], "usermacro" => %w[hostmacroid hostmacroids],
|
|
13
|
+
"item" => %w[itemid itemids], "trigger" => %w[triggerid triggerids]
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
def initialize(token:, api_url: nil, http: nil)
|
|
17
|
+
unless token.is_a?(String) && !token.strip.empty? && !token.match?(/[\r\n\x00]/)
|
|
18
|
+
raise Netstack::InputError.new("Zabbix token is required", code: "invalid_zabbix_configuration")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
@token = token.dup.freeze
|
|
22
|
+
@http = http || Http::Client.new(base_url: api_url)
|
|
23
|
+
@sequence = 0
|
|
24
|
+
@mutex = Mutex.new
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def inspect = "#<#{self.class.name}>"
|
|
28
|
+
|
|
29
|
+
def call(method, params = {}, authenticate: true)
|
|
30
|
+
unless method.is_a?(String) && method.match?(/\A[a-z]+\.[a-z]+\z/i) && (params.is_a?(Hash) || params.is_a?(Array))
|
|
31
|
+
raise Netstack::InputError.new("Invalid JSON-RPC request", code: "invalid_rpc_input")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
id = @mutex.synchronize { @sequence += 1 }
|
|
35
|
+
headers = { "Accept" => "application/json", "Content-Type" => "application/json-rpc" }
|
|
36
|
+
headers["Authorization"] = "Bearer #{@token}" if authenticate
|
|
37
|
+
payload = @http.request(:post, "", headers: headers,
|
|
38
|
+
json: { jsonrpc: "2.0", method: method, params: params, id: id }).json
|
|
39
|
+
unless payload.is_a?(Hash) && payload["jsonrpc"] == "2.0" && payload["id"].is_a?(Integer) &&
|
|
40
|
+
payload["id"] == id && (payload.key?("result") ^ payload.key?("error"))
|
|
41
|
+
invalid_response!(method)
|
|
42
|
+
end
|
|
43
|
+
if payload.key?("error")
|
|
44
|
+
error = payload["error"]
|
|
45
|
+
invalid_response!(method) unless error.is_a?(Hash) && error["code"].is_a?(Integer) && error["message"].is_a?(String)
|
|
46
|
+
raise Error.new("Zabbix rejected #{method}", code: "zabbix_rejected", stage: "response",
|
|
47
|
+
details: { "method" => method, "provider_code" => error["code"] }), cause: nil
|
|
48
|
+
end
|
|
49
|
+
payload.fetch("result")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def api_version
|
|
53
|
+
version = call("apiinfo.version", {}, authenticate: false)
|
|
54
|
+
invalid_response!("apiinfo.version") unless version.is_a?(String) && version.match?(/\A\d+\.\d+(?:\.\d+)?\z/)
|
|
55
|
+
version
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def query(resource, params = {})
|
|
59
|
+
singular, = resource_keys(resource)
|
|
60
|
+
method = "#{resource}.get"
|
|
61
|
+
rows = call(method, default_query(resource).merge(params))
|
|
62
|
+
rows = validate_rows!(rows, method: method, id_key: singular)
|
|
63
|
+
rows = rows.map { |row| normalize_host!(row, method) } if resource.to_s == "host"
|
|
64
|
+
rows.each { |row| validate_interface!(row, method) } if resource.to_s == "hostinterface"
|
|
65
|
+
if resource.to_s == "trigger"
|
|
66
|
+
rows.each do |row|
|
|
67
|
+
validate_rows!(row["dependencies"], method: method, id_key: "triggerid")
|
|
68
|
+
invalid_response!(method) unless valid_tags?(row["tags"])
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
rows
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def get(resource, id)
|
|
75
|
+
_, plural = resource_keys(resource)
|
|
76
|
+
rows = query(resource, plural => [identifier!(id)])
|
|
77
|
+
invalid_response!("#{resource}.get") unless rows.size <= 1
|
|
78
|
+
row = rows.first
|
|
79
|
+
invalid_response!("#{resource}.get") if row && row.fetch(resource_keys(resource).first).to_s != id.to_s
|
|
80
|
+
row
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def find(resource, params)
|
|
84
|
+
rows = query(resource, params)
|
|
85
|
+
invalid_response!("#{resource}.get") unless rows.size <= 1
|
|
86
|
+
rows.first
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def create(resource, attributes)
|
|
90
|
+
mutate(resource, "create", attributes)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def update(resource, id, attributes)
|
|
94
|
+
singular, = resource_keys(resource)
|
|
95
|
+
identifier!(id)
|
|
96
|
+
mutate(resource, "update", attributes.merge(singular => id.to_s), expected_id: id.to_s)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def history(item_id:, value_type:, from:, to:, limit: 10_000)
|
|
100
|
+
range!(from, to, limit)
|
|
101
|
+
raise Netstack::InputError.new("Only numeric Zabbix history is supported", code: "unsupported_history_type") unless [0, 3].include?(value_type)
|
|
102
|
+
|
|
103
|
+
rows = call("history.get", { history: value_type, itemids: [identifier!(item_id)],
|
|
104
|
+
time_from: from, time_till: to, limit: limit, output: "extend", sortfield: %w[clock ns], sortorder: "ASC" })
|
|
105
|
+
validate_rows!(rows, method: "history.get", id_key: "itemid", unique: false)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def trends(item_id:, from:, to:, limit: 10_000)
|
|
109
|
+
range!(from, to, limit)
|
|
110
|
+
rows = call("trend.get", { itemids: [identifier!(item_id)], time_from: from, time_till: to,
|
|
111
|
+
limit: limit, output: %w[itemid clock num value_min value_avg value_max], sortfield: "clock", sortorder: "ASC" })
|
|
112
|
+
validate_rows!(rows, method: "trend.get", id_key: "itemid", unique: false)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def problems(host_id:, from:, limit: 100)
|
|
116
|
+
range!(from, from, limit)
|
|
117
|
+
rows = call("problem.get", { hostids: [identifier!(host_id)], time_from: from, recent: true,
|
|
118
|
+
limit: limit, output: %w[eventid objectid name severity acknowledged clock r_clock], sortfield: "eventid", sortorder: "DESC" })
|
|
119
|
+
validate_rows!(rows, method: "problem.get", id_key: "eventid")
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def resource_keys(resource)
|
|
123
|
+
RESOURCES.fetch(resource.to_s) do
|
|
124
|
+
raise Netstack::InputError.new("Unsupported Zabbix resource", code: "unsupported_zabbix_resource")
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
private
|
|
129
|
+
|
|
130
|
+
def validate_interface!(row, method)
|
|
131
|
+
unless valid_identifier?(row["hostid"]) && %w[1 2 3 4].include?(row["type"].to_s) &&
|
|
132
|
+
%w[0 1].include?(row["main"].to_s) && %w[0 1].include?(row["useip"].to_s) &&
|
|
133
|
+
%w[ip dns port].all? { |key| row[key].is_a?(String) }
|
|
134
|
+
invalid_response!(method)
|
|
135
|
+
end
|
|
136
|
+
invalid_response!(method) if row["type"].to_s == "2" && !row["details"].is_a?(Hash)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def valid_tags?(tags)
|
|
140
|
+
tags.is_a?(Array) && tags.all? { |tag| tag.is_a?(Hash) && tag["tag"].is_a?(String) && tag["value"].is_a?(String) }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def normalize_host!(row, method)
|
|
144
|
+
groups = row["hostgroups"]
|
|
145
|
+
templates = row["parentTemplates"]
|
|
146
|
+
tags = row["tags"]
|
|
147
|
+
inventory = row["inventory"]
|
|
148
|
+
validate_rows!(groups, method: method, id_key: "groupid")
|
|
149
|
+
validate_rows!(templates, method: method, id_key: "templateid")
|
|
150
|
+
unless valid_tags?(tags) &&
|
|
151
|
+
(inventory.is_a?(Hash) || inventory == [])
|
|
152
|
+
invalid_response!(method)
|
|
153
|
+
end
|
|
154
|
+
row.merge("groups" => groups, "inventory" => inventory == [] ? {} : inventory)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def default_query(resource)
|
|
158
|
+
case resource.to_s
|
|
159
|
+
when "host"
|
|
160
|
+
{ "output" => "extend", "selectHostGroups" => %w[groupid], "selectParentTemplates" => %w[templateid],
|
|
161
|
+
"selectTags" => "extend", "selectInventory" => "extend" }
|
|
162
|
+
when "trigger"
|
|
163
|
+
{ "output" => "extend", "selectTags" => "extend", "selectDependencies" => %w[triggerid], "expandExpression" => true }
|
|
164
|
+
when "usermacro"
|
|
165
|
+
# Secret macro values cannot be read back and are never needed for discovery.
|
|
166
|
+
{ "output" => %w[hostmacroid hostid macro type] }
|
|
167
|
+
else
|
|
168
|
+
{ "output" => "extend" }
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def mutate(resource, action, attributes, expected_id: nil)
|
|
173
|
+
_, plural = resource_keys(resource)
|
|
174
|
+
method = "#{resource}.#{action}"
|
|
175
|
+
result = call(method, attributes)
|
|
176
|
+
ids = result[plural] if result.is_a?(Hash)
|
|
177
|
+
invalid_response!(method) unless ids.is_a?(Array) && ids.one? && valid_identifier?(ids.first) && (!expected_id || ids.first.to_s == expected_id)
|
|
178
|
+
ids.first.to_s
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def validate_rows!(rows, method:, id_key:, unique: true)
|
|
182
|
+
invalid_response!(method) unless rows.is_a?(Array) && rows.all? { |row| row.is_a?(Hash) && valid_identifier?(row[id_key]) }
|
|
183
|
+
ids = rows.map { |row| row[id_key].to_s }
|
|
184
|
+
invalid_response!(method) if unique && ids.uniq.size != ids.size
|
|
185
|
+
rows
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def valid_identifier?(value)
|
|
189
|
+
(value.is_a?(Integer) && value.positive?) || (value.is_a?(String) && value.match?(/\A[1-9][0-9]*\z/))
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def identifier!(value)
|
|
193
|
+
return value.to_s if valid_identifier?(value)
|
|
194
|
+
|
|
195
|
+
raise Netstack::InputError.new("Invalid Zabbix identifier", code: "invalid_zabbix_id")
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def range!(from, to, limit)
|
|
199
|
+
return if from.is_a?(Integer) && from >= 0 && to.is_a?(Integer) && to >= from && limit.is_a?(Integer) && (1..100_000).cover?(limit)
|
|
200
|
+
|
|
201
|
+
raise Netstack::InputError.new("Invalid measurement interval or limit", code: "invalid_measurement_range")
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def invalid_response!(method)
|
|
205
|
+
raise Error.new("Invalid Zabbix response for #{method}", code: "invalid_zabbix_response", stage: "response",
|
|
206
|
+
details: { "method" => method }), cause: nil
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ipaddr"
|
|
4
|
+
|
|
5
|
+
module Netstack
|
|
6
|
+
module Inventory
|
|
7
|
+
class Address < Data.define(:host, :prefix, :family)
|
|
8
|
+
def initialize(host:, prefix: nil, family: nil)
|
|
9
|
+
text = Values.text(host, name: "interface address")
|
|
10
|
+
raise InputError, "scoped interface addresses need an explicit interface scope" if text.include?("%")
|
|
11
|
+
|
|
12
|
+
address, suffix = text.split("/", 2)
|
|
13
|
+
raise InputError, "prefix specified twice" if suffix && !prefix.nil?
|
|
14
|
+
|
|
15
|
+
ip = ::IPAddr.new(address)
|
|
16
|
+
limit = ip.ipv4? ? 32 : 128
|
|
17
|
+
prefix = Values.integer(prefix || suffix || limit, name: "prefix", min: 0, max: limit)
|
|
18
|
+
actual_family = ip.ipv4? ? :ipv4 : :ipv6
|
|
19
|
+
raise InputError, "address family mismatch" if family && family.to_sym != actual_family
|
|
20
|
+
|
|
21
|
+
super(host: ip.to_s.freeze, prefix: prefix, family: actual_family)
|
|
22
|
+
rescue ::IPAddr::Error
|
|
23
|
+
raise InputError, "invalid interface address"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def cidr = "#{host}/#{prefix}"
|
|
27
|
+
def network = "#{::IPAddr.new(cidr)}/#{prefix}"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Inventory
|
|
5
|
+
class Changes < Data.define(:target, :source, :dimensions, :skipped)
|
|
6
|
+
def initialize(target:, source:, dimensions:, skipped:)
|
|
7
|
+
super(target: target, source: source, dimensions: Values.copy(dimensions), skipped: Values.copy(skipped))
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def empty? = dimensions.values.all? { |change| change.values.all?(&:empty?) }
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Inventory
|
|
5
|
+
class CliParser
|
|
6
|
+
PLATFORMS = %w[h3c h3c_wireless huawei cisco_ios cisco_nxos].freeze
|
|
7
|
+
|
|
8
|
+
def supports?(platform) = PLATFORMS.include?(platform.to_s)
|
|
9
|
+
|
|
10
|
+
def device(configuration:, version:, target:)
|
|
11
|
+
raise UnsupportedCapability, "inventory platform is not implemented" unless supports?(target.platform)
|
|
12
|
+
|
|
13
|
+
config = Configuration::Reader.new(configuration).content
|
|
14
|
+
version = Configuration::Reader.new(version).content
|
|
15
|
+
name = config[/^\s*(?:hostname|sysname)\s+(\S+)\s*$/, 1]
|
|
16
|
+
raise InputError, "configuration does not contain a recognizable device identity" unless name
|
|
17
|
+
|
|
18
|
+
serial = version[/(?:Processor board ID|Device serial number\s*:|Serial Number\s*:|System serial number\s*:)\s*(\S+)/i, 1]
|
|
19
|
+
os = version[/(?:NXOS: version|system:\s*version|Software, Version|VRP.*Version|IOS.*Version)\s*([^\r\n,]+)/i, 1]&.strip
|
|
20
|
+
model = version[/^\s*(?:cisco|H3C|HUAWEI)\s+([A-Za-z0-9][A-Za-z0-9_-]+)(?:\s+(?:processor|Chassis|Switch|Router)|\s*\()/i, 1]
|
|
21
|
+
Device.new(key: target.key, name: name, serial: serial, model: model, vendor: target.platform.split("_").first,
|
|
22
|
+
os_version: os, attributes: {})
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def interfaces(configuration:, platform:)
|
|
26
|
+
raise UnsupportedCapability, "inventory platform is not implemented" unless supports?(platform)
|
|
27
|
+
|
|
28
|
+
reader = Configuration::Reader.new(configuration)
|
|
29
|
+
rows = []
|
|
30
|
+
current = nil
|
|
31
|
+
while (line = reader.read)
|
|
32
|
+
if (match = /^interface\s+(\S+)\s*$/.match(line))
|
|
33
|
+
rows << build_interface(current) if current
|
|
34
|
+
current = { name: match[1], addresses: [], admin_status: :unknown, attributes: {} }
|
|
35
|
+
elsif current && line.match?(/\A\S/) && !line.match?(/\A(?:#|!)\s*\z/)
|
|
36
|
+
rows << build_interface(current)
|
|
37
|
+
current = nil
|
|
38
|
+
elsif current
|
|
39
|
+
parse_interface_line(current, line.strip)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
rows << build_interface(current) if current
|
|
43
|
+
unless reader.content.match?(/^\s*(?:hostname|sysname)\s+\S+/)
|
|
44
|
+
raise InputError, "interface configuration identity is missing"
|
|
45
|
+
end
|
|
46
|
+
raise InputError, "duplicate interface identity" unless rows.map(&:key).uniq.size == rows.size
|
|
47
|
+
|
|
48
|
+
rows.freeze
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def parse_interface_line(current, text)
|
|
54
|
+
case text
|
|
55
|
+
when /\Adescription\s+(.+)\z/ then current[:description] = Regexp.last_match(1)
|
|
56
|
+
when /\A(?:shutdown|disable)\z/ then current[:admin_status] = :down
|
|
57
|
+
when /\A(?:no shutdown|undo shutdown)\z/ then current[:admin_status] = :up
|
|
58
|
+
when /\Aip address\s+(\S+\/\d+)(?:\s+.*)?\z/
|
|
59
|
+
current[:addresses] << Address.new(host: IPv4.host_cidr(Regexp.last_match(1)))
|
|
60
|
+
when /\Aip address\s+(\S+)\s+(\S+)(?:\s+.*)?\z/
|
|
61
|
+
current[:addresses] << Address.new(host: IPv4.host_cidr(Regexp.last_match(1), Regexp.last_match(2)))
|
|
62
|
+
when /\Aipv6 address\s+(\S+\/\d+)(?:\s+.*)?\z/
|
|
63
|
+
current[:addresses] << Address.new(host: IPv6.canonical(Regexp.last_match(1), host: true))
|
|
64
|
+
when /\Amtu\s+(\d+)\z/ then current[:attributes][:mtu] = Integer(Regexp.last_match(1), 10)
|
|
65
|
+
when /\A(?:ip|ipv6) address\b/
|
|
66
|
+
raise InputError, "unsupported interface address syntax"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def build_interface(attributes)
|
|
71
|
+
Interface.new(**attributes)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|