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,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Wireless
|
|
5
|
+
module H3c
|
|
6
|
+
class LldpParser
|
|
7
|
+
def parse(content, ap_name:, terminated: false)
|
|
8
|
+
ap_name = Values.text(ap_name, name: "LLDP AP name")
|
|
9
|
+
text = Normalizer.text(content)
|
|
10
|
+
neighbors = []
|
|
11
|
+
diagnostics = []
|
|
12
|
+
current = nil
|
|
13
|
+
local = nil
|
|
14
|
+
flush = lambda do
|
|
15
|
+
if current && current[:remote_chassis_id] && current[:remote_interface]
|
|
16
|
+
neighbors << Neighbor.new(key: Normalizer.key(ap_name, local, current[:remote_chassis_id], current[:remote_interface]),
|
|
17
|
+
ap_name: ap_name, local_interface: local, **current)
|
|
18
|
+
elsif current
|
|
19
|
+
diagnostics << { code: :incomplete_neighbor }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
text.lines.each_with_index do |line, index|
|
|
23
|
+
next if line.strip.empty? || /\A\s*[-=]+\s*\z/.match?(line)
|
|
24
|
+
if (match = /LLDP neighbor-information of port \d+\[(.*?)\]\s*:?/i.match(line))
|
|
25
|
+
flush.call
|
|
26
|
+
local = match[1]
|
|
27
|
+
current = nil
|
|
28
|
+
elsif /\ALLDP neighbor index\s*:/i.match?(line.strip)
|
|
29
|
+
flush.call
|
|
30
|
+
current = {}
|
|
31
|
+
elsif (match = /\A\s*([^:]+):\s*(.*?)\s*\z/.match(line))
|
|
32
|
+
label, value = match.captures
|
|
33
|
+
next if /total|number of neighbors/i.match?(label)
|
|
34
|
+
unless local
|
|
35
|
+
diagnostics << { code: :missing_local_interface, line: index + 1 }
|
|
36
|
+
next
|
|
37
|
+
end
|
|
38
|
+
current ||= {}
|
|
39
|
+
case label.strip.downcase
|
|
40
|
+
when "chassis id" then current[:remote_chassis_id] = value.downcase
|
|
41
|
+
when "port id" then current[:remote_interface] = value
|
|
42
|
+
when "system name" then current[:remote_name] = value
|
|
43
|
+
when "management address"
|
|
44
|
+
begin
|
|
45
|
+
current[:management_address] = Normalizer.ip(Normalizer.value(value))
|
|
46
|
+
rescue InputError
|
|
47
|
+
diagnostics << { code: :invalid_management_address, line: index + 1 }
|
|
48
|
+
end
|
|
49
|
+
when "system description", "port description", "port vlan id", "vlan id", "maximum frame size", "chassis type", "port type", "time to live", "system capabilities supported", "system capabilities enabled"
|
|
50
|
+
(current[:attributes] ||= {})[label.strip.downcase.tr(" ", "_").to_sym] = value
|
|
51
|
+
else diagnostics << { code: :unrecognized_field, line: index + 1 }
|
|
52
|
+
end
|
|
53
|
+
elsif !/no (?:LLDP )?neighbors|total.*\b0\b/i.match?(line)
|
|
54
|
+
diagnostics << { code: :unrecognized_line, line: index + 1 }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
flush.call
|
|
58
|
+
count = /(?:total(?: (?:LLDP )?neighbors)?|number of neighbors)[^\n:]*:\s*(\d+)\s*$/i.match(text)&.[](1)&.to_i
|
|
59
|
+
diagnostics << { code: :count_mismatch, expected: count, parsed: neighbors.size } if count && count != neighbors.size
|
|
60
|
+
zero = /no (?:LLDP )?neighbors|(?:total|number of neighbors)[^\n]*:\s*0\s*$/i.match?(text)
|
|
61
|
+
diagnostics << { code: :missing_completeness_evidence } unless zero || (terminated && !neighbors.empty?)
|
|
62
|
+
ParseResult.new(records: neighbors, status: diagnostics.empty? ? :complete : neighbors.empty? ? :failed : :partial, diagnostics: diagnostics)
|
|
63
|
+
rescue InputError => error
|
|
64
|
+
ParseResult.new(records: [], status: :failed, diagnostics: [{ code: :invalid_output, error: error.code }])
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Wireless
|
|
5
|
+
module H3c
|
|
6
|
+
# CLI grammar adapted from PDK::Inspection::Runtime::H3c::WirelessAp::Parser.
|
|
7
|
+
class RuntimeParser
|
|
8
|
+
AP_FIELDS = { "ap id" => :ap_id, "ap group name" => :group_name, "state" => :state, "model" => :model,
|
|
9
|
+
"mac address" => :mac_address, "ip address" => :ip_address, "client count" => :client_count,
|
|
10
|
+
"number of clients" => :client_count, "online clients" => :client_count, "online time" => :online_seconds }.freeze
|
|
11
|
+
RADIO_FIELDS = { "state" => :admin_state, "admin state" => :admin_state, "radio type" => :radio_type,
|
|
12
|
+
"channel" => :channel, "channel usage(%)" => :channel_usage, "channel usage" => :channel_usage,
|
|
13
|
+
"noise floor" => :noise_floor }.freeze
|
|
14
|
+
CLIENT_FIELDS = { "ap name" => :ap_name, "ap id" => :ap_id, "radio id" => :radio_id, "ssid" => :ssid,
|
|
15
|
+
"bssid" => :bssid, "ipv4 address" => :ip_address, "ip address" => :ip_address, "ipv6 address" => :ipv6_address,
|
|
16
|
+
"vlan id" => :vlan_id, "rssi" => :rssi, "online time" => :online_seconds }.freeze
|
|
17
|
+
CLIENT_AUTH = %w[username authentication\ method security\ mode akm\ mode cipher\ suite user\ authentication\ mode wpa3\ status pmf\ status].freeze
|
|
18
|
+
CLIENT_TERMINAL = %w[sm\ power\ save short\ gi\ for\ 20mhz stbc\ rx\ capability ldpc\ rx\ capability ofdma\ random\ access\ rus twt\ scheduled qos\ mode listen\ interval].freeze
|
|
19
|
+
CLIENT_ATTRIBUTES = %w[aid channel wireless\ mode channel\ bandwidth roam\ status btm\ mode].freeze
|
|
20
|
+
BSS_FIELDS = { "ap name" => :ap_name, "ap id" => :ap_id, "radio id" => :radio_id, "ssid" => :ssid,
|
|
21
|
+
"service template" => :service_template, "service template name" => :service_template,
|
|
22
|
+
"vlan id" => :vlan_id, "state" => :state, "client count" => :client_count,
|
|
23
|
+
"number of clients" => :client_count, "online clients" => :client_count }.freeze
|
|
24
|
+
AUTH_FIELDS = { "username" => :username, "user name" => :username, "access interface" => :interface,
|
|
25
|
+
"interface" => :interface, "ap name" => :ap_name, "ap id" => :ap_id, "radio id" => :radio_id,
|
|
26
|
+
"ssid" => :ssid, "bssid" => :bssid, "ip address" => :ip_address, "ipv4 address" => :ip_address,
|
|
27
|
+
"authentication domain" => :domain, "domain" => :domain, "authentication protocol" => :protocol,
|
|
28
|
+
"auth protocol" => :protocol, "access protocol" => :protocol, "protocol" => :protocol,
|
|
29
|
+
"authentication state" => :state, "state" => :state, "status" => :state,
|
|
30
|
+
"initial vlan" => :initial_vlan_id, "initial vlan id" => :initial_vlan_id,
|
|
31
|
+
"authorization vlan" => :authorized_vlan_id, "authorization vlan id" => :authorized_vlan_id,
|
|
32
|
+
"authorized vlan" => :authorized_vlan_id, "authorized vlan id" => :authorized_vlan_id,
|
|
33
|
+
"online time" => :online_seconds }.freeze
|
|
34
|
+
|
|
35
|
+
def parse(dimension, content, terminated: false)
|
|
36
|
+
text = Normalizer.text(content)
|
|
37
|
+
case dimension.to_sym
|
|
38
|
+
when :access_points, :radios then parse_access_points(text, dimension.to_sym, terminated: terminated)
|
|
39
|
+
when :clients then parse_blocks(text, :clients, /\Amac address\z/, terminated: terminated)
|
|
40
|
+
when :bsses then parse_blocks(text, :bsses, /\Abssid\z/, terminated: terminated)
|
|
41
|
+
when :dot1x_connections, :mab_connections
|
|
42
|
+
parse_blocks(text, dimension.to_sym, /\A(?:user )?mac address\z/, terminated: terminated)
|
|
43
|
+
when :blocked_clients then parse_blocked_clients(text, terminated: terminated)
|
|
44
|
+
else raise InputError, "unsupported H3c wireless dimension"
|
|
45
|
+
end
|
|
46
|
+
rescue InputError => error
|
|
47
|
+
ParseResult.new(records: [], status: :failed, diagnostics: [{ code: :invalid_output, error: error.code }])
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def parse_access_points(text, dimension, terminated:)
|
|
53
|
+
aps = []
|
|
54
|
+
radios = []
|
|
55
|
+
diagnostics = []
|
|
56
|
+
current = radio = nil
|
|
57
|
+
each_field(text, diagnostics) do |label, value, line|
|
|
58
|
+
if label == "ap name"
|
|
59
|
+
current = { name: value }
|
|
60
|
+
aps << current
|
|
61
|
+
radio = nil
|
|
62
|
+
elsif current && (match = /\Aradio (\d+)\z/.match(label))
|
|
63
|
+
radio = { ap_name: current[:name], radio_id: match[1].to_i }
|
|
64
|
+
radios << radio
|
|
65
|
+
elsif radio && RADIO_FIELDS.key?(label)
|
|
66
|
+
safely(diagnostics, line) { assign(radio, RADIO_FIELDS.fetch(label), value) }
|
|
67
|
+
elsif current && AP_FIELDS.key?(label)
|
|
68
|
+
safely(diagnostics, line) { assign(current, AP_FIELDS.fetch(label), value) }
|
|
69
|
+
elsif current && label == "capwap data-tunnel status"
|
|
70
|
+
(current[:configuration] ||= {})[:capwap_data_tunnel_status] = value
|
|
71
|
+
elsif !summary?(label)
|
|
72
|
+
diagnostics << { code: :unrecognized_field, line: line }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
records = if dimension == :access_points
|
|
76
|
+
aps.filter_map do |attributes|
|
|
77
|
+
next if attributes[:name].nil?
|
|
78
|
+
|
|
79
|
+
AccessPoint.new(key: Normalizer.key(attributes[:name]), **attributes)
|
|
80
|
+
end
|
|
81
|
+
else
|
|
82
|
+
radios.map { |attributes| Radio.new(key: Normalizer.key(attributes[:ap_name], attributes[:radio_id]), **attributes) }
|
|
83
|
+
end
|
|
84
|
+
total = total_count(text, :access_points)
|
|
85
|
+
diagnostics << { code: :count_mismatch, expected: total, parsed: aps.size } if total && total != aps.size
|
|
86
|
+
result(records, diagnostics, total: total, terminated: terminated, recognized: !aps.empty?)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def parse_blocks(text, dimension, start, terminated:)
|
|
90
|
+
blocks = []
|
|
91
|
+
current = nil
|
|
92
|
+
diagnostics = []
|
|
93
|
+
each_field(text, diagnostics) do |label, value, line|
|
|
94
|
+
if start.match?(label)
|
|
95
|
+
current = { (dimension == :bsses ? :bssid : :mac_address) => value }
|
|
96
|
+
blocks << current
|
|
97
|
+
elsif current
|
|
98
|
+
current[:fields] ||= []
|
|
99
|
+
current[:fields] << [label, value, line]
|
|
100
|
+
elsif !summary?(label)
|
|
101
|
+
diagnostics << { code: :unrecognized_field, line: line }
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
records = blocks.filter_map do |block|
|
|
105
|
+
safely(diagnostics, nil) do
|
|
106
|
+
fields = block.delete(:fields) || []
|
|
107
|
+
identity_field = dimension == :bsses ? :bssid : :mac_address
|
|
108
|
+
block[identity_field] = Normalizer.mac(block[identity_field])
|
|
109
|
+
raise InputError, "missing wireless identity" unless block[identity_field]
|
|
110
|
+
|
|
111
|
+
fields.each { |label, value, line| safely(diagnostics, line) { block_field(block, dimension, label, value, diagnostics, line) } }
|
|
112
|
+
build_record(block, dimension)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
total = total_count(text, dimension)
|
|
116
|
+
diagnostics << { code: :count_mismatch, expected: total, parsed: records.size } if total && total != records.size
|
|
117
|
+
result(records, diagnostics, total: total, terminated: terminated)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def block_field(record, dimension, label, value, diagnostics, line)
|
|
121
|
+
mapping = dimension == :clients ? CLIENT_FIELDS : dimension == :bsses ? BSS_FIELDS : AUTH_FIELDS
|
|
122
|
+
if mapping.key?(label)
|
|
123
|
+
assign(record, mapping.fetch(label), value)
|
|
124
|
+
elsif dimension == :clients && CLIENT_AUTH.include?(label)
|
|
125
|
+
(record[:authentication] ||= {})[label.tr(" -", "__").to_sym] = value
|
|
126
|
+
elsif dimension == :clients && CLIENT_TERMINAL.include?(label)
|
|
127
|
+
(record[:terminal] ||= {})[label.tr(" -", "__").to_sym] = value
|
|
128
|
+
elsif dimension == :clients && CLIENT_ATTRIBUTES.include?(label)
|
|
129
|
+
(record[:attributes] ||= {})[label.tr(" -", "__").to_sym] = value
|
|
130
|
+
elsif dimension == :clients && ["rx/tx rate", "speed"].include?(label)
|
|
131
|
+
rates = value && Normalizer.rate_pair(value, default: label == "speed" ? "Kbps" : "Mbps")
|
|
132
|
+
if label == "speed"
|
|
133
|
+
(record[:attributes] ||= {})[:throughput_bps] = rates
|
|
134
|
+
else
|
|
135
|
+
record[:rx_rate_bps], record[:tx_rate_bps] = rates
|
|
136
|
+
end
|
|
137
|
+
elsif dimension == :bsses && ["security mode", "akm mode", "cipher suite", "bss id"].include?(label)
|
|
138
|
+
(record[:security] ||= {})[label.tr(" ", "_").to_sym] = value
|
|
139
|
+
elsif dimension != :clients && label == "authentication method"
|
|
140
|
+
record[:protocol] ||= value
|
|
141
|
+
else
|
|
142
|
+
diagnostics << { code: :unrecognized_field, line: line }
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def build_record(attributes, dimension)
|
|
147
|
+
case dimension
|
|
148
|
+
when :clients
|
|
149
|
+
rssi = attributes[:rssi]
|
|
150
|
+
attributes[:rssi] = nil if rssi == 0
|
|
151
|
+
Client.new(key: Normalizer.key(attributes[:mac_address]), rssi_scale: rssi && rssi < 0 ? :dbm : :relative, **attributes)
|
|
152
|
+
when :bsses
|
|
153
|
+
Bss.new(key: Normalizer.key(attributes[:bssid]), **attributes)
|
|
154
|
+
else
|
|
155
|
+
kind = dimension == :dot1x_connections ? :dot1x : :mab
|
|
156
|
+
AuthenticationConnection.new(key: Normalizer.key(kind, attributes[:mac_address], attributes[:interface], attributes[:bssid]), kind: kind, **attributes)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def assign(record, key, value)
|
|
161
|
+
record[key] = case key
|
|
162
|
+
when :ap_id, :radio_id, :client_count then Normalizer.integer(value)
|
|
163
|
+
when :vlan_id, :initial_vlan_id, :authorized_vlan_id then Normalizer.integer(value, max: 4094)
|
|
164
|
+
when :mac_address, :bssid then Normalizer.mac(value)
|
|
165
|
+
when :ip_address, :ipv6_address then Normalizer.ip(value)
|
|
166
|
+
when :online_seconds then Normalizer.duration(value)
|
|
167
|
+
when :rssi then Normalizer.number(value, unit: "dBm")
|
|
168
|
+
when :noise_floor then Normalizer.number(value, unit: "dBm")&.then { |number| number.negative? ? number : nil }
|
|
169
|
+
when :channel_usage
|
|
170
|
+
number = Normalizer.number(value, unit: "%")
|
|
171
|
+
raise InputError, "invalid channel usage" if number && !(0..100).cover?(number)
|
|
172
|
+
number
|
|
173
|
+
else value
|
|
174
|
+
end
|
|
175
|
+
record[:channel_number] = Normalizer.channel(value) if key == :channel
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def each_field(text, diagnostics)
|
|
179
|
+
text.lines.each_with_index do |line, index|
|
|
180
|
+
next if line.strip.empty? || /\A\s*[-=]+\s*\z/.match?(line)
|
|
181
|
+
match = /\A\s*([^:]+?)\s*:\s*(.*?)\s*\z/.match(line)
|
|
182
|
+
unless match
|
|
183
|
+
diagnostics << { code: :unrecognized_line, line: index + 1 }
|
|
184
|
+
next
|
|
185
|
+
end
|
|
186
|
+
yield match[1].strip.downcase.gsub(/\s+/, " "), Normalizer.value(match[2]), index + 1
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def total_count(text, dimension)
|
|
191
|
+
noun = { access_points: "APs", clients: "clients", bsses: "BSSs?", dot1x_connections: "(?:connections|users|online users)", mab_connections: "(?:connections|users|online users)" }.fetch(dimension)
|
|
192
|
+
match = /(?:Total(?: number of)?\s+(?:#{noun})|Total)\s*:\s*(\d+)\s*$/i.match(text)
|
|
193
|
+
match && match[1].to_i
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def summary?(label) = /\A(?:total|number of|connected|remaining|maximum|current number)\b/.match?(label)
|
|
197
|
+
|
|
198
|
+
def safely(diagnostics, line)
|
|
199
|
+
yield
|
|
200
|
+
rescue InputError
|
|
201
|
+
diagnostics << { code: :invalid_field, line: line }.compact
|
|
202
|
+
nil
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def result(records, diagnostics, total:, terminated:, recognized: !records.empty?)
|
|
206
|
+
diagnostics << { code: :missing_completeness_evidence } unless total || (terminated && recognized)
|
|
207
|
+
status = diagnostics.empty? ? :complete : records.empty? ? :failed : :partial
|
|
208
|
+
ParseResult.new(records: records, status: status, diagnostics: diagnostics)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def parse_blocked_clients(text, terminated:)
|
|
212
|
+
records = []
|
|
213
|
+
diagnostics = []
|
|
214
|
+
explicit_zero = false
|
|
215
|
+
text.lines.each_with_index do |line, index|
|
|
216
|
+
next if line.strip.empty? || /\A\s*[-=]+\s*\z/.match?(line)
|
|
217
|
+
if /\A\s*No blocked clients\s*\z/i.match?(line)
|
|
218
|
+
explicit_zero = true
|
|
219
|
+
next
|
|
220
|
+
end
|
|
221
|
+
if /\A\s*(?:MAC address(?:\s+.*)?|(?:Client )?block list|Blocked clients|Total(?: number of)?(?:\s+\w+)*\s*:\s*\d+)\s*\z/i.match?(line)
|
|
222
|
+
next
|
|
223
|
+
end
|
|
224
|
+
match = /\A\s*((?:[0-9a-f]{4}-){2}[0-9a-f]{4}|(?:[0-9a-f]{2}:){5}[0-9a-f]{2})\s*(.*?)\s*\z/i.match(line)
|
|
225
|
+
if match
|
|
226
|
+
mac = Normalizer.mac(match[1])
|
|
227
|
+
records << { key: Normalizer.key(mac), mac_address: mac, reason: match[2] }
|
|
228
|
+
else
|
|
229
|
+
diagnostics << { code: :unrecognized_line, line: index + 1 }
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
count = /Total(?: number of)?(?:\s+\w+)*\s*:\s*(\d+)/i.match(text)&.[](1)&.to_i
|
|
233
|
+
count ||= 0 if explicit_zero
|
|
234
|
+
diagnostics << { code: :count_mismatch, expected: count, parsed: records.size } if count && count != records.size
|
|
235
|
+
result(records, diagnostics, total: count, terminated: terminated)
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Wireless
|
|
5
|
+
module Huawei
|
|
6
|
+
# Only the AP and station table formats evidenced by PDK are supported.
|
|
7
|
+
class RuntimeParser
|
|
8
|
+
def parse(dimension, content, terminated: false)
|
|
9
|
+
raise InputError, "unsupported Huawei wireless dimension" unless %i[access_points clients].include?(dimension.to_sym)
|
|
10
|
+
|
|
11
|
+
text = Normalizer.text(content)
|
|
12
|
+
records = []
|
|
13
|
+
diagnostics = []
|
|
14
|
+
header = false
|
|
15
|
+
total = nil
|
|
16
|
+
text.lines.each_with_index do |line, index|
|
|
17
|
+
row = line.strip
|
|
18
|
+
next if row.empty? || /\A[-=]+\z/.match?(row)
|
|
19
|
+
if (match = /\ATotal\s*:\s*(\d+)\s*\z/i.match(row))
|
|
20
|
+
total = match[1].to_i
|
|
21
|
+
next
|
|
22
|
+
end
|
|
23
|
+
if dimension.to_sym == :access_points ? /\AID\s+MAC\s+Name\s+Group\s+IP\s+Type\s+State\s+STA\s+Uptime\b/i.match?(row) : /\ASTA MAC\s+AP ID\s+Ap name\s+Rf\/WLAN\b/i.match?(row)
|
|
24
|
+
header = true
|
|
25
|
+
next
|
|
26
|
+
end
|
|
27
|
+
next if /\ATotal AP information:|\A(?:nor|fault|idle|stdby)\s*:/i.match?(row)
|
|
28
|
+
unless header
|
|
29
|
+
diagnostics << { code: :missing_header, line: index + 1 }
|
|
30
|
+
next
|
|
31
|
+
end
|
|
32
|
+
begin
|
|
33
|
+
records << (dimension.to_sym == :access_points ? access_point(row) : client(row))
|
|
34
|
+
rescue InputError
|
|
35
|
+
diagnostics << { code: :invalid_row, line: index + 1 }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
diagnostics << { code: :missing_header } unless header
|
|
39
|
+
diagnostics << { code: :count_mismatch, expected: total, parsed: records.size } if total && total != records.size
|
|
40
|
+
diagnostics << { code: :missing_completeness_evidence } unless total || (terminated && header && !records.empty?)
|
|
41
|
+
ParseResult.new(records: records, status: diagnostics.empty? ? :complete : records.empty? ? :failed : :partial, diagnostics: diagnostics)
|
|
42
|
+
rescue InputError => error
|
|
43
|
+
ParseResult.new(records: [], status: :failed, diagnostics: [{ code: :invalid_output, error: error.code }])
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def access_point(row)
|
|
49
|
+
columns = row.split(/\s+/)
|
|
50
|
+
raise InputError, "invalid AP table row" unless columns.size >= 9
|
|
51
|
+
|
|
52
|
+
id, mac, name, group, ip, model, state, clients, uptime = columns
|
|
53
|
+
state = { "nor" => "Normal", "normal" => "Normal", "fault" => "Fault", "idle" => "Idle", "stdby" => "Standby" }.fetch(state.downcase, state)
|
|
54
|
+
AccessPoint.new(key: Normalizer.key(name), name: name, ap_id: Normalizer.integer(id),
|
|
55
|
+
mac_address: Normalizer.mac(mac), group_name: group, ip_address: Normalizer.ip(Normalizer.value(ip)),
|
|
56
|
+
model: model, state: state, client_count: Normalizer.integer(clients), online_seconds: Normalizer.duration(Normalizer.value(uptime)))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def client(row)
|
|
60
|
+
columns = row.split(/\s+/)
|
|
61
|
+
raise InputError, "invalid station table row" unless columns.size == 15
|
|
62
|
+
|
|
63
|
+
mac, ap_id, ap_name, radio_wlan, band, type, rates, rssi, vlan, ipv4, ssid, state, ipv6, ac_id, online = columns
|
|
64
|
+
raise InputError, "invalid station identity" unless /\A\d+\/\d+\z/.match?(radio_wlan)
|
|
65
|
+
|
|
66
|
+
build_client(mac, ap_id, ap_name, radio_wlan, band, type, rates, rssi, vlan, ipv4, ssid, state, ipv6, ac_id, online)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def build_client(mac, ap_id, ap_name, radio_wlan, band, type, rates, rssi, vlan, ipv4, ssid, state, ipv6, ac_id, online)
|
|
70
|
+
address = Normalizer.mac(mac)
|
|
71
|
+
rx, tx = Normalizer.rate_pair(rates)
|
|
72
|
+
signal = Normalizer.number(Normalizer.value(rssi), unit: "dBm")
|
|
73
|
+
signal = nil if signal == 0
|
|
74
|
+
Client.new(key: Normalizer.key(address), mac_address: address, ap_id: Normalizer.integer(ap_id), ap_name: ap_name,
|
|
75
|
+
radio_id: Normalizer.integer(radio_wlan.split("/").first), ssid: ssid, vlan_id: Normalizer.integer(vlan, max: 4094),
|
|
76
|
+
ip_address: Normalizer.ip(Normalizer.value(ipv4)), ipv6_address: Normalizer.ip(Normalizer.value(ipv6)),
|
|
77
|
+
rssi: signal, rssi_scale: :dbm, online_seconds: Normalizer.duration(Normalizer.value(online)), rx_rate_bps: rx, tx_rate_bps: tx,
|
|
78
|
+
attributes: { band: band, wireless_mode: type, state: state, wlan_id: radio_wlan.split("/").last.to_i, ac_id: Normalizer.integer(ac_id) })
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Wireless
|
|
5
|
+
class InvalidSnapshot < InputError
|
|
6
|
+
def initialize(message = "invalid wireless snapshot", **options)
|
|
7
|
+
super(message, code: :invalid_wireless_snapshot, **options)
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Wireless
|
|
5
|
+
class Neighbor < Data.define(:key, :ap_name, :local_interface, :remote_chassis_id, :remote_interface, :remote_name, :management_address, :attributes)
|
|
6
|
+
def initialize(key:, ap_name:, local_interface:, remote_chassis_id:, remote_interface:, remote_name: nil, management_address: nil, attributes: {})
|
|
7
|
+
key = Values.text(key, name: "wireless record key")
|
|
8
|
+
super(**Values.copy(key: key, ap_name: ap_name, local_interface: local_interface, remote_chassis_id: remote_chassis_id, remote_interface: remote_interface, remote_name: remote_name, management_address: management_address, attributes: attributes))
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ipaddr"
|
|
4
|
+
|
|
5
|
+
module Netstack
|
|
6
|
+
module Wireless
|
|
7
|
+
module Normalizer
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def text(content)
|
|
11
|
+
raise InputError, "wireless output must be text" unless content.is_a?(String)
|
|
12
|
+
|
|
13
|
+
result = content.dup.force_encoding(Encoding::UTF_8)
|
|
14
|
+
raise InputError, "invalid wireless output encoding" unless result.valid_encoding? && !result.include?("\0")
|
|
15
|
+
|
|
16
|
+
result.gsub(/\r\n?/, "\n")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def value(text)
|
|
20
|
+
normalized = text.strip
|
|
21
|
+
%w[N/A -- -].include?(normalized) || normalized.empty? ? nil : normalized
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def mac(value)
|
|
25
|
+
return nil if value.nil?
|
|
26
|
+
unless /\A(?:[0-9a-f]{12}|(?:[0-9a-f]{4}[-.]){2}[0-9a-f]{4}|(?:[0-9a-f]{2}:){5}[0-9a-f]{2})\z/i.match?(value)
|
|
27
|
+
raise InputError, "invalid wireless MAC address"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
value.delete("-.:").downcase.scan(/../).join(":")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def ip(value)
|
|
34
|
+
return nil if value.nil?
|
|
35
|
+
raise InputError, "invalid wireless IP address" if value.include?("/") || value.include?("%")
|
|
36
|
+
|
|
37
|
+
IPAddr.new(value).to_s
|
|
38
|
+
rescue IPAddr::InvalidAddressError
|
|
39
|
+
raise InputError, "invalid wireless IP address"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def integer(value, max: nil)
|
|
43
|
+
return nil if value.nil?
|
|
44
|
+
|
|
45
|
+
Values.integer(value, name: "wireless integer", min: 0, max: max)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def number(value, unit: nil)
|
|
49
|
+
return nil if value.nil?
|
|
50
|
+
|
|
51
|
+
pattern = unit ? /\A(-?\d+(?:\.\d+)?)\s*(?:#{Regexp.escape(unit)})?\z/i : /\A(-?\d+(?:\.\d+)?)\z/
|
|
52
|
+
match = pattern.match(value.to_s)
|
|
53
|
+
raise InputError, "invalid wireless number" unless match
|
|
54
|
+
|
|
55
|
+
Float(match[1])
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def duration(value)
|
|
59
|
+
return nil if value.nil?
|
|
60
|
+
return value.split(":").map(&:to_i).zip([86_400, 3600, 60, 1]).sum { |a, b| a * b } if /\A\d+:\d{2}:\d{2}:\d{2}\z/.match?(value)
|
|
61
|
+
|
|
62
|
+
parts = value.scan(/(\d+)\s*(days?|hours?|minutes?|seconds?|[dhms])\b/i)
|
|
63
|
+
rest = value.gsub(/\d+\s*(?:days?|hours?|minutes?|seconds?|[dhms])\b/i, "").delete(" :,")
|
|
64
|
+
raise InputError, "invalid wireless duration" if parts.empty? || !rest.empty?
|
|
65
|
+
|
|
66
|
+
parts.sum { |n, unit| n.to_i * { "d" => 86_400, "h" => 3600, "m" => 60, "s" => 1 }.fetch(unit[0].downcase) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def rate_pair(value, default: "Mbps")
|
|
70
|
+
match = /\A(\d+(?:\.\d+)?)\s*\/\s*(\d+(?:\.\d+)?)\s*([kmgt]?bps)?\z/i.match(value)
|
|
71
|
+
raise InputError, "invalid wireless rate" unless match
|
|
72
|
+
|
|
73
|
+
multiplier = { "bps" => 1, "kbps" => 1000, "mbps" => 1000_000, "gbps" => 1000_000_000, "tbps" => 1000_000_000_000 }.fetch((match[3] || default).downcase)
|
|
74
|
+
[match[1], match[2]].map { |number| (Float(number) * multiplier).round }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# A length-prefixed composite key cannot collide when names contain punctuation.
|
|
78
|
+
def key(*parts) = parts.map { |part| text = part.to_s; "#{text.bytesize}:#{text}" }.join("|")
|
|
79
|
+
|
|
80
|
+
def channel(value)
|
|
81
|
+
return nil if value.nil? || /\Aauto\z/i.match?(value)
|
|
82
|
+
|
|
83
|
+
match = /\A(\d+)(?:\s*\([^\n]*\))?\z/.match(value)
|
|
84
|
+
raise InputError, "invalid wireless channel" unless match
|
|
85
|
+
|
|
86
|
+
integer(match[1], max: 233)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Wireless
|
|
5
|
+
class ParseResult < Data.define(:records, :status, :diagnostics)
|
|
6
|
+
def initialize(records:, status:, diagnostics: [])
|
|
7
|
+
raise InputError, "invalid parser status" unless %i[complete partial failed].include?(status)
|
|
8
|
+
|
|
9
|
+
super(records: Values.copy(records), status: status, diagnostics: Values.copy(diagnostics))
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Wireless
|
|
5
|
+
class Radio < Data.define(:key, :ap_name, :radio_id, :admin_state, :radio_type, :channel, :channel_number, :channel_usage, :noise_floor, :configuration)
|
|
6
|
+
def initialize(key:, ap_name:, radio_id:, admin_state: nil, radio_type: nil, channel: nil, channel_number: nil, channel_usage: nil, noise_floor: nil, configuration: {})
|
|
7
|
+
key = Values.text(key, name: "wireless record key")
|
|
8
|
+
super(**Values.copy(key: key, ap_name: ap_name, radio_id: radio_id, admin_state: admin_state, radio_type: radio_type, channel: channel, channel_number: channel_number, channel_usage: channel_usage, noise_floor: noise_floor, configuration: configuration))
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Wireless
|
|
5
|
+
# 生成新的保留状态而不持久化;部分观察只合并已知字段,
|
|
6
|
+
# 不能删除缺失身份;时间水位用于拒绝过期输入。
|
|
7
|
+
class Reconciler
|
|
8
|
+
def call(previous:, observed:)
|
|
9
|
+
raise InvalidSnapshot, "wireless observation must be a Snapshot" unless observed.is_a?(Snapshot)
|
|
10
|
+
previous = State.from_snapshot(previous) if previous.is_a?(Snapshot)
|
|
11
|
+
previous ||= State.new(target: observed.target)
|
|
12
|
+
raise InvalidSnapshot, "wireless state must be a State" unless previous.is_a?(State)
|
|
13
|
+
if previous.target.key != observed.target.key || previous.target.platform != observed.target.platform || previous.target.host != observed.target.host
|
|
14
|
+
raise InvalidSnapshot, "wireless snapshots belong to different controllers"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
records = previous.records.dup
|
|
18
|
+
watermarks = previous.watermarks.dup
|
|
19
|
+
changes = {}
|
|
20
|
+
observed.dimensions.each do |dimension|
|
|
21
|
+
prior = (records[dimension.name] || []).to_h { |record| [key(record), record] }
|
|
22
|
+
if dimension.status != :not_requested
|
|
23
|
+
raise InvalidSnapshot, "wireless observation has no collection time" unless dimension.completed_at
|
|
24
|
+
if watermarks[dimension.name] && dimension.completed_at < watermarks[dimension.name]
|
|
25
|
+
raise InvalidSnapshot, "stale wireless dimension"
|
|
26
|
+
end
|
|
27
|
+
watermarks[dimension.name] = dimension.completed_at
|
|
28
|
+
end
|
|
29
|
+
same_time = previous.watermarks[dimension.name] && dimension.completed_at == previous.watermarks[dimension.name]
|
|
30
|
+
incoming = dimension.records.to_h { |record| [key(record), record] }
|
|
31
|
+
if dimension.partial?
|
|
32
|
+
incoming = incoming.transform_values do |record|
|
|
33
|
+
old = prior[key(record)]
|
|
34
|
+
old ? merge_record(old, record) : record
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
next_records = if dimension.complete?
|
|
38
|
+
incoming
|
|
39
|
+
elsif dimension.partial?
|
|
40
|
+
prior.merge(incoming)
|
|
41
|
+
else
|
|
42
|
+
prior
|
|
43
|
+
end
|
|
44
|
+
raise InvalidSnapshot, "conflicting wireless observations at the same time" if same_time && next_records != prior
|
|
45
|
+
changes[dimension.name] = {
|
|
46
|
+
added: next_records.keys - prior.keys,
|
|
47
|
+
updated: (next_records.keys & prior.keys).select { |identity| next_records[identity] != prior[identity] },
|
|
48
|
+
removed: prior.keys - next_records.keys,
|
|
49
|
+
retained: prior.keys - incoming.keys,
|
|
50
|
+
status: dimension.status
|
|
51
|
+
}
|
|
52
|
+
records[dimension.name] = next_records.values
|
|
53
|
+
end
|
|
54
|
+
state = State.new(target: observed.target, records: records, watermarks: watermarks)
|
|
55
|
+
Reconciliation.new(state: state, changes: changes, observation: observed)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def key(record) = record.is_a?(Hash) ? record.fetch(:key) : record.key
|
|
61
|
+
|
|
62
|
+
def merge_record(old, current)
|
|
63
|
+
return old.merge(current.reject { |_, value| value.nil? }) if old.is_a?(Hash)
|
|
64
|
+
if current.is_a?(Configuration)
|
|
65
|
+
return Configuration.new(access_points: merge_hash(old.access_points, current.access_points),
|
|
66
|
+
ap_groups: merge_records(old.ap_groups, current.ap_groups), service_templates: merge_records(old.service_templates, current.service_templates),
|
|
67
|
+
global_settings: merge_hash(old.global_settings, current.global_settings),
|
|
68
|
+
blacklist: merge_mac_entries(old.blacklist, current.blacklist),
|
|
69
|
+
whitelist: merge_mac_entries(old.whitelist, current.whitelist))
|
|
70
|
+
end
|
|
71
|
+
current.class.new(**old.to_h.merge(current.to_h) do |_, before, after|
|
|
72
|
+
if after.nil?
|
|
73
|
+
before
|
|
74
|
+
elsif before.is_a?(Hash) && after.is_a?(Hash)
|
|
75
|
+
merge_hash(before, after)
|
|
76
|
+
elsif before.is_a?(Array) && after.is_a?(Array)
|
|
77
|
+
(before + after).uniq
|
|
78
|
+
else
|
|
79
|
+
after
|
|
80
|
+
end
|
|
81
|
+
end)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def merge_records(old, current)
|
|
85
|
+
values = old.to_h { |record| [record.key, record] }
|
|
86
|
+
current.each { |record| values[record.key] = values[record.key] ? merge_record(values[record.key], record) : record }
|
|
87
|
+
values.values
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# 部分名单按 MAC 更新已观测字段,保留缺失字段和未观测身份,不能授权删除。
|
|
91
|
+
def merge_mac_entries(old, current)
|
|
92
|
+
values = old.to_h { |record| [record.fetch(:mac_address), record] }
|
|
93
|
+
current.each do |record|
|
|
94
|
+
identity = record.fetch(:mac_address)
|
|
95
|
+
values[identity] = values[identity] ? merge_record(values[identity], record) : record
|
|
96
|
+
end
|
|
97
|
+
values.values
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def merge_hash(old, current)
|
|
101
|
+
old.merge(current) do |field, before, after|
|
|
102
|
+
if field == :service_templates && before.is_a?(Array) && after.is_a?(Array)
|
|
103
|
+
ServiceTemplateBindings.merge(before, after)
|
|
104
|
+
elsif before.is_a?(Hash) && after.is_a?(Hash)
|
|
105
|
+
merge_hash(before, after)
|
|
106
|
+
elsif before.is_a?(Array) && after.is_a?(Array)
|
|
107
|
+
(before + after).uniq
|
|
108
|
+
else
|
|
109
|
+
after.nil? ? before : after
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Wireless
|
|
5
|
+
class Reconciliation < Data.define(:state, :changes, :observation)
|
|
6
|
+
def initialize(state:, changes:, observation:)
|
|
7
|
+
super(state: Values.copy(state), changes: Values.copy(changes), observation: Values.copy(observation))
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|