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,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Wireless
|
|
5
|
+
class AccessPoint < Data.define(:key, :name, :ap_id, :group_name, :model, :serial_id, :mac_address, :ip_address, :state, :client_count, :online_seconds, :configuration)
|
|
6
|
+
def initialize(key:, name:, ap_id: nil, group_name: nil, model: nil, serial_id: nil, mac_address: nil, ip_address: nil, state: nil, client_count: nil, online_seconds: nil, configuration: {})
|
|
7
|
+
key = Values.text(key, name: "wireless record key")
|
|
8
|
+
super(**Values.copy(key: key, name: name, ap_id: ap_id, group_name: group_name, model: model, serial_id: serial_id, mac_address: mac_address, ip_address: ip_address, state: state, client_count: client_count, online_seconds: online_seconds, configuration: configuration))
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Wireless
|
|
5
|
+
class ApGroup < Data.define(:key, :name, :ap_names, :serial_ids, :vlan_ids, :model_profiles, :configuration)
|
|
6
|
+
def initialize(key:, name:, ap_names: [], serial_ids: [], vlan_ids: [], model_profiles: {}, configuration: {})
|
|
7
|
+
key = Values.text(key, name: "wireless record key")
|
|
8
|
+
super(**Values.copy(key: key, name: name, ap_names: ap_names, serial_ids: serial_ids, vlan_ids: vlan_ids, model_profiles: model_profiles, configuration: configuration))
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Wireless
|
|
5
|
+
class Assessment < Data.define(:target, :findings, :dimension_statuses, :counts)
|
|
6
|
+
def initialize(target:, findings:, dimension_statuses:, counts:)
|
|
7
|
+
super(**Values.copy(target: target, findings: findings, dimension_statuses: dimension_statuses, counts: counts))
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def healthy? = findings.empty? && dimension_statuses.values.any?(:complete) && dimension_statuses.values.all? { |status| %i[complete not_requested].include?(status) }
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Wireless
|
|
5
|
+
class Assessor
|
|
6
|
+
def initialize(relative_rssi_warning: 20, dbm_rssi_warning: -70, channel_usage_warning: 80, noise_floor_warning: -75,
|
|
7
|
+
ap_recent_seconds: 3600, client_recent_seconds: 1800)
|
|
8
|
+
@relative_rssi_warning = Float(relative_rssi_warning)
|
|
9
|
+
@dbm_rssi_warning = Float(dbm_rssi_warning)
|
|
10
|
+
@channel_usage_warning = Float(channel_usage_warning)
|
|
11
|
+
@noise_floor_warning = Float(noise_floor_warning)
|
|
12
|
+
@ap_recent_seconds = Values.integer(ap_recent_seconds, name: "AP recent uptime threshold", min: 0)
|
|
13
|
+
@client_recent_seconds = Values.integer(client_recent_seconds, name: "client recent uptime threshold", min: 0)
|
|
14
|
+
unless [@relative_rssi_warning, @dbm_rssi_warning, @channel_usage_warning, @noise_floor_warning].all?(&:finite?) &&
|
|
15
|
+
@dbm_rssi_warning.negative? && @noise_floor_warning.negative? && (0..100).cover?(@channel_usage_warning)
|
|
16
|
+
raise InputError, "invalid wireless assessment threshold"
|
|
17
|
+
end
|
|
18
|
+
rescue ArgumentError, TypeError
|
|
19
|
+
raise InputError, "invalid wireless assessment threshold"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def call(snapshot, expected_ap_names: [])
|
|
23
|
+
raise InvalidSnapshot, "wireless assessment requires a Snapshot" unless snapshot.is_a?(Snapshot)
|
|
24
|
+
|
|
25
|
+
findings = []
|
|
26
|
+
snapshot.dimensions.each do |dimension|
|
|
27
|
+
next if %i[complete not_requested].include?(dimension.status)
|
|
28
|
+
findings << finding(:incomplete_collection, :unknown, dimension.name, nil, status: dimension.status)
|
|
29
|
+
end
|
|
30
|
+
snapshot.records(:access_points).each do |ap|
|
|
31
|
+
if ap.state && !/\A(?:run|running|online|normal|up)\z/i.match?(ap.state)
|
|
32
|
+
findings << finding(:ap_not_online, :critical, :access_points, ap.key, state: ap.state)
|
|
33
|
+
elsif ap.state.nil?
|
|
34
|
+
findings << finding(:ap_state_unknown, :unknown, :access_points, ap.key)
|
|
35
|
+
end
|
|
36
|
+
if ap.online_seconds && ap.online_seconds < @ap_recent_seconds
|
|
37
|
+
findings << finding(:ap_recently_online, :warning, :access_points, ap.key, online_seconds: ap.online_seconds)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
expected_ap_names.each { |name| Values.text(name, name: "expected AP name") }
|
|
41
|
+
missing = expected_ap_names - snapshot.records(:access_points).map(&:name)
|
|
42
|
+
missing.each do |name|
|
|
43
|
+
findings << finding(snapshot.dimension(:access_points).complete? ? :expected_ap_missing : :expected_ap_unconfirmed,
|
|
44
|
+
snapshot.dimension(:access_points).complete? ? :critical : :unknown, :access_points, Normalizer.key(name))
|
|
45
|
+
end
|
|
46
|
+
snapshot.records(:radios).each do |radio|
|
|
47
|
+
findings << finding(:radio_disabled, :warning, :radios, radio.key) if radio.admin_state && /\A(?:down|disabled)\z/i.match?(radio.admin_state)
|
|
48
|
+
if radio.channel_usage && radio.channel_usage > @channel_usage_warning
|
|
49
|
+
findings << finding(:radio_channel_busy, :warning, :radios, radio.key, channel_usage: radio.channel_usage)
|
|
50
|
+
end
|
|
51
|
+
if radio.noise_floor&.negative? && radio.noise_floor >= @noise_floor_warning
|
|
52
|
+
findings << finding(:radio_noise_high, :warning, :radios, radio.key, noise_floor: radio.noise_floor)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
snapshot.records(:clients).each do |client|
|
|
56
|
+
threshold = client.rssi_scale == :dbm ? @dbm_rssi_warning : @relative_rssi_warning
|
|
57
|
+
if client.rssi && client.rssi != 0 && client.rssi < threshold
|
|
58
|
+
findings << finding(:client_weak_signal, :warning, :clients, client.key, rssi: client.rssi, scale: client.rssi_scale)
|
|
59
|
+
end
|
|
60
|
+
if client.online_seconds && client.online_seconds < @client_recent_seconds
|
|
61
|
+
findings << finding(:client_recently_online, :warning, :clients, client.key, online_seconds: client.online_seconds)
|
|
62
|
+
end
|
|
63
|
+
roam = client.attributes[:roam_status]
|
|
64
|
+
if roam && /fail|abnormal|error|frequent|sticky/i.match?(roam)
|
|
65
|
+
findings << finding(:client_roam_abnormal, :warning, :clients, client.key, roam_status: roam)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
snapshot.view.diagnostics.each do |diagnostic|
|
|
69
|
+
findings << finding(diagnostic[:code], :unknown, :configuration, diagnostic[:record_key])
|
|
70
|
+
end
|
|
71
|
+
Assessment.new(target: snapshot.target, findings: findings, dimension_statuses: snapshot.dimensions.to_h { |dimension| [dimension.name, dimension.status] },
|
|
72
|
+
counts: snapshot.dimensions.to_h { |dimension| [dimension.name, dimension.records.size] })
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def finding(code, severity, dimension, record_key, **details)
|
|
78
|
+
Finding.new(key: Normalizer.key(code, dimension, record_key), code: code, severity: severity,
|
|
79
|
+
dimension: dimension, record_key: record_key, details: details)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Wireless
|
|
5
|
+
class AuthenticationConnection < Data.define(:key, :kind, :mac_address, :ap_name, :ap_id, :radio_id, :bssid, :ssid, :username, :ip_address, :interface, :domain, :protocol, :state, :initial_vlan_id, :authorized_vlan_id, :online_seconds)
|
|
6
|
+
def initialize(key:, kind:, mac_address:, ap_name: nil, ap_id: nil, radio_id: nil, bssid: nil, ssid: nil, username: nil, ip_address: nil, interface: nil, domain: nil, protocol: nil, state: nil, initial_vlan_id: nil, authorized_vlan_id: nil, online_seconds: nil)
|
|
7
|
+
key = Values.text(key, name: "wireless record key")
|
|
8
|
+
super(**Values.copy(key: key, kind: kind, mac_address: mac_address, ap_name: ap_name, ap_id: ap_id, radio_id: radio_id, bssid: bssid, ssid: ssid, username: username, ip_address: ip_address, interface: interface, domain: domain, protocol: protocol, state: state, initial_vlan_id: initial_vlan_id, authorized_vlan_id: authorized_vlan_id, online_seconds: online_seconds))
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Wireless
|
|
5
|
+
class Bss < Data.define(:key, :bssid, :ap_name, :ap_id, :radio_id, :ssid, :service_template, :vlan_id, :state, :client_count, :security)
|
|
6
|
+
def initialize(key:, bssid:, ap_name: nil, ap_id: nil, radio_id: nil, ssid: nil, service_template: nil, vlan_id: nil, state: nil, client_count: nil, security: {})
|
|
7
|
+
key = Values.text(key, name: "wireless record key")
|
|
8
|
+
super(**Values.copy(key: key, bssid: bssid, ap_name: ap_name, ap_id: ap_id, radio_id: radio_id, ssid: ssid, service_template: service_template, vlan_id: vlan_id, state: state, client_count: client_count, security: security))
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Wireless
|
|
5
|
+
class Client < Data.define(:key, :mac_address, :ap_name, :ap_id, :radio_id, :bssid, :ssid, :ip_address, :ipv6_address, :vlan_id, :rssi, :rssi_scale, :online_seconds, :rx_rate_bps, :tx_rate_bps, :authentication, :terminal, :attributes)
|
|
6
|
+
def initialize(key:, mac_address:, ap_name: nil, ap_id: nil, radio_id: nil, bssid: nil, ssid: nil, ip_address: nil, ipv6_address: nil, vlan_id: nil, rssi: nil, rssi_scale: nil, online_seconds: nil, rx_rate_bps: nil, tx_rate_bps: nil, authentication: {}, terminal: {}, attributes: {})
|
|
7
|
+
key = Values.text(key, name: "wireless record key")
|
|
8
|
+
super(**Values.copy(key: key, mac_address: mac_address, ap_name: ap_name, ap_id: ap_id, radio_id: radio_id, bssid: bssid, ssid: ssid, ip_address: ip_address, ipv6_address: ipv6_address, vlan_id: vlan_id, rssi: rssi, rssi_scale: rssi_scale, online_seconds: online_seconds, rx_rate_bps: rx_rate_bps, tx_rate_bps: tx_rate_bps, authentication: authentication, terminal: terminal, attributes: attributes))
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module Netstack
|
|
6
|
+
# 控制器观察、保留状态与评估;各维度分别记录证据。
|
|
7
|
+
module Wireless
|
|
8
|
+
# One controller, sequential read-only CLI. The caller owns credentials, scheduling and persistence.
|
|
9
|
+
class Collector
|
|
10
|
+
H3C_COMMANDS = {
|
|
11
|
+
access_points: "display wlan ap all verbose", radios: "display wlan ap all verbose",
|
|
12
|
+
clients: "display wlan client verbose", bsses: "display wlan bss all verbose",
|
|
13
|
+
dot1x_connections: "display dot1x connection", mab_connections: "display mac-authentication connection",
|
|
14
|
+
blocked_clients: "display wlan client-security block-mac",
|
|
15
|
+
configuration: "display current-configuration", ap_groups: "display current-configuration", service_templates: "display current-configuration"
|
|
16
|
+
}.freeze
|
|
17
|
+
HUAWEI_COMMANDS = { access_points: "display ap all", clients: "display station all" }.freeze
|
|
18
|
+
DEFAULT_DIMENSIONS = (Snapshot::DIMENSIONS - [:lldp]).freeze
|
|
19
|
+
|
|
20
|
+
def initialize(connector_factory: nil, clock: -> { Time.now.utc }, evidence_writer: nil)
|
|
21
|
+
@connector_factory = connector_factory
|
|
22
|
+
@clock = clock
|
|
23
|
+
@evidence_writer = evidence_writer
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# 传入的控制器连接仍归调用者管理;通过工厂创建的
|
|
27
|
+
# 控制器和 AP 会话由此处关闭;原始证据仅在显式启用时保存。
|
|
28
|
+
def call(target:, dimensions: DEFAULT_DIMENSIONS, connector: nil, ap_connector_factory: nil)
|
|
29
|
+
raise InputError, "wireless collection target must be a Target" unless target.is_a?(Target)
|
|
30
|
+
raise InputError, "wireless dimensions must be an Array" unless dimensions.is_a?(Array)
|
|
31
|
+
raise InputError, "invalid wireless dimension name" unless dimensions.all? { |name| name.is_a?(Symbol) || name.is_a?(String) }
|
|
32
|
+
requested = dimensions.map(&:to_sym).uniq
|
|
33
|
+
raise InputError, "unknown wireless collection dimension" unless (requested - Snapshot::DIMENSIONS).empty?
|
|
34
|
+
platform = platform_for(target)
|
|
35
|
+
requested |= [:access_points] if requested.include?(:lldp)
|
|
36
|
+
requested |= [:configuration] unless (requested & %i[ap_groups service_templates]).empty?
|
|
37
|
+
commands = platform == :h3c ? H3C_COMMANDS : platform == :huawei ? HUAWEI_COMMANDS : {}
|
|
38
|
+
collected = []
|
|
39
|
+
cache = {}
|
|
40
|
+
owned = connector.nil?
|
|
41
|
+
connection_error = nil
|
|
42
|
+
if commands.any? && requested.any? { |name| commands.key?(name) }
|
|
43
|
+
begin
|
|
44
|
+
connector ||= @connector_factory&.call(target)
|
|
45
|
+
raise InputError, "wireless connector is required" unless connector
|
|
46
|
+
connector.connect if owned
|
|
47
|
+
rescue StandardError => error
|
|
48
|
+
connection_error = error_code(error)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
requested.each do |name|
|
|
52
|
+
next if name == :lldp
|
|
53
|
+
unless commands.key?(name)
|
|
54
|
+
collected << unavailable(name, :unsupported, :unsupported_platform_dimension)
|
|
55
|
+
next
|
|
56
|
+
end
|
|
57
|
+
if connection_error
|
|
58
|
+
collected << unavailable(name, :failed, connection_error)
|
|
59
|
+
next
|
|
60
|
+
end
|
|
61
|
+
command = commands.fetch(name)
|
|
62
|
+
observation = cache[command] ||= read(connector, target, command)
|
|
63
|
+
collected << parse_observation(name, platform, observation)
|
|
64
|
+
end
|
|
65
|
+
if requested.include?(:lldp)
|
|
66
|
+
collected << collect_lldp(target, platform, collected, ap_connector_factory)
|
|
67
|
+
end
|
|
68
|
+
Snapshot.new(target: target, dimensions: collected, observed_at: now)
|
|
69
|
+
ensure
|
|
70
|
+
original_error = $!
|
|
71
|
+
if owned && connector
|
|
72
|
+
begin
|
|
73
|
+
connector.close
|
|
74
|
+
rescue StandardError
|
|
75
|
+
raise Error.new("wireless session cleanup failed", code: :cleanup_failed, stage: :collect) unless original_error
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def platform_for(target)
|
|
83
|
+
case target.platform.to_s.downcase
|
|
84
|
+
when "h3c", "h3c_wireless" then :h3c
|
|
85
|
+
when "huawei" then :huawei
|
|
86
|
+
else :unsupported
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def now = Values.time(@clock.call, name: "wireless collection clock")
|
|
91
|
+
|
|
92
|
+
def unavailable(name, status, code)
|
|
93
|
+
time = now
|
|
94
|
+
Dimension.new(name: name, status: status, started_at: time, completed_at: time, diagnostics: [{ code: code }])
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def read(connector, target, command)
|
|
98
|
+
started_at = now
|
|
99
|
+
prompt = connector.current_prompt
|
|
100
|
+
raise InputError, "wireless collection requires a confirmed prompt" unless prompt.is_a?(String) && !prompt.empty?
|
|
101
|
+
|
|
102
|
+
exact_prompt = /(?:\A|(?<=[\r\n]))[ \t\x00]*#{Regexp.escape(prompt)}[ \t\x00]*[\r\n]*\z/
|
|
103
|
+
result = connector.execute_operation(Connector::Script.new([Connector::Command.new(command, output_sensitive: true)]),
|
|
104
|
+
name: :wireless_collection, prompt: exact_prompt, privilege: false)
|
|
105
|
+
raw = result.output
|
|
106
|
+
evidence = { command: command, sha256: Digest::SHA256.hexdigest(raw), bytes: raw.bytesize }
|
|
107
|
+
if @evidence_writer
|
|
108
|
+
evidence[:reference] = Values.copy(@evidence_writer.call(target: target, command: command, output: raw))
|
|
109
|
+
end
|
|
110
|
+
return { started_at: started_at, completed_at: now, evidence: [evidence], error: error_code(result.error) } unless result.success?
|
|
111
|
+
|
|
112
|
+
step = result.steps.last
|
|
113
|
+
raise InputError, "wireless collection has no completed command" unless step
|
|
114
|
+
body = Connector::TerminalRenderer.render(step.output.delete_suffix(step.prompt.to_s))
|
|
115
|
+
lines = body.lines
|
|
116
|
+
lines.shift while lines.first&.strip == ""
|
|
117
|
+
lines.shift if lines.first&.strip == command
|
|
118
|
+
{ text: lines.join, started_at: started_at, completed_at: now, evidence: [evidence] }
|
|
119
|
+
rescue StandardError => error
|
|
120
|
+
{ started_at: started_at || now, completed_at: now, evidence: [], error: error_code(error) }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def parse_observation(name, platform, observation)
|
|
124
|
+
metadata = observation.slice(:started_at, :completed_at, :evidence)
|
|
125
|
+
return Dimension.new(name: name, status: :failed, diagnostics: [{ code: observation[:error] }], **metadata) if observation[:error]
|
|
126
|
+
|
|
127
|
+
parsed = if %i[configuration ap_groups service_templates].include?(name)
|
|
128
|
+
H3c::ConfigParser.new.parse(observation.fetch(:text), terminated: true)
|
|
129
|
+
else
|
|
130
|
+
parser = platform == :h3c ? H3c::RuntimeParser.new : Huawei::RuntimeParser.new
|
|
131
|
+
parser.parse(name, observation.fetch(:text), terminated: true)
|
|
132
|
+
end
|
|
133
|
+
records = parsed.records
|
|
134
|
+
records = records.first&.public_send(name) || [] if %i[ap_groups service_templates].include?(name)
|
|
135
|
+
records = [] if parsed.status == :failed
|
|
136
|
+
Dimension.new(name: name, status: parsed.status, records: records, diagnostics: parsed.diagnostics, **metadata)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def collect_lldp(target, platform, collected, factory)
|
|
140
|
+
return unavailable(:lldp, :unsupported, :unsupported_platform_dimension) unless platform == :h3c
|
|
141
|
+
return unavailable(:lldp, :failed, :ap_connector_required) unless factory
|
|
142
|
+
|
|
143
|
+
aps = collected.find { |dimension| dimension.name == :access_points }
|
|
144
|
+
return unavailable(:lldp, :failed, :access_points_unavailable) if aps.unavailable?
|
|
145
|
+
|
|
146
|
+
started_at = now
|
|
147
|
+
records = []
|
|
148
|
+
evidence = []
|
|
149
|
+
diagnostics = []
|
|
150
|
+
complete = aps.complete?
|
|
151
|
+
aps.records.each do |ap|
|
|
152
|
+
device = nil
|
|
153
|
+
begin
|
|
154
|
+
device = factory.call(target, ap)
|
|
155
|
+
raise InputError, "AP connector unavailable" unless device
|
|
156
|
+
device.connect
|
|
157
|
+
observation = read(device, target, "display lldp neighbor-information verbose")
|
|
158
|
+
evidence.concat(observation[:evidence].map { |item| item.merge(ap_name: ap.name) })
|
|
159
|
+
if observation[:error]
|
|
160
|
+
complete = false
|
|
161
|
+
diagnostics << { code: observation[:error], ap_name: ap.name }
|
|
162
|
+
next
|
|
163
|
+
end
|
|
164
|
+
parsed = H3c::LldpParser.new.parse(observation.fetch(:text), ap_name: ap.name, terminated: true)
|
|
165
|
+
complete &&= parsed.status == :complete
|
|
166
|
+
records.concat(parsed.records)
|
|
167
|
+
diagnostics.concat(parsed.diagnostics.map { |item| item.merge(ap_name: ap.name) })
|
|
168
|
+
rescue StandardError => error
|
|
169
|
+
complete = false
|
|
170
|
+
diagnostics << { code: error_code(error), ap_name: ap.name }
|
|
171
|
+
ensure
|
|
172
|
+
begin
|
|
173
|
+
device&.close
|
|
174
|
+
rescue StandardError
|
|
175
|
+
complete = false
|
|
176
|
+
diagnostics << { code: :cleanup_failed, ap_name: ap.name }
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
diagnostics << { code: :partial_access_point_coverage } unless aps.complete?
|
|
181
|
+
status = complete ? :complete : records.empty? ? :failed : :partial
|
|
182
|
+
Dimension.new(name: :lldp, status: status, records: records, started_at: started_at, completed_at: now,
|
|
183
|
+
evidence: evidence, diagnostics: diagnostics)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def error_code(error)
|
|
187
|
+
error.respond_to?(:code) && error.code.is_a?(Symbol) ? error.code : :collection_failed
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Wireless
|
|
5
|
+
# 控制器配置的结构化观察;完整性由外层 Dimension 表达,不能从空集合推断。
|
|
6
|
+
class Configuration < Data.define(:key, :access_points, :ap_groups, :service_templates, :global_settings, :blacklist, :whitelist)
|
|
7
|
+
def initialize(key: "controller", access_points: {}, ap_groups: [], service_templates: [], global_settings: {}, blacklist: [], whitelist: [])
|
|
8
|
+
super(**Values.copy(key: key, access_points: access_points, ap_groups: ap_groups, service_templates: service_templates,
|
|
9
|
+
global_settings: global_settings, blacklist: blacklist, whitelist: whitelist))
|
|
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 Finding < Data.define(:key, :code, :severity, :dimension, :record_key, :details)
|
|
6
|
+
def initialize(key:, code:, severity:, dimension:, record_key: nil, details: {})
|
|
7
|
+
key = Values.text(key, name: "wireless record key")
|
|
8
|
+
super(**Values.copy(key: key, code: code, severity: severity, dimension: dimension, record_key: record_key, details: details))
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Wireless
|
|
5
|
+
module H3c
|
|
6
|
+
# Wireless sections only. Unknown wireless statements remain visible without copying secrets.
|
|
7
|
+
class ConfigParser
|
|
8
|
+
# rubocop:disable Naming/PredicateMethod -- These methods consume a line and mutate the current parse record.
|
|
9
|
+
def parse(content, terminated: false)
|
|
10
|
+
reader = Netstack::Configuration::Reader.new(Normalizer.text(content), vendor: "h3c")
|
|
11
|
+
aps = {}
|
|
12
|
+
groups = {}
|
|
13
|
+
templates = {}
|
|
14
|
+
global = {}
|
|
15
|
+
lists = { blacklist: [], whitelist: [] }
|
|
16
|
+
diagnostics = []
|
|
17
|
+
section = kind = radio = model = nil
|
|
18
|
+
recognized = false
|
|
19
|
+
reader.lines.each_with_index do |raw, index|
|
|
20
|
+
line = raw.strip
|
|
21
|
+
next if line.empty?
|
|
22
|
+
if line == "#"
|
|
23
|
+
section = kind = radio = model = nil
|
|
24
|
+
next
|
|
25
|
+
end
|
|
26
|
+
begin
|
|
27
|
+
if (match = /\Awlan ap-group (\S+)(?: type (.+))?\z/i.match(line))
|
|
28
|
+
name = match[1]
|
|
29
|
+
raise InputError, "duplicate AP group" if groups.key?(name)
|
|
30
|
+
section = groups[name] = { name: name, ap_names: [], serial_ids: [], vlan_ids: [], model_profiles: {}, configuration: {} }
|
|
31
|
+
section[:configuration][:type] = match[2] if match[2]
|
|
32
|
+
kind = :group
|
|
33
|
+
radio = model = nil
|
|
34
|
+
recognized = true
|
|
35
|
+
elsif (match = /\Awlan ap (\S+) model (.+)\z/i.match(line))
|
|
36
|
+
name = match[1]
|
|
37
|
+
raise InputError, "duplicate configured AP" if aps.key?(name)
|
|
38
|
+
section = aps[name] = { name: name, model: match[2], radios: {} }
|
|
39
|
+
kind = :ap
|
|
40
|
+
radio = model = nil
|
|
41
|
+
recognized = true
|
|
42
|
+
elsif (match = /\Awlan service-template (\S+)\z/i.match(line))
|
|
43
|
+
name = match[1]
|
|
44
|
+
raise InputError, "duplicate service template" if templates.key?(name)
|
|
45
|
+
section = templates[name] = { name: name, akm_modes: [], cipher_suites: [], security_ies: [] }
|
|
46
|
+
kind = :template
|
|
47
|
+
radio = model = nil
|
|
48
|
+
recognized = true
|
|
49
|
+
elsif /\A(?:undo )?(?:wlan|dot1x|mac-authentication)\b/i.match?(line) && !section && !/\A\s/.match?(raw)
|
|
50
|
+
recognized = true
|
|
51
|
+
unknown(diagnostics, index) unless global_line(global, lists, line)
|
|
52
|
+
elsif section
|
|
53
|
+
if kind == :group && (match = /\Aap-model (.+)\z/i.match(line))
|
|
54
|
+
model = section[:model_profiles][match[1]] ||= { radios: {} }
|
|
55
|
+
radio = nil
|
|
56
|
+
elsif %i[ap group].include?(kind) && (match = /\Aradio (\d+)\z/i.match(line))
|
|
57
|
+
parent = kind == :ap ? section : model
|
|
58
|
+
raise InputError, "group radio requires AP model" unless parent
|
|
59
|
+
radio = (parent[:radios] ||= {})[match[1].to_i] ||= { service_templates: [] }
|
|
60
|
+
elsif radio
|
|
61
|
+
unknown(diagnostics, index) unless radio_line(radio, line)
|
|
62
|
+
elsif kind == :template
|
|
63
|
+
unknown(diagnostics, index) unless template_line(section, line)
|
|
64
|
+
elsif model
|
|
65
|
+
if (match = /\Amap-configuration (.+)\z/i.match(line))
|
|
66
|
+
model[:map_configuration] = match[1]
|
|
67
|
+
else
|
|
68
|
+
unknown(diagnostics, index)
|
|
69
|
+
end
|
|
70
|
+
elsif kind == :ap
|
|
71
|
+
unknown(diagnostics, index) unless ap_line(section, line)
|
|
72
|
+
else
|
|
73
|
+
unknown(diagnostics, index) unless group_line(section, line)
|
|
74
|
+
end
|
|
75
|
+
elsif /\Areturn\z/i.match?(line)
|
|
76
|
+
recognized = true
|
|
77
|
+
end
|
|
78
|
+
reader.mark(index)
|
|
79
|
+
rescue InputError
|
|
80
|
+
diagnostics << { code: :invalid_configuration_line, line: index + 1 }
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
diagnostics << { code: :missing_completeness_evidence } unless terminated || reader.lines.any? { |line| line.strip.casecmp?("return") }
|
|
84
|
+
diagnostics << { code: :unrecognized_configuration } unless recognized
|
|
85
|
+
group_records = groups.map { |name, attributes| ApGroup.new(key: Normalizer.key(name), **attributes) }
|
|
86
|
+
template_records = templates.map do |name, attributes|
|
|
87
|
+
ServiceTemplate.new(key: Normalizer.key(name), name: name, ssid: attributes[:ssid], vlan_id: attributes[:vlan_id],
|
|
88
|
+
enabled: attributes[:enabled], authentication_type: authentication_type(attributes), configuration: attributes)
|
|
89
|
+
end
|
|
90
|
+
config = Configuration.new(access_points: aps, ap_groups: group_records, service_templates: template_records,
|
|
91
|
+
global_settings: global, **lists.transform_values { |records| records.uniq { |record| record[:mac_address] } })
|
|
92
|
+
config.access_points.each_value { |ap| ServiceTemplateBindings.validate_radios!(ap.fetch(:radios, {})) }
|
|
93
|
+
ServiceTemplateBindings.validate_groups!(config.ap_groups)
|
|
94
|
+
ParseResult.new(records: recognized ? [config] : [], status: diagnostics.empty? ? :complete : recognized ? :partial : :failed,
|
|
95
|
+
diagnostics: diagnostics)
|
|
96
|
+
rescue InputError => error
|
|
97
|
+
ParseResult.new(records: [], status: :failed, diagnostics: [{ code: :invalid_configuration, error: error.code }])
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def unknown(diagnostics, index) = diagnostics << { code: :unrecognized_wireless_configuration, line: index + 1 }
|
|
103
|
+
|
|
104
|
+
def common_line(record, line)
|
|
105
|
+
case line
|
|
106
|
+
when /\Aclient keep-alive (enable|disable)\z/i then record[:client_keep_alive] = Regexp.last_match(1).casecmp?("enable")
|
|
107
|
+
when /\Aclient keep-alive interval (\d+)\z/i then record[:client_keep_alive_interval] = Regexp.last_match(1).to_i
|
|
108
|
+
when /\Aclient-rate-limit (enable|disable)\z/i then record[:client_rate_limit_enabled] = Regexp.last_match(1).casecmp?("enable")
|
|
109
|
+
when /\Aclient-rate-limit (inbound|outbound) mode (\S+) cir (\d+)\z/i
|
|
110
|
+
(record[:client_rate_limit] ||= {})[Regexp.last_match(1).downcase.to_sym] = { mode: Regexp.last_match(2), cir_bps: Regexp.last_match(3).to_i * 1000 }
|
|
111
|
+
else return false
|
|
112
|
+
end
|
|
113
|
+
true
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def ap_line(record, line)
|
|
117
|
+
return true if common_line(record, line)
|
|
118
|
+
|
|
119
|
+
case line
|
|
120
|
+
when /\Aserial-id (\S+)\z/i then record[:serial_id] = Regexp.last_match(1)
|
|
121
|
+
when /\Adescription (.+)\z/i then record[:description] = Regexp.last_match(1)
|
|
122
|
+
when /\Avlan (\d+)\z/i then record[:vlan_id] = Normalizer.integer(Regexp.last_match(1), max: 4094)
|
|
123
|
+
when /\A(undo )?band-navigation enable\z/i then record[:band_navigation] = Regexp.last_match(1).nil?
|
|
124
|
+
when /\A(undo )?radio-load-balance band-navigation enable\z/i then record[:radio_load_balance_band_navigation] = Regexp.last_match(1).nil?
|
|
125
|
+
when /\Aclient max-count (\d+)\z/i then record[:client_max_count] = Regexp.last_match(1).to_i
|
|
126
|
+
else return false
|
|
127
|
+
end
|
|
128
|
+
true
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def group_line(record, line)
|
|
132
|
+
return true if common_line(record[:configuration], line)
|
|
133
|
+
|
|
134
|
+
case line
|
|
135
|
+
when /\Avlan (\d+)\z/i then record[:vlan_ids] << Normalizer.integer(Regexp.last_match(1), max: 4094)
|
|
136
|
+
when /\Aserial-id (\S+)\z/i then record[:serial_ids] << Regexp.last_match(1)
|
|
137
|
+
when /\Aap (\S+)\z/i then record[:ap_names] << Regexp.last_match(1)
|
|
138
|
+
when /\Awlan tunnel-preempt enable\z/i then record[:configuration][:tunnel_preempt] = true
|
|
139
|
+
when /\Abroadcast-probe reply disable\z/i then record[:configuration][:broadcast_probe_reply] = false
|
|
140
|
+
when /\Abackup-ac ip (\S+)\z/i then record[:configuration][:backup_ac_ip] = Normalizer.ip(Regexp.last_match(1))
|
|
141
|
+
else return false
|
|
142
|
+
end
|
|
143
|
+
true
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def radio_line(record, line)
|
|
147
|
+
return true if common_line(record, line)
|
|
148
|
+
|
|
149
|
+
case line
|
|
150
|
+
when /\Achannel band-width (.+)\z/i then record[:channel_bandwidth] = Regexp.last_match(1)
|
|
151
|
+
when /\Amax-power (\d+(?:\.\d+)?)\z/i then record[:max_power] = Float(Regexp.last_match(1))
|
|
152
|
+
when /\Aradio (enable|disable)\z/i then record[:enabled] = Regexp.last_match(1).casecmp?("enable")
|
|
153
|
+
when /\Achannel (.+)\z/i
|
|
154
|
+
record[:channel] = Regexp.last_match(1)
|
|
155
|
+
record[:channel_number] = Normalizer.channel(record[:channel])
|
|
156
|
+
when /\Aradar-detect disable\z/i then record[:radar_detect_disabled] = true
|
|
157
|
+
when /\Asacp anti-sticky enable(?: rssi (-?\d+(?:\.\d+)?))? interval (\d+)\z/i
|
|
158
|
+
record[:anti_sticky] = { enabled: true, rssi: Regexp.last_match(1)&.to_f, interval: Regexp.last_match(2).to_i }
|
|
159
|
+
when /\Arate (mandatory|supported|disabled) (.+)\z/i
|
|
160
|
+
kind, values = Regexp.last_match.captures
|
|
161
|
+
(record[:rates] ||= {})[kind.downcase.to_sym] = values.split.map { |value| Normalizer.number(value) }
|
|
162
|
+
when /\Arate multicast (.+)\z/i then (record[:rates] ||= {})[:multicast] = Regexp.last_match(1)
|
|
163
|
+
when /\Aoption client (reject|reconnect) enable rssi (-?\d+(?:\.\d+)?)\z/i
|
|
164
|
+
(record[:client_options] ||= {})[Regexp.last_match(1).downcase.to_sym] = Float(Regexp.last_match(2))
|
|
165
|
+
when /\Aservice-template (\S+)(?: vlan (\d+))?\z/i
|
|
166
|
+
record[:service_templates] << { name: Regexp.last_match(1), vlan_id: Normalizer.integer(Regexp.last_match(2), max: 4094) }
|
|
167
|
+
else return false
|
|
168
|
+
end
|
|
169
|
+
true
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def template_line(record, line)
|
|
173
|
+
return true if common_line(record, line)
|
|
174
|
+
|
|
175
|
+
case line
|
|
176
|
+
when /\A(description|ssid) (.+)\z/i then record[Regexp.last_match(1).downcase.to_sym] = Regexp.last_match(2)
|
|
177
|
+
when /\A(undo )?beacon ssid-hide\z/i then record[:ssid_hidden] = Regexp.last_match(1).nil?
|
|
178
|
+
when /\Avlan (\d+)\z/i then record[:vlan_id] = Normalizer.integer(Regexp.last_match(1), max: 4094)
|
|
179
|
+
when /\Aclient forwarding-location (\S+)\z/i then record[:forwarding_location] = Regexp.last_match(1)
|
|
180
|
+
when /\Aclient cache aging-time (\d+)\z/i then record[:client_cache_aging_time] = Regexp.last_match(1).to_i
|
|
181
|
+
when /\Apacket-filter name (\S+) (inbound|outbound)\z/i
|
|
182
|
+
(record[:packet_filters] ||= {})[Regexp.last_match(2).downcase.to_sym] = Regexp.last_match(1)
|
|
183
|
+
when /\A(undo )?bss transition-management enable\z/i then record[:btm_enabled] = Regexp.last_match(1).nil?
|
|
184
|
+
when /\Abss transition-management disassociation forced timer (\d+)\z/i then record[:btm_disassociation_timer] = Regexp.last_match(1).to_i
|
|
185
|
+
when /\Aakm mode (.+)\z/i then record[:akm_modes] |= Regexp.last_match(1).downcase.gsub("802.1x", "dot1x").split
|
|
186
|
+
when /\Acipher-suite (.+)\z/i then record[:cipher_suites] |= Regexp.last_match(1).downcase.split
|
|
187
|
+
when /\Asecurity-ie (.+)\z/i then record[:security_ies] |= Regexp.last_match(1).downcase.split
|
|
188
|
+
when /\Apreshared-key\s+.+\z/i then record[:has_preshared_key] = true
|
|
189
|
+
when /\Aclient-security authentication-mode (.+)\z/i then record[:client_security_authentication_mode] = Regexp.last_match(1).downcase
|
|
190
|
+
when /\Aclient-security (ignore-authentication|ignore-authorization)\z/i then record[Regexp.last_match(1).tr("-", "_").to_sym] = true
|
|
191
|
+
when /\Adot1x domain (\S+)\z/i then record[:dot1x_domain] = Regexp.last_match(1)
|
|
192
|
+
when /\Amac-authentication domain (\S+)\z/i then record[:mac_authentication_domain] = Regexp.last_match(1)
|
|
193
|
+
when /\Aportal (enable method|domain|bas-ip|apply web-server|apply mac-trigger-server) (\S+)\z/i
|
|
194
|
+
(record[:portal] ||= {})[Regexp.last_match(1).tr(" -", "__").to_sym] = Regexp.last_match(2)
|
|
195
|
+
when /\A(undo )?service-template enable\z/i then record[:enabled] = Regexp.last_match(1).nil?
|
|
196
|
+
else return false
|
|
197
|
+
end
|
|
198
|
+
true
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def global_line(global, lists, line)
|
|
202
|
+
case line
|
|
203
|
+
when /\A(undo )?wlan (auto-ap|auto-persistent) enable\z/i then global[Regexp.last_match(2).tr("-", "_").to_sym] = Regexp.last_match(1).nil?
|
|
204
|
+
when /\A(undo )?dot1x\z/i then global[:dot1x_enabled] = Regexp.last_match(1).nil?
|
|
205
|
+
when /\Adot1x authentication-method (.+)\z/i then global[:dot1x_authentication_method] = Regexp.last_match(1)
|
|
206
|
+
when /\A(undo )?mac-authentication\z/i then global[:mac_authentication_enabled] = Regexp.last_match(1).nil?
|
|
207
|
+
when /\Amac-authentication user-name-format (.+)\z/i then global[:mac_authentication_user_name_format] = Regexp.last_match(1)
|
|
208
|
+
when /\Awlan (?:client-security )?static-(black|white)list mac-address (\S+)(?: (.+))?\z/i
|
|
209
|
+
kind, mac, reason = Regexp.last_match.captures
|
|
210
|
+
lists[kind.downcase == "black" ? :blacklist : :whitelist] << { mac_address: Normalizer.mac(mac), reason: reason }
|
|
211
|
+
else return false
|
|
212
|
+
end
|
|
213
|
+
true
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def authentication_type(record)
|
|
217
|
+
mode = record[:client_security_authentication_mode]
|
|
218
|
+
return :dot1x if mode == "dot1x" || record[:akm_modes].include?("dot1x") || record[:dot1x_domain]
|
|
219
|
+
return :mab if mode == "mac" || record[:mac_authentication_domain]
|
|
220
|
+
return :psk if record[:akm_modes].include?("psk") || record[:has_preshared_key]
|
|
221
|
+
return :portal if record[:portal]&.key?(:enable_method)
|
|
222
|
+
|
|
223
|
+
nil
|
|
224
|
+
end
|
|
225
|
+
# rubocop:enable Naming/PredicateMethod
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|